feat(i18n): add Catalan, Basque and Galician (23 locales, all 100%)
- Full dictionaries (8,703 keys each) in src/i18n/{ca,eu,gl}, 7 namespace
batches, composed with deepMerge; placeholders verified, 2-4% identical
strings are only brand/protocol names.
- Register in Language/LANGUAGE_LOADERS, detectLanguage (before es),
LanguageToggle (CA/EU/GL text badges - no Unicode region flags exist) and
dateFnsI18n (ca-ES/eu-ES/gl-ES).
- check-i18n.mjs now covers ca/eu/gl: all 23 locales at 100%, tsc/tests/build
green.
This commit is contained in:
parent
67f21bf1ac
commit
d4c6261b81
33 changed files with 29196 additions and 2 deletions
18
AGENTS.md
18
AGENTS.md
|
|
@ -2697,6 +2697,24 @@ similar are internal endpoints/hook names, not credits — leave them.)
|
|||
stays English (spot, park, bowl, DIY, vert, crew, line, Stash, The Lab, Nostr,
|
||||
Lightning, Zap, Relay, sats) because Thai skaters use the English terms.
|
||||
|
||||
### Catalan, Basque, Galician — 23 locales, all 100% (2026-10-26)
|
||||
- Added **ca (Català), eu (Euskara), gl (Galego)** → **23 locales**.
|
||||
- Registered in `Language` + `LANGUAGE_LOADERS`, `detectLanguage` (ca/eu/gl
|
||||
checked before `es`), the `LanguageToggle` list, and `dateFnsI18n`
|
||||
(date-fns `ca`/`eu`/`gl`, codes `ca-ES`/`eu-ES`/`gl-ES`).
|
||||
- `LanguageToggle` uses **text badges `CA` / `EU` / `GL`**: there are no Unicode
|
||||
region flags for these languages, and a country flag would be wrong. The
|
||||
`CreateTourPage` language list already had ca/eu/gl for tour tags.
|
||||
- Full dictionaries in `src/i18n/{ca,eu,gl}/` — 7 namespace batches each
|
||||
(8,703 keys), composed in `index.ts`; placeholders verified, only 2–4%
|
||||
identical strings (brand/protocol names). `check-i18n.mjs` includes them in
|
||||
`extraLocales` and reports 100%.
|
||||
- Latin script, so **no new font**; each locale lazy-loads like the rest.
|
||||
- Translation sources were split by namespace into
|
||||
`/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
|
||||
23 locales** (batch file or `drift.ts`).
|
||||
|
||||
### i18n drift closed — all 20 locales 100% (2026-10-13)
|
||||
- Every non-en/es locale had fallen to **97%** (245 keys each — `trustUi`,
|
||||
`labOptions`, `cordnUi`, `socialUi`, `walletUi`, `marketTerms`, …). Backfilled
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ async function main() {
|
|||
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', 'zh-CN', 'zh-TW', 'uk', 'tr', 'vi', 'id', 'sv', 'cs', 'ro', 'el'];
|
||||
const extraLocales = ['pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru', 'ko', 'zh-CN', 'zh-TW', 'uk', 'tr', 'vi', 'id', 'sv', 'cs', 'ro', 'el', 'ca', 'eu', 'gl'];
|
||||
const extraStats = [];
|
||||
for (const lang of extraLocales) {
|
||||
const dict = flatten(await loadTranslations(join(SRC, `i18n/${lang}/index.ts`)));
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ interface LanguageOption {
|
|||
const languages: LanguageOption[] = [
|
||||
{ code: 'en', name: 'English', nativeName: 'English', flag: '🇬🇧' },
|
||||
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
|
||||
{ code: 'ca', name: 'Catalan', nativeName: 'Català', flag: 'CA' },
|
||||
{ code: 'eu', name: 'Basque', nativeName: 'Euskara', flag: 'EU' },
|
||||
{ code: 'gl', name: 'Galician', nativeName: 'Galego', flag: 'GL' },
|
||||
{ code: 'pt', name: 'Portuguese', nativeName: 'Português', flag: '🇧🇷' },
|
||||
{ code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
|
||||
{ code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪' },
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ function detectLanguage(): Language {
|
|||
if (tags.some((t) => t.startsWith('cs'))) return 'cs';
|
||||
if (tags.some((t) => t.startsWith('ro'))) return 'ro';
|
||||
if (tags.some((t) => t.startsWith('el'))) return 'el';
|
||||
if (tags.some((t) => t.startsWith('ca'))) return 'ca';
|
||||
if (tags.some((t) => t.startsWith('eu'))) return 'eu';
|
||||
if (tags.some((t) => t.startsWith('gl'))) return 'gl';
|
||||
if (tags.some((t) => t.startsWith('es'))) return 'es';
|
||||
return 'en';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import en from '@/i18n/en';
|
|||
* `react-refresh/only-export-components` rule.
|
||||
*/
|
||||
|
||||
export type Language = 'en' | 'es' | 'th' | 'ja' | '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' | 'th' | 'ja' | '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 TranslationValue = string | { [key: string]: TranslationValue };
|
||||
|
|
@ -39,6 +39,9 @@ export const englishTranslations = en;
|
|||
export const LANGUAGE_LOADERS: Record<Language, () => Promise<Translations>> = {
|
||||
en: async () => en,
|
||||
es: () => import('@/i18n/es').then((m) => m.default),
|
||||
ca: () => import('@/i18n/ca').then((m) => m.default),
|
||||
eu: () => import('@/i18n/eu').then((m) => m.default),
|
||||
gl: () => import('@/i18n/gl').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),
|
||||
|
|
|
|||
1268
src/i18n/ca/batch1.ts
Normal file
1268
src/i18n/ca/batch1.ts
Normal file
File diff suppressed because it is too large
Load diff
1491
src/i18n/ca/batch2.ts
Normal file
1491
src/i18n/ca/batch2.ts
Normal file
File diff suppressed because it is too large
Load diff
1300
src/i18n/ca/batch3.ts
Normal file
1300
src/i18n/ca/batch3.ts
Normal file
File diff suppressed because it is too large
Load diff
1427
src/i18n/ca/batch4.ts
Normal file
1427
src/i18n/ca/batch4.ts
Normal file
File diff suppressed because it is too large
Load diff
1463
src/i18n/ca/batch5.ts
Normal file
1463
src/i18n/ca/batch5.ts
Normal file
File diff suppressed because it is too large
Load diff
1137
src/i18n/ca/batch6.ts
Normal file
1137
src/i18n/ca/batch6.ts
Normal file
File diff suppressed because it is too large
Load diff
1599
src/i18n/ca/batch7.ts
Normal file
1599
src/i18n/ca/batch7.ts
Normal file
File diff suppressed because it is too large
Load diff
9
src/i18n/ca/drift.ts
Normal file
9
src/i18n/ca/drift.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Post-translation corrections for ca.
|
||||
*
|
||||
* Add keys here when a batch translation needs fixing without editing the
|
||||
* generated batch files. Values must also exist in `en`.
|
||||
*/
|
||||
const drift = {};
|
||||
|
||||
export default drift;
|
||||
24
src/i18n/ca/index.ts
Normal file
24
src/i18n/ca/index.ts
Normal 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 ca = deepMerge(
|
||||
{
|
||||
...batch1,
|
||||
...batch2,
|
||||
...batch3,
|
||||
...batch4,
|
||||
...batch5,
|
||||
...batch6,
|
||||
...batch7,
|
||||
},
|
||||
drift,
|
||||
);
|
||||
|
||||
export default ca;
|
||||
1268
src/i18n/eu/batch1.ts
Normal file
1268
src/i18n/eu/batch1.ts
Normal file
File diff suppressed because it is too large
Load diff
1491
src/i18n/eu/batch2.ts
Normal file
1491
src/i18n/eu/batch2.ts
Normal file
File diff suppressed because it is too large
Load diff
1300
src/i18n/eu/batch3.ts
Normal file
1300
src/i18n/eu/batch3.ts
Normal file
File diff suppressed because it is too large
Load diff
1427
src/i18n/eu/batch4.ts
Normal file
1427
src/i18n/eu/batch4.ts
Normal file
File diff suppressed because it is too large
Load diff
1463
src/i18n/eu/batch5.ts
Normal file
1463
src/i18n/eu/batch5.ts
Normal file
File diff suppressed because it is too large
Load diff
1137
src/i18n/eu/batch6.ts
Normal file
1137
src/i18n/eu/batch6.ts
Normal file
File diff suppressed because it is too large
Load diff
1599
src/i18n/eu/batch7.ts
Normal file
1599
src/i18n/eu/batch7.ts
Normal file
File diff suppressed because it is too large
Load diff
9
src/i18n/eu/drift.ts
Normal file
9
src/i18n/eu/drift.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Post-translation corrections for eu.
|
||||
*
|
||||
* Add keys here when a batch translation needs fixing without editing the
|
||||
* generated batch files. Values must also exist in `en`.
|
||||
*/
|
||||
const drift = {};
|
||||
|
||||
export default drift;
|
||||
24
src/i18n/eu/index.ts
Normal file
24
src/i18n/eu/index.ts
Normal 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 eu = deepMerge(
|
||||
{
|
||||
...batch1,
|
||||
...batch2,
|
||||
...batch3,
|
||||
...batch4,
|
||||
...batch5,
|
||||
...batch6,
|
||||
...batch7,
|
||||
},
|
||||
drift,
|
||||
);
|
||||
|
||||
export default eu;
|
||||
1268
src/i18n/gl/batch1.ts
Normal file
1268
src/i18n/gl/batch1.ts
Normal file
File diff suppressed because it is too large
Load diff
1491
src/i18n/gl/batch2.ts
Normal file
1491
src/i18n/gl/batch2.ts
Normal file
File diff suppressed because it is too large
Load diff
1300
src/i18n/gl/batch3.ts
Normal file
1300
src/i18n/gl/batch3.ts
Normal file
File diff suppressed because it is too large
Load diff
1427
src/i18n/gl/batch4.ts
Normal file
1427
src/i18n/gl/batch4.ts
Normal file
File diff suppressed because it is too large
Load diff
1463
src/i18n/gl/batch5.ts
Normal file
1463
src/i18n/gl/batch5.ts
Normal file
File diff suppressed because it is too large
Load diff
1137
src/i18n/gl/batch6.ts
Normal file
1137
src/i18n/gl/batch6.ts
Normal file
File diff suppressed because it is too large
Load diff
1600
src/i18n/gl/batch7.ts
Normal file
1600
src/i18n/gl/batch7.ts
Normal file
File diff suppressed because it is too large
Load diff
9
src/i18n/gl/drift.ts
Normal file
9
src/i18n/gl/drift.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Post-translation corrections for gl.
|
||||
*
|
||||
* Add keys here when a batch translation needs fixing without editing the
|
||||
* generated batch files. Values must also exist in `en`.
|
||||
*/
|
||||
const drift = {};
|
||||
|
||||
export default drift;
|
||||
24
src/i18n/gl/index.ts
Normal file
24
src/i18n/gl/index.ts
Normal 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 gl = deepMerge(
|
||||
{
|
||||
...batch1,
|
||||
...batch2,
|
||||
...batch3,
|
||||
...batch4,
|
||||
...batch5,
|
||||
...batch6,
|
||||
...batch7,
|
||||
},
|
||||
drift,
|
||||
);
|
||||
|
||||
export default gl;
|
||||
|
|
@ -22,6 +22,9 @@ import {
|
|||
import {
|
||||
enUS,
|
||||
es as esLocale,
|
||||
ca as caLocale,
|
||||
eu as euLocale,
|
||||
gl as glLocale,
|
||||
th as thLocale,
|
||||
ja as jaLocale,
|
||||
ptBR as ptLocale,
|
||||
|
|
@ -54,6 +57,15 @@ export function setDateLocale(language: Language): void {
|
|||
if (language === 'es') {
|
||||
activeLocale = esLocale;
|
||||
activeCode = 'es-ES';
|
||||
} else if (language === 'ca') {
|
||||
activeLocale = caLocale;
|
||||
activeCode = 'ca-ES';
|
||||
} else if (language === 'eu') {
|
||||
activeLocale = euLocale;
|
||||
activeCode = 'eu-ES';
|
||||
} else if (language === 'gl') {
|
||||
activeLocale = glLocale;
|
||||
activeCode = 'gl-ES';
|
||||
} else if (language === 'th') {
|
||||
activeLocale = thLocale;
|
||||
activeCode = 'th-TH';
|
||||
|
|
|
|||
Loading…
Reference in a new issue