## Overview Internationalizes the **developer portal**, which previously had **zero i18n** — every string was hardcoded across ~118 components. Rather than stand up a parallel system, this shares the **editor's** existing i18n setup (same TOML locale format, same Crowdin pipeline), then converts every portal surface to `react-i18next` and adds a CI guard so coverage can't regress. ## What's included ### 🔗 Shared i18n core (`@shared/i18n`) - Extracts the editor's `TomlBackend` (HTTP loader for `public/locales/{lng}/translation.toml`) and language metadata/helpers (the 42-language list, RTL set, `LanguageSource` priority, code normalizers) into `frontend/shared/i18n/`. - The **editor** now imports and re-exports these from `@shared/i18n` — its 20+ consumers are unchanged. Its local `tomlBackend.ts` is deleted. - The **portal** builds its own i18next instance from the shared core, with **en-US as the source of truth** and the same `/locales/{lng}/translation.toml` layout. ### 🌍 Full portal coverage - Every view and component converted to `t()` — all feature areas (home, pipelines, sources, infrastructure, usage, documents, agent-builder, editor-admin, policies, users, docs, catalogue, components view) plus app shell, nav, modals, and the home/domain widgets. - **1108 keys across ~30 namespaces** in `portal/public/locales/en-US/translation.toml`, grouped by feature; shared strings under `[common]`. Plurals use i18next count forms; dynamic labels (nav, settings sections, status badges) use template keys against populated tables. - Data-driven strings (values from `@portal/api/*` mocks, enum/id values, code samples) are intentionally left untranslated — they're data, not UI chrome. ### ✅ CI coverage guard - `portal/scripts/check-i18n.mjs` fails if any static `t("key")` in portal source is missing from the en-US locale. Wired into `frontend:check` and `frontend:check:all`, so missed keys break CI. This mirrors the editor's `missingTranslations` test for the portal, which has no vitest harness of its own. ## Testing - `task frontend:check:all` passes locally (typecheck all variants, lint, format, **portal i18n guard**, builds, tests, storybook). - Every static `t()` key verified to resolve in the locale (1108 keys / 186 source files); all dynamic key prefixes map to populated tables. - Runtime sweep of all 12 portal routes shows **no unresolved keys** on screen; nav labels, plurals, and array-backed copy all render real text. ## Follow-ups (not in this PR) - **Crowdin** — register `frontend/portal/public/locales/` as a source so portal strings flow through the same translation pipeline as the editor (an ops step on the Crowdin side; there's no Crowdin config in the repo). - Only `en-US` is populated; other languages will arrive via the pipeline.
98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
/**
|
|
* Shared language metadata and code helpers for the editor and portal i18n
|
|
* setups. Pure data + string utilities — no i18next instance or app state.
|
|
*/
|
|
|
|
/** Supported languages, keyed by BCP-47-ish code → native display name. */
|
|
export const supportedLanguages: Record<string, string> = {
|
|
"en-US": "English (US)",
|
|
"en-GB": "English (UK)",
|
|
"ar-AR": "العربية",
|
|
"az-AZ": "Azərbaycan Dili",
|
|
"bg-BG": "Български",
|
|
"ca-CA": "Català",
|
|
"cs-CZ": "Česky",
|
|
"da-DK": "Dansk",
|
|
"de-DE": "Deutsch",
|
|
"el-GR": "Ελληνικά",
|
|
"es-ES": "Español",
|
|
"eu-ES": "Euskara",
|
|
"fa-IR": "فارسی",
|
|
"fr-FR": "Français",
|
|
"ga-IE": "Gaeilge",
|
|
"hi-IN": "हिंदी",
|
|
"hr-HR": "Hrvatski",
|
|
"hu-HU": "Magyar",
|
|
"id-ID": "Bahasa Indonesia",
|
|
"it-IT": "Italiano",
|
|
"ja-JP": "日本語",
|
|
"ko-KR": "한국어",
|
|
"ml-ML": "മലയാളം",
|
|
"nl-NL": "Nederlands",
|
|
"no-NB": "Norsk",
|
|
"pl-PL": "Polski",
|
|
"pt-BR": "Português (Brasil)",
|
|
"pt-PT": "Português",
|
|
"ro-RO": "Română",
|
|
"ru-RU": "Русский",
|
|
"sk-SK": "Slovensky",
|
|
"sl-SI": "Slovenščina",
|
|
"sr-LATN-RS": "Srpski",
|
|
"sv-SE": "Svenska",
|
|
"th-TH": "ไทย",
|
|
"tr-TR": "Türkçe",
|
|
"uk-UA": "Українська",
|
|
"vi-VN": "Tiếng Việt",
|
|
"zh-BO": "བོད་ཡིག",
|
|
"zh-CN": "简体中文",
|
|
"zh-TW": "繁體中文",
|
|
};
|
|
|
|
/** Right-to-left languages (drives `document.dir`). */
|
|
export const rtlLanguages = ["ar-AR", "fa-IR"];
|
|
|
|
/** LocalStorage keys for i18next language persistence. */
|
|
export const I18N_STORAGE_KEYS = {
|
|
LANGUAGE: "i18nextLng",
|
|
LANGUAGE_SOURCE: "i18nextLng-source",
|
|
} as const;
|
|
|
|
/**
|
|
* Language selection priority levels.
|
|
* Higher number = higher priority (cannot be overridden by lower priority).
|
|
*/
|
|
export enum LanguageSource {
|
|
Fallback = 0,
|
|
Browser = 1,
|
|
ServerDefault = 2,
|
|
User = 3,
|
|
}
|
|
|
|
export function normalizeLanguageCode(languageCode: string): string {
|
|
// Replace underscores with hyphens to align with i18next/translation file naming
|
|
const hyphenated = languageCode.replace(/_/g, "-");
|
|
const [base, ...rest] = hyphenated.split("-");
|
|
|
|
if (rest.length === 0) {
|
|
return base.toLowerCase();
|
|
}
|
|
|
|
const normalizedParts = rest.map((part) =>
|
|
part.length <= 3 ? part.toUpperCase() : part,
|
|
);
|
|
return [base.toLowerCase(), ...normalizedParts].join("-");
|
|
}
|
|
|
|
/**
|
|
* Convert language codes to underscore format (e.g., en-US → en_US).
|
|
* Used for backend API communication which expects underscore format.
|
|
*/
|
|
export function toUnderscoreFormat(languageCode: string): string {
|
|
return languageCode.replace(/-/g, "_");
|
|
}
|
|
|
|
/** Convert an array of language codes to underscore format. */
|
|
export function toUnderscoreLanguages(languages: string[]): string[] {
|
|
return languages.map(toUnderscoreFormat);
|
|
}
|