feat(i18n): add Hindi (24 dictionaries, 25 codes, all 100%)

- Full 8,703-key Hindi dictionary in src/i18n/hi (7 namespace batches),
  placeholders verified, only 2% identical strings (brand/protocol names).
- Register hi in Language/LANGUAGE_LOADERS, detectLanguage (hi-IN),
  LanguageToggle (🇮🇳, हिन्दी) and dateFnsI18n (hi-IN).
- Lazy-load Noto Sans Devanagari for hi via lib/localeFonts.ts (renamed from
  cjkFonts.ts) and add it to the CSS font stack; other locales don't download it.
- check-i18n covers hi: 24 dictionaries / 25 codes at 100%; tsc/tests/build green.
This commit is contained in:
SkateSpots 2026-10-26 16:35:53 +07:00
parent 7e48884eb9
commit 828684cf15
20 changed files with 9776 additions and 32 deletions

View file

@ -2688,7 +2688,7 @@ similar are internal endpoints/hook names, not credits — leave them.)
is now **multi-way** — en/es stay key-for-key identical (unchanged), `th` (and is now **multi-way** — en/es stay key-for-key identical (unchanged), `th` (and
`ja`) may be partial and are reported as **coverage %**; a `th`/`ja` key absent `ja`) may be partial and are reported as **coverage %**; a `th`/`ja` key absent
from `en` fails. from `en` fails.
- **Font:** `@fontsource/kanit` (Kanit), **lazy-loaded per locale** (`lib/cjkFonts.ts`) and added to the - **Font:** `@fontsource/kanit` (Kanit), **lazy-loaded per locale** (`lib/localeFonts.ts`) and added to the
CSS + Tailwind font stacks — ⚠️ Outfit/Inter/Bebas/Barlow have **no Thai CSS + Tailwind font stacks — ⚠️ Outfit/Inter/Bebas/Barlow have **no Thai
glyphs**, so without it Thai silently falls back to a system font. glyphs**, so without it Thai silently falls back to a system font.
- **Detection:** a Thai-device browser (`th*`) starts in `th` (`detectLanguage`). - **Detection:** a Thai-device browser (`th*`) starts in `th` (`detectLanguage`).
@ -2697,18 +2697,19 @@ similar are internal endpoints/hook names, not credits — leave them.)
stays English (spot, park, bowl, DIY, vert, crew, line, Stash, The Lab, Nostr, stays English (spot, park, bowl, DIY, vert, crew, line, Stash, The Lab, Nostr,
Lightning, Zap, Relay, sats) because Thai skaters use the English terms. Lightning, Zap, Relay, sats) because Thai skaters use the English terms.
### Catalan, Basque, Galician + pt-PT — 23 dictionaries, 24 codes, all 100% (2026-10-26) ### Catalan, Basque, Galician, Hindi + pt-PT — 24 dictionaries, 25 codes, all 100% (2026-10-26)
- Added **ca (Català), eu (Euskara), gl (Galego)** → **23 dictionaries**; - Added **ca (Català), eu (Euskara), gl (Galego), hi (हिन्दी)** → **24
**pt-PT** is a variant that shares the `pt` dictionary (24 selectable codes). dictionaries**; **pt-PT** is a variant that shares the `pt` dictionary
- Registered in `Language` + `LANGUAGE_LOADERS`, `detectLanguage` (ca/eu/gl (25 selectable codes).
- Registered in `Language` + `LANGUAGE_LOADERS`, `detectLanguage` (ca/eu/gl/hi
checked before `es`; `pt-BR`→`pt`, `pt-PT`→`pt-PT`), the `LanguageToggle` checked before `es`; `pt-BR`→`pt`, `pt-PT`→`pt-PT`), the `LanguageToggle`
list, and `dateFnsI18n` (date-fns `ca`/`eu`/`gl`/`pt`, codes list, and `dateFnsI18n` (date-fns `ca`/`eu`/`gl`/`hi`/`pt`, codes
`ca-ES`/`eu-ES`/`gl-ES`/`pt-BR`/`pt-PT`). `ca-ES`/`eu-ES`/`gl-ES`/`hi-IN`/`pt-BR`/`pt-PT`).
- **Flags are real SVGs** in `public/flags/` for `ca`, `eu`, `gl` and `gb-eng` - **Flags are real SVGs** in `public/flags/` for `ca`, `eu`, `gl` and `gb-eng`
(there are no Unicode region flags for Catalonia/Basque/Galicia, and England (there are no Unicode region flags for Catalonia/Basque/Galicia, and England
is not the UK flag). `LanguageToggle` renders a path as `<img>` and an emoji is not the UK flag). `LanguageToggle` renders a path as `<img>` and an emoji
as text. **English now uses the England flag** (not the UK), and Portuguese as text. **English now uses the England flag** (not the UK), Portuguese has
has **Brasil 🇧🇷** (`pt`) + **Portugal 🇵🇹** (`pt-PT`). **Brasil 🇧🇷** (`pt`) + **Portugal 🇵🇹** (`pt-PT`), and Hindi uses 🇮🇳.
- **`pt-PT`** shares the `pt` base and applies a European drift - **`pt-PT`** shares the `pt` base and applies a European drift
(`src/i18n/pt-PT/drift.ts`: Salvar→Guardar, Senha→Palavra-passe, (`src/i18n/pt-PT/drift.ts`: Salvar→Guardar, Senha→Palavra-passe,
Ônibus→Autocarro, etc.). A full European-Portuguese review is a follow-up. Ônibus→Autocarro, etc.). A full European-Portuguese review is a follow-up.
@ -2718,15 +2719,18 @@ similar are internal endpoints/hook names, not credits — leave them.)
covering the surface (no grey bands), while still letting you drag across the covering the surface (no grey bands), while still letting you drag across the
antimeridian to Hawaii/Pacific. On a wide desktop the whole globe cannot also antimeridian to Hawaii/Pacific. On a wide desktop the whole globe cannot also
fit without distortion; the clamp prioritises coverage. fit without distortion; the clamp prioritises coverage.
- Full dictionaries in `src/i18n/{ca,eu,gl}/` — 7 namespace batches each - Full dictionaries in `src/i18n/{ca,eu,gl,hi}/` — 7 namespace batches each
(8,703 keys), composed in `index.ts`; placeholders verified, only 2–4% (8,703 keys), composed in `index.ts`; placeholders verified, only 2–4%
identical strings (brand/protocol names). `check-i18n.mjs` includes them in identical strings (brand/protocol names). `check-i18n.mjs` includes them in
`extraLocales` and reports 100%. `extraLocales` and reports 100%.
- Latin script, so **no new font**; each locale lazy-loads like the rest. - Catalan/Galician/Portuguese are Latin; **Hindi is Devanagari**, so
`@fontsource/noto-sans-devanagari` (400/500/700) is **lazy-loaded** for `hi`
via `lib/localeFonts.ts` (same pattern as CJK) and the CSS stack includes
`'Noto Sans Devanagari'` before `system-ui`.
- Translation sources were split by namespace into - Translation sources were split by namespace into
`/tmp/opencode/i18n-src/batch1..7.json` (regenerate from `en.ts` if `/tmp/opencode/i18n-src/batch1..7.json` (regenerate from `en.ts` if
re-translating). When adding new UI strings, add them to `en`/`es` **and all re-translating). When adding new UI strings, add them to `en`/`es` **and all
23 locales** (batch file or `drift.ts`). 24 dictionaries (25 codes)** (batch file or `drift.ts`).
### i18n drift closed — all 20 locales 100% (2026-10-13) ### i18n drift closed — all 20 locales 100% (2026-10-13)
- Every non-en/es locale had fallen to **97%** (245 keys each — `trustUi`, - Every non-en/es locale had fallen to **97%** (245 keys each — `trustUi`,
@ -2753,7 +2757,7 @@ similar are internal endpoints/hook names, not credits — leave them.)
bloats the app. Each language is its own on-demand chunk. bloats the app. Each language is its own on-demand chunk.
- **Fonts:** added **Noto Sans KR** (Korean) and **Inter** (Cyrillic/Russian) to - **Fonts:** added **Noto Sans KR** (Korean) and **Inter** (Cyrillic/Russian) to
the CSS + Tailwind stacks; JP/KR/TH each need their own font (Outfit/Inter the CSS + Tailwind stacks; JP/KR/TH each need their own font (Outfit/Inter
lack CJK). **(2026-10-12) CJK fonts are lazy-loaded per locale** (`lib/cjkFonts.ts`, lack CJK). **(2026-10-12) CJK fonts are lazy-loaded per locale** (`lib/localeFonts.ts`,
wired in `contexts/LanguageContext.tsx`) — they're ~16 MB of font payload that wired in `contexts/LanguageContext.tsx`) — they're ~16 MB of font payload that
non-CJK users no longer download at boot. non-CJK users no longer download at boot.
- `check-i18n.mjs` now reports **all 12**; new locales are *partial* (coverage %) - `check-i18n.mjs` now reports **all 12**; new locales are *partial* (coverage %)
@ -2775,7 +2779,7 @@ similar are internal endpoints/hook names, not credits — leave them.)
Both partial-locale rules below still hold for any *future* keys. Both partial-locale rules below still hold for any *future* keys.
- **Partial by design** (same as `th`): a missing `ja` key falls back to English. - **Partial by design** (same as `th`): a missing `ja` key falls back to English.
`check-i18n.mjs` reports `ja` coverage and fails if a `ja` key is absent from `en`. `check-i18n.mjs` reports `ja` coverage and fails if a `ja` key is absent from `en`.
- **Font:** `@fontsource/noto-sans-jp` (Noto Sans JP), **lazy-loaded per locale** (`lib/cjkFonts.ts`) and - **Font:** `@fontsource/noto-sans-jp` (Noto Sans JP), **lazy-loaded per locale** (`lib/localeFonts.ts`) and
added to the CSS + Tailwind font stacks — ⚠️ Outfit/Inter/Bebas/Barlow/Kanit have added to the CSS + Tailwind font stacks — ⚠️ Outfit/Inter/Bebas/Barlow/Kanit have
**no Japanese glyphs**, so without it Japanese falls back to a system font. **no Japanese glyphs**, so without it Japanese falls back to a system font.
- **Detection:** a Japanese-device browser (`ja*`) starts in `ja` (`detectLanguage`). - **Detection:** a Japanese-device browser (`ja*`) starts in `ja` (`detectLanguage`).

10
package-lock.json generated
View file

@ -24,6 +24,7 @@
"@fontsource/barlow-condensed": "^5.2.8", "@fontsource/barlow-condensed": "^5.2.8",
"@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-devanagari": "^5.3.0",
"@fontsource/noto-sans-jp": "^5.3.0", "@fontsource/noto-sans-jp": "^5.3.0",
"@fontsource/noto-sans-kr": "^5.3.0", "@fontsource/noto-sans-kr": "^5.3.0",
"@getalby/sdk": "^5.1.1", "@getalby/sdk": "^5.1.1",
@ -1531,6 +1532,15 @@
"url": "https://github.com/sponsors/ayuhito" "url": "https://github.com/sponsors/ayuhito"
} }
}, },
"node_modules/@fontsource/noto-sans-devanagari": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/noto-sans-devanagari/-/noto-sans-devanagari-5.3.0.tgz",
"integrity": "sha512-7khYmipS/5KAUUmrO1DKir8yL8XI00d0+ZE9BqbS8XF1jzObv6CpPOk3UeYxF+Pvi8bx9bBvprBCOcbx/eQWTQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/noto-sans-jp": { "node_modules/@fontsource/noto-sans-jp": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/noto-sans-jp/-/noto-sans-jp-5.3.0.tgz", "resolved": "https://registry.npmjs.org/@fontsource/noto-sans-jp/-/noto-sans-jp-5.3.0.tgz",

View file

@ -44,6 +44,7 @@
"@fontsource/barlow-condensed": "^5.2.8", "@fontsource/barlow-condensed": "^5.2.8",
"@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-devanagari": "^5.3.0",
"@fontsource/noto-sans-jp": "^5.3.0", "@fontsource/noto-sans-jp": "^5.3.0",
"@fontsource/noto-sans-kr": "^5.3.0", "@fontsource/noto-sans-kr": "^5.3.0",
"@getalby/sdk": "^5.1.1", "@getalby/sdk": "^5.1.1",

View file

@ -130,7 +130,7 @@ async function main() {
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). // 5. Additional partial locales (same rules as th/ja).
const extraLocales = ['pt', 'pt-PT', 'fr', 'de', 'it', 'nl', 'pl', 'ru', 'ko', 'zh-CN', 'zh-TW', 'uk', 'tr', 'vi', 'id', 'sv', 'cs', 'ro', 'el', 'ca', 'eu', 'gl']; const extraLocales = ['pt', 'pt-PT', 'fr', 'de', 'it', 'nl', 'pl', 'ru', 'ko', 'zh-CN', 'zh-TW', 'uk', 'tr', 'vi', 'id', 'sv', 'cs', 'ro', 'el', 'ca', 'eu', 'gl', 'hi'];
const extraStats = []; const extraStats = [];
for (const lang of extraLocales) { for (const lang of extraLocales) {
const dict = flatten(await loadTranslations(join(SRC, `i18n/${lang}/index.ts`))); const dict = flatten(await loadTranslations(join(SRC, `i18n/${lang}/index.ts`)));

View file

@ -45,6 +45,7 @@ const languages: LanguageOption[] = [
{ code: 'el', name: 'Greek', nativeName: 'Ελληνικά', flag: '🇬🇷' }, { code: 'el', name: 'Greek', nativeName: 'Ελληνικά', flag: '🇬🇷' },
{ code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', flag: '🇻🇳' }, { code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', flag: '🇻🇳' },
{ code: 'tr', name: 'Turkish', nativeName: 'Türkçe', flag: '🇹🇷' }, { code: 'tr', name: 'Turkish', nativeName: 'Türkçe', flag: '🇹🇷' },
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', flag: '🇮🇳' },
]; ];
interface LanguageToggleProps { interface LanguageToggleProps {

View file

@ -10,7 +10,7 @@ import {
type Translations, type Translations,
} from './language'; } from './language';
import { setDateLocale } from '@/lib/dateFnsI18n'; import { setDateLocale } from '@/lib/dateFnsI18n';
import { loadCjkFont } from '@/lib/cjkFonts'; import { loadLocaleFont } from '@/lib/localeFonts';
interface LanguageProviderProps { interface LanguageProviderProps {
children: ReactNode; children: ReactNode;
@ -52,6 +52,7 @@ function detectLanguage(): Language {
if (tags.some((t) => t.startsWith('ca'))) return 'ca'; if (tags.some((t) => t.startsWith('ca'))) return 'ca';
if (tags.some((t) => t.startsWith('eu'))) return 'eu'; if (tags.some((t) => t.startsWith('eu'))) return 'eu';
if (tags.some((t) => t.startsWith('gl'))) return 'gl'; if (tags.some((t) => t.startsWith('gl'))) return 'gl';
if (tags.some((t) => t.startsWith('hi'))) return 'hi';
if (tags.some((t) => t.startsWith('es'))) return 'es'; if (tags.some((t) => t.startsWith('es'))) return 'es';
return 'en'; return 'en';
} }
@ -85,7 +86,7 @@ export function LanguageProvider({ children }: LanguageProviderProps) {
// Lazy-load the CJK display font for the active locale (ja/ko/th) — large font // Lazy-load the CJK display font for the active locale (ja/ko/th) — large font
// files that non-CJK users no longer have to download at boot. // files that non-CJK users no longer have to download at boot.
useEffect(() => { useEffect(() => {
loadCjkFont(language); loadLocaleFont(language);
}, [language]); }, [language]);
const setLanguage = useCallback((lang: Language) => { const setLanguage = useCallback((lang: Language) => {

View file

@ -7,7 +7,7 @@ import en from '@/i18n/en';
* `react-refresh/only-export-components` rule. * `react-refresh/only-export-components` rule.
*/ */
export type Language = 'en' | 'es' | 'ca' | 'eu' | 'gl' | 'th' | 'ja' | 'pt' | 'pt-PT' | 'fr' | 'de' | 'it' | 'nl' | 'pl' | 'ru' | 'ko' | 'ko-KP' | 'zh-CN' | 'zh-TW' | 'uk' | 'tr' | 'vi' | 'id' | 'sv' | 'cs' | 'ro' | 'el'; export type Language = 'en' | 'es' | 'ca' | 'eu' | 'gl' | 'hi' | 'th' | 'ja' | 'pt' | 'pt-PT' | 'fr' | 'de' | 'it' | 'nl' | 'pl' | 'ru' | 'ko' | 'ko-KP' | 'zh-CN' | 'zh-TW' | 'uk' | 'tr' | 'vi' | 'id' | 'sv' | 'cs' | 'ro' | 'el';
type TranslationKey = string; type TranslationKey = string;
type TranslationValue = string | { [key: string]: TranslationValue }; type TranslationValue = string | { [key: string]: TranslationValue };
@ -42,6 +42,7 @@ export const LANGUAGE_LOADERS: Record<Language, () => Promise<Translations>> = {
ca: () => import('@/i18n/ca').then((m) => m.default), ca: () => import('@/i18n/ca').then((m) => m.default),
eu: () => import('@/i18n/eu').then((m) => m.default), eu: () => import('@/i18n/eu').then((m) => m.default),
gl: () => import('@/i18n/gl').then((m) => m.default), gl: () => import('@/i18n/gl').then((m) => m.default),
hi: () => import('@/i18n/hi').then((m) => m.default),
th: () => import('@/i18n/th').then((m) => m.default), th: () => import('@/i18n/th').then((m) => m.default),
ja: () => import('@/i18n/ja').then((m) => m.default), ja: () => import('@/i18n/ja').then((m) => m.default),
pt: () => import('@/i18n/pt').then((m) => m.default), pt: () => import('@/i18n/pt').then((m) => m.default),

1268
src/i18n/hi/batch1.ts Normal file

File diff suppressed because it is too large Load diff

1491
src/i18n/hi/batch2.ts Normal file

File diff suppressed because it is too large Load diff

1300
src/i18n/hi/batch3.ts Normal file

File diff suppressed because it is too large Load diff

1427
src/i18n/hi/batch4.ts Normal file

File diff suppressed because it is too large Load diff

1463
src/i18n/hi/batch5.ts Normal file

File diff suppressed because it is too large Load diff

1137
src/i18n/hi/batch6.ts Normal file

File diff suppressed because it is too large Load diff

1599
src/i18n/hi/batch7.ts Normal file

File diff suppressed because it is too large Load diff

4
src/i18n/hi/drift.ts Normal file
View file

@ -0,0 +1,4 @@
/** Post-translation corrections for hi. */
const drift = {};
export default drift;

24
src/i18n/hi/index.ts Normal file
View file

@ -0,0 +1,24 @@
import batch1 from './batch1';
import batch2 from './batch2';
import batch3 from './batch3';
import batch4 from './batch4';
import batch5 from './batch5';
import batch6 from './batch6';
import batch7 from './batch7';
import drift from './drift';
import { deepMerge } from '../deepMerge';
const hi = deepMerge(
{
...batch1,
...batch2,
...batch3,
...batch4,
...batch5,
...batch6,
...batch7,
},
drift,
);
export default hi;

View file

@ -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', 'Noto Sans KR', 'Impact', 'Arial Black', sans-serif; font-family: 'Bebas Neue', 'Kanit', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans Devanagari', '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', 'Noto Sans KR', 'Arial Narrow', sans-serif; font-family: 'Barlow Condensed', 'Kanit', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans Devanagari', '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', 'Noto Sans KR', 'Impact', 'Arial Black', sans-serif; font-family: 'Bebas Neue', 'Kanit', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans Devanagari', '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', 'Noto Sans KR', system-ui, sans-serif; font-family: 'Inter Variable', 'Inter', 'Kanit', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans Devanagari', 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);
} }

View file

@ -25,6 +25,7 @@ import {
ca as caLocale, ca as caLocale,
eu as euLocale, eu as euLocale,
gl as glLocale, gl as glLocale,
hi as hiLocale,
th as thLocale, th as thLocale,
ja as jaLocale, ja as jaLocale,
ptBR as ptLocale, ptBR as ptLocale,
@ -67,6 +68,9 @@ export function setDateLocale(language: Language): void {
} else if (language === 'gl') { } else if (language === 'gl') {
activeLocale = glLocale; activeLocale = glLocale;
activeCode = 'gl-ES'; activeCode = 'gl-ES';
} else if (language === 'hi') {
activeLocale = hiLocale;
activeCode = 'hi-IN';
} else if (language === 'th') { } else if (language === 'th') {
activeLocale = thLocale; activeLocale = thLocale;
activeCode = 'th-TH'; activeCode = 'th-TH';

View file

@ -3,15 +3,17 @@ import type { Language } from '@/contexts/language';
const loaded = new Set<Language>(); const loaded = new Set<Language>();
/** /**
* Lazy-load the CJK display font for the active locale. * Lazy-load the script-specific display font for the active locale (CJK +
* Devanagari).
* *
* The Thai (Kanit), Japanese (Noto Sans JP) and Korean (Noto Sans KR) font * The Thai (Kanit), Japanese (Noto Sans JP), Korean (Noto Sans KR) and Hindi
* families are large and only needed for users on those locales — but they were * (Noto Sans Devanagari) font families are large and only needed for users on
* imported statically in main.tsx, so every visitor downloaded them. They are * those locales — but they were imported statically in main.tsx, so every
* now loaded on demand, once per locale, and non-CJK users never fetch them. * visitor downloaded them. They are now loaded on demand, once per locale, and
* Text briefly falls back to a system font until the stylesheet resolves. * other users never fetch them. Text briefly falls back to a system font until
* the stylesheet resolves.
*/ */
export function loadCjkFont(language: Language): void { export function loadLocaleFont(language: Language): void {
if (loaded.has(language)) return; if (loaded.has(language)) return;
loaded.add(language); loaded.add(language);
@ -34,5 +36,11 @@ export function loadCjkFont(language: Language): void {
import('@fontsource/kanit/600.css'), import('@fontsource/kanit/600.css'),
import('@fontsource/kanit/700.css'), import('@fontsource/kanit/700.css'),
]).catch(() => {}); ]).catch(() => {});
} else if (language === 'hi') {
void Promise.all([
import('@fontsource/noto-sans-devanagari/400.css'),
import('@fontsource/noto-sans-devanagari/500.css'),
import('@fontsource/noto-sans-devanagari/700.css'),
]).catch(() => {});
} }
} }

View file

@ -34,9 +34,10 @@ import '@fontsource/bebas-neue';
import '@fontsource/barlow-condensed/400.css'; import '@fontsource/barlow-condensed/400.css';
import '@fontsource/barlow-condensed/600.css'; import '@fontsource/barlow-condensed/600.css';
import '@fontsource/barlow-condensed/700.css'; import '@fontsource/barlow-condensed/700.css';
// CJK fonts (Thai Kanit, Noto Sans JP, Noto Sans KR) are NOT imported here — // Script fonts (Thai Kanit, Noto Sans JP/KR, Noto Sans Devanagari) are NOT
// they're large and only needed for ja/ko/th users, so they're lazy-loaded per // imported here — they're large and only needed for ja/ko/th/hi users, so
// locale (lib/cjkFonts.ts, wired in contexts/LanguageContext.tsx). // they're lazy-loaded per locale (lib/localeFonts.ts, wired in
// contexts/LanguageContext.tsx).
// Cyrillic (Russian) — Inter has Cyrillic; Outfit does not // Cyrillic (Russian) — Inter has Cyrillic; Outfit does not
import '@fontsource-variable/inter'; import '@fontsource-variable/inter';