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,
|
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 — 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)
|
### 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`,
|
||||||
`labOptions`, `cordnUi`, `socialUi`, `walletUi`, `marketTerms`, …). Backfilled
|
`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;
|
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', '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 = [];
|
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`)));
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ 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: '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: 'pt', name: 'Portuguese', nativeName: 'Português', flag: '🇧🇷' },
|
||||||
{ code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
|
{ code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
|
||||||
{ code: 'de', name: 'German', nativeName: 'Deutsch', 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('cs'))) return 'cs';
|
||||||
if (tags.some((t) => t.startsWith('ro'))) return 'ro';
|
if (tags.some((t) => t.startsWith('ro'))) return 'ro';
|
||||||
if (tags.some((t) => t.startsWith('el'))) return 'el';
|
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';
|
if (tags.some((t) => t.startsWith('es'))) return 'es';
|
||||||
return 'en';
|
return 'en';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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' | '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 TranslationKey = string;
|
||||||
type TranslationValue = string | { [key: string]: TranslationValue };
|
type TranslationValue = string | { [key: string]: TranslationValue };
|
||||||
|
|
@ -39,6 +39,9 @@ export const englishTranslations = en;
|
||||||
export const LANGUAGE_LOADERS: Record<Language, () => Promise<Translations>> = {
|
export const LANGUAGE_LOADERS: Record<Language, () => Promise<Translations>> = {
|
||||||
en: async () => en,
|
en: async () => en,
|
||||||
es: () => import('@/i18n/es').then((m) => m.default),
|
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),
|
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/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 {
|
import {
|
||||||
enUS,
|
enUS,
|
||||||
es as esLocale,
|
es as esLocale,
|
||||||
|
ca as caLocale,
|
||||||
|
eu as euLocale,
|
||||||
|
gl as glLocale,
|
||||||
th as thLocale,
|
th as thLocale,
|
||||||
ja as jaLocale,
|
ja as jaLocale,
|
||||||
ptBR as ptLocale,
|
ptBR as ptLocale,
|
||||||
|
|
@ -54,6 +57,15 @@ export function setDateLocale(language: Language): void {
|
||||||
if (language === 'es') {
|
if (language === 'es') {
|
||||||
activeLocale = esLocale;
|
activeLocale = esLocale;
|
||||||
activeCode = 'es-ES';
|
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') {
|
} else if (language === 'th') {
|
||||||
activeLocale = thLocale;
|
activeLocale = thLocale;
|
||||||
activeCode = 'th-TH';
|
activeCode = 'th-TH';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue