Skip to content

Commit dffc292

Browse files
authored
I18n on portal (Stirling-Tools#6761)
## 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.
1 parent c95fb89 commit dffc292

112 files changed

Lines changed: 4805 additions & 2262 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/editor/src/core/i18n.ts

Lines changed: 21 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,29 @@
11
import i18n from "i18next";
22
import { initReactI18next } from "react-i18next";
33
import LanguageDetector from "i18next-browser-languagedetector";
4-
import TomlBackend from "@app/i18n/tomlBackend";
5-
6-
// Define supported languages (based on your existing translations)
7-
export const supportedLanguages = {
8-
"en-US": "English (US)",
9-
"en-GB": "English (UK)",
10-
"ar-AR": "العربية",
11-
"az-AZ": "Azərbaycan Dili",
12-
"bg-BG": "Български",
13-
"ca-CA": "Català",
14-
"cs-CZ": "Česky",
15-
"da-DK": "Dansk",
16-
"de-DE": "Deutsch",
17-
"el-GR": "Ελληνικά",
18-
"es-ES": "Español",
19-
"eu-ES": "Euskara",
20-
"fa-IR": "فارسی",
21-
"fr-FR": "Français",
22-
"ga-IE": "Gaeilge",
23-
"hi-IN": "हिंदी",
24-
"hr-HR": "Hrvatski",
25-
"hu-HU": "Magyar",
26-
"id-ID": "Bahasa Indonesia",
27-
"it-IT": "Italiano",
28-
"ja-JP": "日本語",
29-
"ko-KR": "한국어",
30-
"ml-ML": "മലയാളം",
31-
"nl-NL": "Nederlands",
32-
"no-NB": "Norsk",
33-
"pl-PL": "Polski",
34-
"pt-BR": "Português (Brasil)",
35-
"pt-PT": "Português",
36-
"ro-RO": "Română",
37-
"ru-RU": "Русский",
38-
"sk-SK": "Slovensky",
39-
"sl-SI": "Slovenščina",
40-
"sr-LATN-RS": "Srpski",
41-
"sv-SE": "Svenska",
42-
"th-TH": "ไทย",
43-
"tr-TR": "Türkçe",
44-
"uk-UA": "Українська",
45-
"vi-VN": "Tiếng Việt",
46-
"zh-BO": "བོད་ཡིག",
47-
"zh-CN": "简体中文",
48-
"zh-TW": "繁體中文",
4+
import TomlBackend from "@shared/i18n/tomlBackend";
5+
import {
6+
supportedLanguages,
7+
rtlLanguages,
8+
I18N_STORAGE_KEYS,
9+
LanguageSource,
10+
normalizeLanguageCode,
11+
toUnderscoreFormat,
12+
toUnderscoreLanguages,
13+
} from "@shared/i18n/languages";
14+
15+
// Language metadata and code helpers are shared with the portal via
16+
// @shared/i18n. Re-export them so existing `@app/i18n` consumers are unchanged.
17+
export {
18+
supportedLanguages,
19+
rtlLanguages,
20+
I18N_STORAGE_KEYS,
21+
LanguageSource,
22+
normalizeLanguageCode,
23+
toUnderscoreFormat,
24+
toUnderscoreLanguages,
4925
};
5026

51-
// RTL languages (based on your existing language.direction property)
52-
export const rtlLanguages = ["ar-AR", "fa-IR"];
53-
54-
// LocalStorage keys for i18next
55-
export const I18N_STORAGE_KEYS = {
56-
LANGUAGE: "i18nextLng",
57-
LANGUAGE_SOURCE: "i18nextLng-source",
58-
} as const;
59-
60-
/**
61-
* Language selection priority levels
62-
* Higher number = higher priority (cannot be overridden by lower priority)
63-
*/
64-
export enum LanguageSource {
65-
Fallback = 0,
66-
Browser = 1,
67-
ServerDefault = 2,
68-
User = 3,
69-
}
70-
7127
i18n
7228
.use(TomlBackend)
7329
.use(LanguageDetector)
@@ -139,36 +95,6 @@ i18n.on("initialized", () => {
13995
}
14096
});
14197

142-
export function normalizeLanguageCode(languageCode: string): string {
143-
// Replace underscores with hyphens to align with i18next/translation file naming
144-
const hyphenated = languageCode.replace(/_/g, "-");
145-
const [base, ...rest] = hyphenated.split("-");
146-
147-
if (rest.length === 0) {
148-
return base.toLowerCase();
149-
}
150-
151-
const normalizedParts = rest.map((part) =>
152-
part.length <= 3 ? part.toUpperCase() : part,
153-
);
154-
return [base.toLowerCase(), ...normalizedParts].join("-");
155-
}
156-
157-
/**
158-
* Convert language codes to underscore format (e.g., en-US → en_US)
159-
* Used for backend API communication which expects underscore format
160-
*/
161-
export function toUnderscoreFormat(languageCode: string): string {
162-
return languageCode.replace(/-/g, "_");
163-
}
164-
165-
/**
166-
* Convert array of language codes to underscore format
167-
*/
168-
export function toUnderscoreLanguages(languages: string[]): string[] {
169-
return languages.map(toUnderscoreFormat);
170-
}
171-
17298
/**
17399
* Get the current language source priority
174100
*/

frontend/editor/src/core/i18n/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import i18n from "i18next";
22
import { initReactI18next } from "react-i18next";
3-
import TomlBackend from "@app/i18n/tomlBackend";
3+
import TomlBackend from "@shared/i18n/tomlBackend";
44

55
i18n
66
.use(TomlBackend)

0 commit comments

Comments
 (0)