Skip to content

Commit a96d81a

Browse files
committed
CHECKPOINT
1 parent 33b68a9 commit a96d81a

File tree

9 files changed

+187
-66
lines changed

9 files changed

+187
-66
lines changed

next/app/api/speak/route.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { NextResponse } from "next/server";
2-
import { tts, CartesiaModelId, SUPPORTED_LANGUAGES, VoiceId, GENDERS } from "@/lib/cartesia";
2+
import { tts, CartesiaModelId, CartesiaLanguages, VoiceId, GENDERS } from "@/lib/cartesia";
33
import { z } from "zod";
44

55
const baseSchema = z.object({
66
text: z.string().min(1),
7-
language: z.enum(SUPPORTED_LANGUAGES).optional(),
7+
language: z.enum(CartesiaLanguages).optional(),
88
modelId: z.nativeEnum(CartesiaModelId).optional(),
99
});
1010

next/components/fragment.tsx

+6-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Language } from "@/lib/types";
1+
import { Language, toKaikkiLanguage, getCountryCode } from "@/lib/types";
22
import { Skeleton } from "./ui/skeleton";
33
import { Button } from "./ui/button";
44
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "./ui/resizable";
@@ -13,8 +13,9 @@ import {
1313
} from "./ui/drawer";
1414
import { useEffect, useState } from "react";
1515
import { useMediaQuery } from "@/hooks/use-media-query";
16-
import type { KaikkiEntry, KaikkiLanguage } from "@/lib/kaikki";
16+
import type { KaikkiEntry } from "@/lib/kaikki";
1717
import { cleanWord, buildKaikkiHtmlUrl } from "@/lib/kaikki";
18+
import * as Flags from 'country-flag-icons/react/3x2';
1819

1920
export type Fragment = {
2021
id: string;
@@ -38,15 +39,6 @@ export const FragmentComponent = ({ fragment, showTranslation, outputLanguage }:
3839
const [isLoading, setIsLoading] = useState(false);
3940
const isDesktop = useMediaQuery("(min-width: 768px)");
4041

41-
// Map our Language enum to Kaikki language
42-
const languageMap: Record<Language, KaikkiLanguage> = {
43-
[Language.ENGLISH_US]: "English",
44-
[Language.SPANISH_MX]: "Spanish",
45-
[Language.CHINESE_CN]: "Chinese",
46-
[Language.ITALIAN]: "Italian",
47-
// Add other languages as needed
48-
};
49-
5042
useEffect(() => {
5143
const handleEsc = (e: KeyboardEvent) => {
5244
if (e.key === "Escape") {
@@ -77,8 +69,9 @@ export const FragmentComponent = ({ fragment, showTranslation, outputLanguage }:
7769
setDefinitions([]);
7870

7971
try {
72+
const kaikkiLang = toKaikkiLanguage(outputLanguage);
8073
const res = await fetch(
81-
`/api/define?word=${encodeURIComponent(cleaned)}&language=${languageMap[outputLanguage] || "English"}`
74+
`/api/define?word=${encodeURIComponent(cleaned)}&language=${kaikkiLang}`
8275
);
8376
if (!res.ok) throw new Error('Failed to fetch definition');
8477
const data = await res.json();
@@ -135,7 +128,7 @@ export const FragmentComponent = ({ fragment, showTranslation, outputLanguage }:
135128
<div key={i} className="mb-6 last:mb-0">
136129
<div className="flex items-center gap-2 mb-2">
137130
<a
138-
href={buildKaikkiHtmlUrl(entry.word, languageMap[outputLanguage] || "English")}
131+
href={buildKaikkiHtmlUrl(entry.word, toKaikkiLanguage(outputLanguage))}
139132
target="_blank"
140133
rel="noopener noreferrer"
141134
className="font-bold hover:underline"

next/components/language-select.tsx

+2-11
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,6 @@ interface LanguageSelectProps {
1717
type: "input" | "output";
1818
}
1919

20-
// Map language codes to country codes for flags
21-
const LANGUAGE_TO_FLAG: Record<Language, keyof typeof Flags> = {
22-
[Language.ENGLISH_US]: 'US',
23-
[Language.SPANISH_MX]: 'MX',
24-
[Language.CHINESE_CN]: 'CN',
25-
[Language.ITALIAN]: 'IT',
26-
// Add more mappings as needed
27-
};
28-
2920
export const LanguageSelect = ({ value, onChange, type }: LanguageSelectProps) => {
3021
return (
3122
<div className="flex items-center gap-2 w-full">
@@ -39,11 +30,11 @@ export const LanguageSelect = ({ value, onChange, type }: LanguageSelectProps) =
3930
</SelectTrigger>
4031
<SelectContent>
4132
{LANGUAGE_INFO.map((lang) => {
42-
const ItemFlagIcon = Flags[LANGUAGE_TO_FLAG[lang.code as Language]];
33+
const FlagIcon = Flags[lang.countryCode as keyof typeof Flags];
4334
return (
4435
<SelectItem key={lang.code} value={lang.code} className="text-lg">
4536
<div className="flex items-center gap-2">
46-
<ItemFlagIcon className="w-6 h-4" />
37+
{FlagIcon && <FlagIcon className="w-6 h-4" />}
4738
{lang.name}
4839
</div>
4940
</SelectItem>

next/lib/cartesia.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,18 @@ export enum CartesiaModelId {
7070
SONIC_PREVIEW = "sonic-preview", // latest features, may be unstable
7171
}
7272

73-
export const SUPPORTED_LANGUAGES = [
73+
export const CartesiaLanguages = [
7474
"en", "fr", "de", "es", "pt", "zh", "ja",
7575
"hi", "it", "ko", "nl", "pl", "ru", "sv", "tr"
7676
] as const;
7777

78-
export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
78+
export type CartesiaLanguage = typeof CartesiaLanguages[number];
7979

8080
export type Gender = "male" | "female";
8181
export const GENDERS = ["male", "female"] as const;
8282

8383
// Voice mapping by language and gender
84-
export const VOICE_MAP: Record<SupportedLanguage, Record<Gender, VoiceId>> = {
84+
export const VOICE_MAP: Record<CartesiaLanguage, Record<Gender, VoiceId>> = {
8585
en: {
8686
male: VoiceId.NATHAN,
8787
female: VoiceId.SARAH
@@ -146,7 +146,7 @@ export const VOICE_MAP: Record<SupportedLanguage, Record<Gender, VoiceId>> = {
146146

147147
interface BaseTTSOptions {
148148
text: string;
149-
language?: SupportedLanguage;
149+
language?: CartesiaLanguage;
150150
modelId?: CartesiaModelId;
151151
}
152152

next/lib/cartesia.unit.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { describe, it, expect } from "bun:test";
2-
import { SUPPORTED_LANGUAGES, GENDERS, VOICE_MAP, type SupportedLanguage, type Gender } from "./cartesia";
2+
import { CartesiaLanguages, GENDERS, VOICE_MAP, type CartesiaLanguage, type Gender } from "./cartesia";
33

44
describe("cartesia voice coverage", () => {
55
it("should have male and female voices for all supported languages", () => {
66
// Check each language has both male and female voices
7-
SUPPORTED_LANGUAGES.forEach((lang: SupportedLanguage) => {
7+
CartesiaLanguages.forEach((lang: CartesiaLanguage) => {
88
GENDERS.forEach((gender: Gender) => {
99
const voiceId = VOICE_MAP[lang][gender];
1010
expect(voiceId).toBeDefined();

next/lib/kaikki.ts

+30-20
Original file line numberDiff line numberDiff line change
@@ -29,37 +29,47 @@ export function cleanWord(word: string): string {
2929
return result.toLowerCase();
3030
}
3131

32+
// from https://kaikki.org
3233
export type KaikkiLanguage =
34+
// Primary dictionaries
3335
| "English"
34-
| "Spanish"
35-
| "French"
36-
| "German"
37-
| "Russian"
38-
| "Japanese"
39-
| "Chinese"
36+
| "Spanish"
4037
| "Italian"
38+
| "German"
39+
| "Russian"
4140
| "Portuguese"
42-
| "Swedish"
43-
| "Finnish"
4441
| "Polish"
42+
| "French"
43+
| "Catalan"
44+
| "Swedish"
45+
| "Latvian"
46+
| "Lithuanian"
4547
| "Dutch"
46-
| "Korean"
47-
| "Vietnamese"
48-
| "Turkish"
49-
| "Hindi"
50-
| "Arabic"
51-
| "Thai"
48+
| "Romanian"
5249
| "Greek"
5350
| "Hungarian"
51+
| "Bulgarian"
5452
| "Czech"
55-
| "Danish"
56-
| "Norwegian"
5753
| "Ukrainian"
54+
| "Irish"
55+
| "Latin"
56+
// Additional downloadable dictionaries
57+
| "Arabic"
58+
| "Armenian"
59+
| "Azerbaijani"
60+
| "Cebuano"
61+
| "Chinese"
5862
| "Hebrew"
59-
| "Indonesian"
60-
| "Romanian"
61-
| "Malay"
62-
| "Persian";
63+
| "Hindi"
64+
| "Japanese"
65+
| "Korean"
66+
| "Marathi"
67+
| "Tagalog"
68+
| "Tamil"
69+
| "Telugu"
70+
| "Turkish"
71+
| "Urdu"
72+
| "Vietnamese";
6373

6474
function buildKaikkiKey(word: string, language: KaikkiLanguage): string {
6575
return `kaikki:${language.toLowerCase()}:${cleanWord(word)}`;

next/lib/types.test.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, it, expect } from "bun:test";
2+
import { Language, normalizeLanguage, normalizeKaikkiLanguage, KAIKKI_LANGUAGES } from "./types";
3+
import { CartesiaLanguages } from "./cartesia";
4+
5+
describe("language compatibility", () => {
6+
it("our Language enum should be a subset of Cartesia supported languages", () => {
7+
const ourLangs = Object.values(Language).map(normalizeLanguage);
8+
const cartesiaLangs = CartesiaLanguages;
9+
10+
for (const lang of ourLangs) {
11+
expect(cartesiaLangs).toContain(lang);
12+
}
13+
});
14+
15+
it("our Language enum should be a subset of Kaikki supported languages", () => {
16+
const ourLangs = Object.values(Language).map(normalizeLanguage);
17+
const kaikkiLangs = KAIKKI_LANGUAGES.map(normalizeKaikkiLanguage);
18+
19+
for (const lang of ourLangs) {
20+
expect(kaikkiLangs).toContain(lang);
21+
}
22+
});
23+
});

next/lib/types.ts

+106-8
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,121 @@
11
export enum Language {
22
ENGLISH_US = "en-US",
33
SPANISH_MX = "es-MX",
4-
CHINESE_CN = "zh-CN",
54
ITALIAN = "it-IT",
5+
JAPANESE = "ja-JP",
6+
FRENCH = "fr-FR",
7+
CHINESE_CN = "zh-CN",
8+
GERMAN = "de-DE",
9+
DUTCH = "nl-NL",
10+
PORTUGUESE = "pt-PT",
11+
RUSSIAN = "ru-RU",
12+
HINDI = "hi-IN",
13+
KOREAN = "ko-KR",
14+
SWEDISH = "sv-SE",
15+
TURKISH = "tr-TR",
16+
POLISH = "pl-PL"
617
}
718

819
interface LanguageInfo {
920
code: Language;
1021
name: string;
11-
order: number;
22+
countryCode: string; // ISO 3166-1 alpha-2 country code for flags
23+
kaikkiName: string; // Name in Kaikki dictionary format
1224
}
1325

26+
// ordered by preference/importance
1427
export const LANGUAGE_INFO: LanguageInfo[] = [
15-
{ code: Language.ENGLISH_US, name: "English (US)", order: 1 },
16-
{ code: Language.SPANISH_MX, name: "Spanish (Mexico)", order: 2 },
17-
{ code: Language.CHINESE_CN, name: "Chinese (Mandarin)", order: 3 },
18-
{ code: Language.ITALIAN, name: "Italian", order: 4 },
19-
].sort((a, b) => a.order - b.order);
28+
{ code: Language.ENGLISH_US, name: "English (US)", countryCode: "US", kaikkiName: "English" },
29+
{ code: Language.SPANISH_MX, name: "Spanish (Mexico)", countryCode: "MX", kaikkiName: "Spanish" },
30+
{ code: Language.ITALIAN, name: "Italian", countryCode: "IT", kaikkiName: "Italian" },
31+
{ code: Language.JAPANESE, name: "Japanese", countryCode: "JP", kaikkiName: "Japanese" },
32+
{ code: Language.FRENCH, name: "French", countryCode: "FR", kaikkiName: "French" },
33+
{ code: Language.CHINESE_CN, name: "Chinese (Mandarin)", countryCode: "CN", kaikkiName: "Chinese" },
34+
{ code: Language.GERMAN, name: "German", countryCode: "DE", kaikkiName: "German" },
35+
{ code: Language.DUTCH, name: "Dutch", countryCode: "NL", kaikkiName: "Dutch" },
36+
{ code: Language.PORTUGUESE, name: "Portuguese", countryCode: "PT", kaikkiName: "Portuguese" },
37+
{ code: Language.RUSSIAN, name: "Russian", countryCode: "RU", kaikkiName: "Russian" },
38+
{ code: Language.HINDI, name: "Hindi", countryCode: "IN", kaikkiName: "Hindi" },
39+
{ code: Language.KOREAN, name: "Korean", countryCode: "KR", kaikkiName: "Korean" },
40+
{ code: Language.SWEDISH, name: "Swedish", countryCode: "SE", kaikkiName: "Swedish" },
41+
{ code: Language.TURKISH, name: "Turkish", countryCode: "TR", kaikkiName: "Turkish" },
42+
{ code: Language.POLISH, name: "Polish", countryCode: "PL", kaikkiName: "Polish" }
43+
];
44+
45+
// Get language info by code
46+
export function getLanguageInfo(code: Language): LanguageInfo {
47+
const info = LANGUAGE_INFO.find(l => l.code === code);
48+
if (!info) throw new Error(`No language info for code: ${code}`);
49+
return info;
50+
}
2051

52+
// Get display name
2153
export function describeLanguage(lang: Language): string {
22-
return LANGUAGE_INFO.find(info => info.code === lang)?.name ?? lang;
54+
return getLanguageInfo(lang).name;
55+
}
56+
57+
// normalize lang codes to basic ISO format (e.g. "en-US" -> "en")
58+
export function normalizeLanguage(lang: string): string {
59+
return lang.toLowerCase().split(/[-_]/)[0];
60+
}
61+
62+
// Get Kaikki dictionary name for a language
63+
export function toKaikkiLanguage(lang: Language): KaikkiLanguage {
64+
return getLanguageInfo(lang).kaikkiName as KaikkiLanguage;
2365
}
66+
67+
// Get country code for flags
68+
export function getCountryCode(lang: Language): string {
69+
return getLanguageInfo(lang).countryCode;
70+
}
71+
72+
// map of Kaikki language names to ISO codes
73+
export const KAIKKI_LANGUAGE_MAP = {
74+
English: "en",
75+
Spanish: "es",
76+
Italian: "it",
77+
German: "de",
78+
Russian: "ru",
79+
Portuguese: "pt",
80+
Polish: "pl",
81+
French: "fr",
82+
Catalan: "ca",
83+
Swedish: "sv",
84+
Latvian: "lv",
85+
Lithuanian: "lt",
86+
Dutch: "nl",
87+
Romanian: "ro",
88+
Greek: "el",
89+
Hungarian: "hu",
90+
Bulgarian: "bg",
91+
Czech: "cs",
92+
Ukrainian: "uk",
93+
Irish: "ga",
94+
Latin: "la",
95+
Arabic: "ar",
96+
Armenian: "hy",
97+
Azerbaijani: "az",
98+
Cebuano: "ceb",
99+
Chinese: "zh",
100+
Hebrew: "he",
101+
Hindi: "hi",
102+
Japanese: "ja",
103+
Korean: "ko",
104+
Marathi: "mr",
105+
Tagalog: "tl",
106+
Tamil: "ta",
107+
Telugu: "te",
108+
Turkish: "tr",
109+
Urdu: "ur",
110+
Vietnamese: "vi"
111+
} as const;
112+
113+
export type KaikkiLanguage = keyof typeof KAIKKI_LANGUAGE_MAP;
114+
115+
// normalize kaikki format to ISO (e.g. "English" -> "en")
116+
export function normalizeKaikkiLanguage(lang: KaikkiLanguage): string {
117+
return KAIKKI_LANGUAGE_MAP[lang];
118+
}
119+
120+
// all supported kaikki languages
121+
export const KAIKKI_LANGUAGES = Object.keys(KAIKKI_LANGUAGE_MAP) as KaikkiLanguage[];

0 commit comments

Comments
 (0)