i18n: lazy-load locales + register 8 new (pt fr de it nl pl ru ko) with fonts
This commit is contained in:
parent
7ab9a5b399
commit
1460b06277
18 changed files with 201 additions and 24 deletions
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -22,6 +22,7 @@
|
||||||
"@fontsource/bebas-neue": "^5.2.7",
|
"@fontsource/bebas-neue": "^5.2.7",
|
||||||
"@fontsource/kanit": "^5.3.0",
|
"@fontsource/kanit": "^5.3.0",
|
||||||
"@fontsource/noto-sans-jp": "^5.3.0",
|
"@fontsource/noto-sans-jp": "^5.3.0",
|
||||||
|
"@fontsource/noto-sans-kr": "^5.3.0",
|
||||||
"@getalby/sdk": "^5.1.1",
|
"@getalby/sdk": "^5.1.1",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
|
@ -1521,6 +1522,15 @@
|
||||||
"url": "https://github.com/sponsors/ayuhito"
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource/noto-sans-kr": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/noto-sans-kr/-/noto-sans-kr-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-/JnpTjaCOXW7xUoqOyCVYSr05VOkDy5Yla9O+WAEiS7u+yYLsmHoqj6v1W0bf91G8G3XA2+NnRS3CX2Tf/FVZg==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@getalby/lightning-tools": {
|
"node_modules/@getalby/lightning-tools": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@getalby/lightning-tools/-/lightning-tools-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@getalby/lightning-tools/-/lightning-tools-5.2.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"@fontsource/bebas-neue": "^5.2.7",
|
"@fontsource/bebas-neue": "^5.2.7",
|
||||||
"@fontsource/kanit": "^5.3.0",
|
"@fontsource/kanit": "^5.3.0",
|
||||||
"@fontsource/noto-sans-jp": "^5.3.0",
|
"@fontsource/noto-sans-jp": "^5.3.0",
|
||||||
|
"@fontsource/noto-sans-kr": "^5.3.0",
|
||||||
"@getalby/sdk": "^5.1.1",
|
"@getalby/sdk": "^5.1.1",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,17 @@ async function main() {
|
||||||
for (const key of ja) if (!en.has(key)) problems.push(`ja has a key not in en: ${key}`);
|
for (const key of ja) if (!en.has(key)) problems.push(`ja has a key not in en: ${key}`);
|
||||||
const jaCoverage = en.size ? Math.round(((en.size - missingJa) / en.size) * 100) : 100;
|
const jaCoverage = en.size ? Math.round(((en.size - missingJa) / en.size) * 100) : 100;
|
||||||
|
|
||||||
|
// 5. Additional partial locales (same rules as th/ja).
|
||||||
|
const extraLocales = ['pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru', 'ko'];
|
||||||
|
const extraStats = [];
|
||||||
|
for (const lang of extraLocales) {
|
||||||
|
const dict = flatten(await loadTranslations(join(SRC, `i18n/${lang}/index.ts`)));
|
||||||
|
for (const key of dict) if (!en.has(key)) problems.push(`${lang} has a key not in en: ${key}`);
|
||||||
|
const missing = [...en].filter((k) => !dict.has(k)).length;
|
||||||
|
const cov = en.size ? Math.round(((en.size - missing) / en.size) * 100) : 100;
|
||||||
|
extraStats.push(`${lang} ${cov}%`);
|
||||||
|
}
|
||||||
|
|
||||||
if (problems.length) {
|
if (problems.length) {
|
||||||
console.error(`\n✖ i18n check failed (${problems.length}):`);
|
console.error(`\n✖ i18n check failed (${problems.length}):`);
|
||||||
for (const p of problems.slice(0, 200)) console.error(` • ${p}`);
|
for (const p of problems.slice(0, 200)) console.error(` • ${p}`);
|
||||||
|
|
@ -136,7 +147,7 @@ async function main() {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✓ i18n check passed — ${en.size} keys in en/es, ${used.size} used · th ${thCoverage}% (${en.size - missingTh}/${en.size}) · ja ${jaCoverage}% (${en.size - missingJa}/${en.size})`);
|
console.log(`✓ i18n check passed — ${en.size} keys in en/es, ${used.size} used · th ${thCoverage}% · ja ${jaCoverage}% · ${extraStats.join(' · ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
main().catch((err) => {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,14 @@ interface LanguageOption {
|
||||||
const languages: LanguageOption[] = [
|
const languages: LanguageOption[] = [
|
||||||
{ code: 'en', name: 'English', nativeName: 'English', flag: '🇬🇧' },
|
{ code: 'en', name: 'English', nativeName: 'English', flag: '🇬🇧' },
|
||||||
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
|
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
|
||||||
|
{ code: 'pt', name: 'Portuguese', nativeName: 'Português', flag: '🇧🇷' },
|
||||||
|
{ code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
|
||||||
|
{ code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪' },
|
||||||
|
{ code: 'it', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹' },
|
||||||
|
{ code: 'nl', name: 'Dutch', nativeName: 'Nederlands', flag: '🇳🇱' },
|
||||||
|
{ code: 'pl', name: 'Polish', nativeName: 'Polski', flag: '🇵🇱' },
|
||||||
|
{ code: 'ru', name: 'Russian', nativeName: 'Русский', flag: '🇷🇺' },
|
||||||
|
{ code: 'ko', name: 'Korean', nativeName: '한국어', flag: '🇰🇷' },
|
||||||
{ code: 'th', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭' },
|
{ code: 'th', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭' },
|
||||||
{ code: 'ja', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵' },
|
{ code: 'ja', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵' },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ import {
|
||||||
LanguageContext,
|
LanguageContext,
|
||||||
getNestedValue,
|
getNestedValue,
|
||||||
interpolate,
|
interpolate,
|
||||||
translations,
|
englishTranslations,
|
||||||
|
LANGUAGE_LOADERS,
|
||||||
type Language,
|
type Language,
|
||||||
|
type Translations,
|
||||||
} from './language';
|
} from './language';
|
||||||
import { setDateLocale } from '@/lib/dateFnsI18n';
|
import { setDateLocale } from '@/lib/dateFnsI18n';
|
||||||
|
|
||||||
|
|
@ -24,6 +26,14 @@ function detectLanguage(): Language {
|
||||||
const tags = [navigator.language, ...(navigator.languages ?? [])].map((t) => t?.toLowerCase() ?? '');
|
const tags = [navigator.language, ...(navigator.languages ?? [])].map((t) => t?.toLowerCase() ?? '');
|
||||||
if (tags.some((t) => t.startsWith('th'))) return 'th';
|
if (tags.some((t) => t.startsWith('th'))) return 'th';
|
||||||
if (tags.some((t) => t.startsWith('ja'))) return 'ja';
|
if (tags.some((t) => t.startsWith('ja'))) return 'ja';
|
||||||
|
if (tags.some((t) => t.startsWith('ko'))) return 'ko';
|
||||||
|
if (tags.some((t) => t.startsWith('pt'))) return 'pt';
|
||||||
|
if (tags.some((t) => t.startsWith('fr'))) return 'fr';
|
||||||
|
if (tags.some((t) => t.startsWith('de'))) return 'de';
|
||||||
|
if (tags.some((t) => t.startsWith('it'))) return 'it';
|
||||||
|
if (tags.some((t) => t.startsWith('nl'))) return 'nl';
|
||||||
|
if (tags.some((t) => t.startsWith('pl'))) return 'pl';
|
||||||
|
if (tags.some((t) => t.startsWith('ru'))) return 'ru';
|
||||||
if (tags.some((t) => t.startsWith('es'))) return 'es';
|
if (tags.some((t) => t.startsWith('es'))) return 'es';
|
||||||
return 'en';
|
return 'en';
|
||||||
}
|
}
|
||||||
|
|
@ -35,12 +45,25 @@ function detectLanguage(): Language {
|
||||||
export function LanguageProvider({ children }: LanguageProviderProps) {
|
export function LanguageProvider({ children }: LanguageProviderProps) {
|
||||||
const [storedLanguage, setStoredLanguage] = useLocalStorage<Language>('app-language', detectLanguage());
|
const [storedLanguage, setStoredLanguage] = useLocalStorage<Language>('app-language', detectLanguage());
|
||||||
const [language, setLanguageState] = useState<Language>(storedLanguage);
|
const [language, setLanguageState] = useState<Language>(storedLanguage);
|
||||||
|
// The loaded dictionary for `language`. Starts as English (the static source)
|
||||||
|
// and is replaced once the lazy locale chunk resolves, so the first paint is
|
||||||
|
// never blocked and untranslated keys fall back to English.
|
||||||
|
const [dict, setDict] = useState<Translations>(englishTranslations);
|
||||||
|
|
||||||
// Sync with localStorage
|
// Sync with localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLanguageState(storedLanguage);
|
setLanguageState(storedLanguage);
|
||||||
}, [storedLanguage]);
|
}, [storedLanguage]);
|
||||||
|
|
||||||
|
// Lazy-load the locale dictionary whenever the language changes.
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
LANGUAGE_LOADERS[language]()
|
||||||
|
.then((d) => { if (active) setDict(d); })
|
||||||
|
.catch(() => { if (active) setDict(englishTranslations); });
|
||||||
|
return () => { active = false; };
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
const setLanguage = useCallback((lang: Language) => {
|
const setLanguage = useCallback((lang: Language) => {
|
||||||
setLanguageState(lang);
|
setLanguageState(lang);
|
||||||
setStoredLanguage(lang);
|
setStoredLanguage(lang);
|
||||||
|
|
@ -54,25 +77,17 @@ export function LanguageProvider({ children }: LanguageProviderProps) {
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
const t = useCallback((key: string, params?: Record<string, string | number>): string => {
|
const t = useCallback((key: string, params?: Record<string, string | number>): string => {
|
||||||
// Try to get translation in current language
|
// Current locale first, then the English source as fallback.
|
||||||
const translation = getNestedValue(translations[language], key);
|
const translation = getNestedValue(dict, key) ?? getNestedValue(englishTranslations, key);
|
||||||
|
|
||||||
if (translation) {
|
if (translation) {
|
||||||
return interpolate(translation, params);
|
return interpolate(translation, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to English
|
|
||||||
if (language !== 'en') {
|
|
||||||
const fallback = getNestedValue(translations.en, key);
|
|
||||||
if (fallback) {
|
|
||||||
return interpolate(fallback, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return key if no translation found (helps identify missing translations)
|
// Return key if no translation found (helps identify missing translations)
|
||||||
console.warn(`Missing translation for key: ${key}`);
|
console.warn(`Missing translation for key: ${key}`);
|
||||||
return key;
|
return key;
|
||||||
}, [language]);
|
}, [dict]);
|
||||||
|
|
||||||
// Currently no RTL languages supported, but ready for future
|
// Currently no RTL languages supported, but ready for future
|
||||||
const isRTL = false;
|
const isRTL = false;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
import en from '@/i18n/en';
|
import en from '@/i18n/en';
|
||||||
import es from '@/i18n/es';
|
|
||||||
import th from '@/i18n/th';
|
|
||||||
import ja from '@/i18n/ja';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Language context + types + hooks. Split from the `LanguageProvider` component
|
* Language context + types + hooks. Split from the `LanguageProvider` component
|
||||||
|
|
@ -10,7 +7,7 @@ import ja from '@/i18n/ja';
|
||||||
* `react-refresh/only-export-components` rule.
|
* `react-refresh/only-export-components` rule.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type Language = 'en' | 'es' | 'th' | 'ja';
|
export type Language = 'en' | 'es' | 'th' | 'ja' | 'pt' | 'fr' | 'de' | 'it' | 'nl' | 'pl' | 'ru' | 'ko';
|
||||||
|
|
||||||
type TranslationKey = string;
|
type TranslationKey = string;
|
||||||
type TranslationValue = string | { [key: string]: TranslationValue };
|
type TranslationValue = string | { [key: string]: TranslationValue };
|
||||||
|
|
@ -32,7 +29,27 @@ export interface LanguageContextType {
|
||||||
|
|
||||||
export const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
export const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const translations: Record<Language, Translations> = { en, es, th, ja };
|
export const englishTranslations = en;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locale dictionaries are loaded on demand — bundling every language eagerly
|
||||||
|
* bloats the app (each locale is hundreds of KB). English is the static source +
|
||||||
|
* runtime fallback; everything else is a dynamic import.
|
||||||
|
*/
|
||||||
|
export const LANGUAGE_LOADERS: Record<Language, () => Promise<Translations>> = {
|
||||||
|
en: async () => en,
|
||||||
|
es: () => import('@/i18n/es').then((m) => m.default),
|
||||||
|
th: () => import('@/i18n/th').then((m) => m.default),
|
||||||
|
ja: () => import('@/i18n/ja').then((m) => m.default),
|
||||||
|
pt: () => import('@/i18n/pt').then((m) => m.default),
|
||||||
|
fr: () => import('@/i18n/fr').then((m) => m.default),
|
||||||
|
de: () => import('@/i18n/de').then((m) => m.default),
|
||||||
|
it: () => import('@/i18n/it').then((m) => m.default),
|
||||||
|
nl: () => import('@/i18n/nl').then((m) => m.default),
|
||||||
|
pl: () => import('@/i18n/pl').then((m) => m.default),
|
||||||
|
ru: () => import('@/i18n/ru').then((m) => m.default),
|
||||||
|
ko: () => import('@/i18n/ko').then((m) => m.default),
|
||||||
|
};
|
||||||
|
|
||||||
/** Get a nested value from an object using dot notation (`nav.home`). */
|
/** Get a nested value from an object using dot notation (`nav.home`). */
|
||||||
export function getNestedValue(obj: Translations, path: string): string | undefined {
|
export function getNestedValue(obj: Translations, path: string): string | undefined {
|
||||||
|
|
|
||||||
9
src/i18n/de/index.ts
Normal file
9
src/i18n/de/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* de translations — fragments composed here.
|
||||||
|
*
|
||||||
|
* Partial by design: missing keys fall back to English; `check-i18n.mjs`
|
||||||
|
* reports coverage. Add fragments as the namespaces land.
|
||||||
|
*/
|
||||||
|
const de = {};
|
||||||
|
|
||||||
|
export default de;
|
||||||
9
src/i18n/fr/index.ts
Normal file
9
src/i18n/fr/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* fr translations — fragments composed here.
|
||||||
|
*
|
||||||
|
* Partial by design: missing keys fall back to English; `check-i18n.mjs`
|
||||||
|
* reports coverage. Add fragments as the namespaces land.
|
||||||
|
*/
|
||||||
|
const fr = {};
|
||||||
|
|
||||||
|
export default fr;
|
||||||
9
src/i18n/it/index.ts
Normal file
9
src/i18n/it/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* it translations — fragments composed here.
|
||||||
|
*
|
||||||
|
* Partial by design: missing keys fall back to English; `check-i18n.mjs`
|
||||||
|
* reports coverage. Add fragments as the namespaces land.
|
||||||
|
*/
|
||||||
|
const it = {};
|
||||||
|
|
||||||
|
export default it;
|
||||||
9
src/i18n/ko/index.ts
Normal file
9
src/i18n/ko/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* ko translations — fragments composed here.
|
||||||
|
*
|
||||||
|
* Partial by design: missing keys fall back to English; `check-i18n.mjs`
|
||||||
|
* reports coverage. Add fragments as the namespaces land.
|
||||||
|
*/
|
||||||
|
const ko = {};
|
||||||
|
|
||||||
|
export default ko;
|
||||||
9
src/i18n/nl/index.ts
Normal file
9
src/i18n/nl/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* nl translations — fragments composed here.
|
||||||
|
*
|
||||||
|
* Partial by design: missing keys fall back to English; `check-i18n.mjs`
|
||||||
|
* reports coverage. Add fragments as the namespaces land.
|
||||||
|
*/
|
||||||
|
const nl = {};
|
||||||
|
|
||||||
|
export default nl;
|
||||||
9
src/i18n/pl/index.ts
Normal file
9
src/i18n/pl/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* pl translations — fragments composed here.
|
||||||
|
*
|
||||||
|
* Partial by design: missing keys fall back to English; `check-i18n.mjs`
|
||||||
|
* reports coverage. Add fragments as the namespaces land.
|
||||||
|
*/
|
||||||
|
const pl = {};
|
||||||
|
|
||||||
|
export default pl;
|
||||||
9
src/i18n/pt/index.ts
Normal file
9
src/i18n/pt/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* pt translations — fragments composed here.
|
||||||
|
*
|
||||||
|
* Partial by design: missing keys fall back to English; `check-i18n.mjs`
|
||||||
|
* reports coverage. Add fragments as the namespaces land.
|
||||||
|
*/
|
||||||
|
const pt = {};
|
||||||
|
|
||||||
|
export default pt;
|
||||||
9
src/i18n/ru/index.ts
Normal file
9
src/i18n/ru/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* ru translations — fragments composed here.
|
||||||
|
*
|
||||||
|
* Partial by design: missing keys fall back to English; `check-i18n.mjs`
|
||||||
|
* reports coverage. Add fragments as the namespaces land.
|
||||||
|
*/
|
||||||
|
const ru = {};
|
||||||
|
|
||||||
|
export default ru;
|
||||||
|
|
@ -149,18 +149,18 @@
|
||||||
/* ── Display font utilities ───────────────────────────────────────────────── */
|
/* ── Display font utilities ───────────────────────────────────────────────── */
|
||||||
/* Bebas Neue — used for all major headlines */
|
/* Bebas Neue — used for all major headlines */
|
||||||
.font-display {
|
.font-display {
|
||||||
font-family: 'Bebas Neue', 'Kanit', 'Noto Sans JP', 'Impact', 'Arial Black', sans-serif;
|
font-family: 'Bebas Neue', 'Kanit', 'Noto Sans JP', 'Noto Sans KR', 'Impact', 'Arial Black', sans-serif;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Barlow Condensed — used for subheadings, labels, nav */
|
/* Barlow Condensed — used for subheadings, labels, nav */
|
||||||
.font-condensed {
|
.font-condensed {
|
||||||
font-family: 'Barlow Condensed', 'Kanit', 'Noto Sans JP', 'Arial Narrow', sans-serif;
|
font-family: 'Barlow Condensed', 'Kanit', 'Noto Sans JP', 'Noto Sans KR', 'Arial Narrow', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.neon-header {
|
.neon-header {
|
||||||
animation: neon-glow 2s ease-in-out infinite;
|
animation: neon-glow 2s ease-in-out infinite;
|
||||||
font-family: 'Bebas Neue', 'Kanit', 'Noto Sans JP', 'Impact', 'Arial Black', sans-serif;
|
font-family: 'Bebas Neue', 'Kanit', 'Noto Sans JP', 'Noto Sans KR', 'Impact', 'Arial Black', sans-serif;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -714,7 +714,7 @@ select {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-family: 'Inter Variable', 'Inter', 'Kanit', 'Noto Sans JP', system-ui, sans-serif;
|
font-family: 'Inter Variable', 'Inter', 'Kanit', 'Noto Sans JP', 'Noto Sans KR', system-ui, sans-serif;
|
||||||
box-shadow: 0 8px 40px rgba(0,0,0,0.6), 0 2px 8px rgba(0,0,0,0.4);
|
box-shadow: 0 8px 40px rgba(0,0,0,0.6), 0 2px 8px rgba(0,0,0,0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,20 @@ import {
|
||||||
formatDistance as _formatDistance,
|
formatDistance as _formatDistance,
|
||||||
format as _format,
|
format as _format,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { enUS, es as esLocale, th as thLocale, ja as jaLocale } from 'date-fns/locale';
|
import {
|
||||||
|
enUS,
|
||||||
|
es as esLocale,
|
||||||
|
th as thLocale,
|
||||||
|
ja as jaLocale,
|
||||||
|
ptBR as ptLocale,
|
||||||
|
fr as frLocale,
|
||||||
|
de as deLocale,
|
||||||
|
it as itLocale,
|
||||||
|
nl as nlLocale,
|
||||||
|
pl as plLocale,
|
||||||
|
ru as ruLocale,
|
||||||
|
ko as koLocale,
|
||||||
|
} from 'date-fns/locale';
|
||||||
import type { Locale } from 'date-fns';
|
import type { Locale } from 'date-fns';
|
||||||
import type { Language } from '@/contexts/language';
|
import type { Language } from '@/contexts/language';
|
||||||
|
|
||||||
|
|
@ -37,6 +50,30 @@ export function setDateLocale(language: Language): void {
|
||||||
} else if (language === 'ja') {
|
} else if (language === 'ja') {
|
||||||
activeLocale = jaLocale;
|
activeLocale = jaLocale;
|
||||||
activeCode = 'ja-JP';
|
activeCode = 'ja-JP';
|
||||||
|
} else if (language === 'pt') {
|
||||||
|
activeLocale = ptLocale;
|
||||||
|
activeCode = 'pt-BR';
|
||||||
|
} else if (language === 'fr') {
|
||||||
|
activeLocale = frLocale;
|
||||||
|
activeCode = 'fr-FR';
|
||||||
|
} else if (language === 'de') {
|
||||||
|
activeLocale = deLocale;
|
||||||
|
activeCode = 'de-DE';
|
||||||
|
} else if (language === 'it') {
|
||||||
|
activeLocale = itLocale;
|
||||||
|
activeCode = 'it-IT';
|
||||||
|
} else if (language === 'nl') {
|
||||||
|
activeLocale = nlLocale;
|
||||||
|
activeCode = 'nl-NL';
|
||||||
|
} else if (language === 'pl') {
|
||||||
|
activeLocale = plLocale;
|
||||||
|
activeCode = 'pl-PL';
|
||||||
|
} else if (language === 'ru') {
|
||||||
|
activeLocale = ruLocale;
|
||||||
|
activeCode = 'ru-RU';
|
||||||
|
} else if (language === 'ko') {
|
||||||
|
activeLocale = koLocale;
|
||||||
|
activeCode = 'ko-KR';
|
||||||
} else {
|
} else {
|
||||||
activeLocale = enUS;
|
activeLocale = enUS;
|
||||||
activeCode = 'en-US';
|
activeCode = 'en-US';
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,12 @@ import '@fontsource/kanit/700.css';
|
||||||
import '@fontsource/noto-sans-jp/400.css';
|
import '@fontsource/noto-sans-jp/400.css';
|
||||||
import '@fontsource/noto-sans-jp/500.css';
|
import '@fontsource/noto-sans-jp/500.css';
|
||||||
import '@fontsource/noto-sans-jp/700.css';
|
import '@fontsource/noto-sans-jp/700.css';
|
||||||
|
// Korean glyphs — Noto Sans KR
|
||||||
|
import '@fontsource/noto-sans-kr/400.css';
|
||||||
|
import '@fontsource/noto-sans-kr/500.css';
|
||||||
|
import '@fontsource/noto-sans-kr/700.css';
|
||||||
|
// Cyrillic (Russian) — Inter has Cyrillic; Outfit does not
|
||||||
|
import '@fontsource-variable/inter';
|
||||||
|
|
||||||
// Leaflet CSS is loaded via CDN in index.html
|
// Leaflet CSS is loaded via CDN in index.html
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export default {
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Outfit Variable', 'Outfit', 'Kanit', 'Noto Sans JP', 'system-ui', 'sans-serif'],
|
sans: ['Outfit Variable', 'Outfit', 'Inter Variable', 'Kanit', 'Noto Sans JP', 'Noto Sans KR', 'system-ui', 'sans-serif'],
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
border: 'hsl(var(--border))',
|
border: 'hsl(var(--border))',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue