Skip to content

feat: (i18n) French localization (en/fr) for the desktop UI#591

Draft
Davidnet wants to merge 26 commits into
mainfrom
feat/i18n-french
Draft

feat: (i18n) French localization (en/fr) for the desktop UI#591
Davidnet wants to merge 26 commits into
mainfrom
feat/i18n-french

Conversation

@Davidnet

@Davidnet Davidnet commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

Adds internationalization to the desktop/renderer UI with react-i18next and ships French (fr) alongside the English (en) base, scoped to French only per epic #580.

Infrastructure & extraction (#571#576)

  • i18next + react-i18next + language detector, initialized in src/i18n/index.ts with 8 namespaces (common, settings, dashboard, activity, mappings, about, onboarding, modals).
  • Every user-facing string in the renderer extracted to namespaced en/fr JSON: core UI & Playground, Settings (all sections), Dashboard & sidebar, Activity & Mappings, and the onboarding/about/modal dialogs.
  • Handles the hard cases: class-component ErrorBoundary via the withTranslation HOC, embedded markup (links, bold, code) via <Trans>, and i18next pluralization for counts.

Language selection & Electron menu (#577#578)

  • Language selector in Settings (LanguageSection), shown in both web and desktop builds; persistence is handled by the i18next detector's localStorage cache.
  • The native application/tray menus (built in the Electron main process, outside react-i18next) are translated via a self-contained menu-i18n.js string table. The renderer pushes its resolved language over a new set-language IPC channel; the main process persists it and rebuilds the menus, seeding from config at startup.

Tooling, CI & docs (#579)

  • scripts/check-i18n-parity.js (npm run i18n:check, wired into CI): a plural-aware en↔fr parity check — base-key parity, plural completeness against each locale's CLDR categories, matching {{placeholder}} sets, and balanced/matching <Trans> tags. It caught French plural keys missing the _many category (i18next does not fall back many → other), fixed here.
  • eslint-plugin-i18next no-literal-string in jsx-text-only mode as an advisory warning over the renderer.
  • New docs/10-internationalization.md (linked from the docs index).

Notes

  • French strings are machine-drafted and pending native-speaker review; the parity check guarantees structural correctness, not translation quality.
  • No changeset is included yet.

Davidnet added 7 commits July 6, 2026 19:31
…gs (en/fr)

Scoped to English (base) + French per the team decision on epic #580
(es/ja out of scope).

#571 — i18next infrastructure:
- Add react-i18next, i18next, i18next-browser-languagedetector
- src/i18n init: en/fr, load: languageOnly (fr-FR→fr), fallback en,
  localStorage→navigator detection, Suspense disabled
- Namespaced locale JSON (common/settings/dashboard/onboarding/activity/
  mappings/about); side-effect import wired into index.js

#572 — core UI extraction (common namespace):
- privacy-proxy-ui.tsx (playground, tabs, status bar, footer, banners)
- ErrorBoundary.tsx via withTranslation HOC (class component)
- Trans for the footer link and model-health Settings link; count
  pluralization and provider/port interpolation

#573 — settings extraction (settings namespace):
- SettingsView, ProvidersSection, PIISection, AdvancedSection,
  CertificatesSection
- Brand names kept; only "Custom Provider" localized
- 26 PII entity labels with capitalize fallback for unknown labels

French files are machine-drafted, pending native-speaker review.
No visible change for English users.
Wire DashboardView and Sidebar to the dashboard namespace: KPI cards,
range selector, area chart, donut, provider bars, highlights, recent
intercept feed, relative-time labels, and the sidebar nav/server status.
Number formatting via fmt() is preserved by passing pre-formatted values
as plain interpolation vars rather than i18next count.
Wire ActivityView and MappingsView to the activity/mappings namespaces:
page headers, clear-confirmation dialogs, JSON toggle, table headers,
row-level direction/message/blocked/report controls, sort aria-labels,
per-row delete controls, load-more, and footer summaries.

Uses i18next pluralization (_one/_other with {{count}}) for entry
counts, message counts, confirmation bodies, and footer summaries.
Thread the translator into logFormatters' getDirectionLabel and
formatMessage so shared log-display strings localize too.
Fill the onboarding and about namespaces and add a new modals namespace
(registered in i18n/index.ts) for the CA-cert and misclassification
dialogs.

- WelcomeModal: role picker, explainer, and the three privacy promises.
- AboutView: header, hero, resource links, no-support notice, and a
  <Trans> copyright line with the 575 Lab link and year interpolation.
- MisclassificationModal: title, entity/confidence details, feedback
  form, and submit.
- CACertSetupModal: intro, tabs, command copy controls, and the full
  system/Firefox/Chrome instruction lists rendered via <Trans> with a
  shared set of inline-markup components (bold/code/accent/amber).

Shared Close aria-labels reuse common:actions.close. Certificate paths
and shell commands stay literal inside the translated strings.
Add a LanguageSection to Settings (shown everywhere, since language is a
universal UI preference) that switches the interface language via
i18next.changeLanguage over the supported locales (en/fr).

Persistence needs no extra code: the i18next language detector is
configured with caches: ["localStorage"], so the choice is written
through on change and restored on next launch. Region subtags like
fr-FR normalize to the fr base via load: "languageOnly". Syncing the
selection to the Electron native menu is handled separately (#578).
The application and tray menus are built in the Electron main process,
which has no access to react-i18next. Add a self-contained main-process
string table (menu-i18n.js) with en/fr labels, a normalize/select/lookup
API, and {{name}} interpolation, then route every hardcoded menu label
through it.

The UI language is persisted as a config field (get-/set-language IPC):
- At startup the menu is built from the last persisted language.
- On set-language the config is saved and the app/tray menus rebuilt.
- The renderer is the source of truth: i18n pushes its resolved base
  language to the main process on init and on every languageChanged,
  guarded so the web build (no electronAPI) is unaffected.
Add tooling to keep translations in sync and guide future extraction:

- scripts/check-i18n-parity.js (npm run i18n:check) — a plural-aware
  en<->fr parity check wired into the frontend-lint CI job. Per namespace
  it verifies base-key parity (plural suffixes normalized), plural
  completeness against each locale's CLDR categories, matching
  {{placeholder}} sets, and matching/balanced <Trans> tags. It caught
  French plural keys missing the _many category — fixed here across
  common/activity/mappings (i18next does not fall back many->other, so a
  missing _many would render the raw key at millions-scale counts).
- eslint-plugin-i18next: no-literal-string in jsx-text-only mode as a
  warning over src/**/*.{jsx,tsx}, surfacing un-extracted UI text without
  gating CI (technical/brand literals in attributes are out of scope).
- docs/10-internationalization.md documenting the setup, usage, the
  French many-plural gotcha, and the tooling; linked from docs/README.

French drafts remain machine-translated pending native review.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Warning

This PR touches 3+ distinct areas of the codebase.

Consider splitting into smaller, focused PRs — each covering a single semantic type.
This makes reviews easier and keeps the git history clean.

Categories found:

docs:

  • docs/10-internationalization.md
  • docs/README.md

code:

  • src/frontend/eslint.config.js
  • src/frontend/index.js
  • src/frontend/scripts/check-i18n-parity.js
  • src/frontend/src/components/ErrorBoundary.tsx
  • src/frontend/src/components/about/AboutView.tsx
  • src/frontend/src/components/activity/ActivityView.tsx
  • src/frontend/src/components/dashboard/DashboardView.tsx
  • src/frontend/src/components/dashboard/Sidebar.tsx
  • src/frontend/src/components/mappings/MappingsView.tsx
  • src/frontend/src/components/modals/CACertSetupModal.tsx
  • src/frontend/src/components/modals/MisclassificationModal.tsx
  • src/frontend/src/components/onboarding/WelcomeModal.tsx
  • src/frontend/src/components/privacy-proxy-ui.tsx
  • src/frontend/src/components/settings/AdvancedSection.tsx
  • src/frontend/src/components/settings/CertificatesSection.tsx
  • src/frontend/src/components/settings/LanguageSection.tsx
  • src/frontend/src/components/settings/PIISection.tsx
  • src/frontend/src/components/settings/ProvidersSection.tsx
  • src/frontend/src/components/settings/SettingsView.tsx
  • src/frontend/src/electron/electron-main.js
  • src/frontend/src/electron/electron-preload.js
  • src/frontend/src/electron/electron.d.ts
  • src/frontend/src/electron/ipc-handlers.js
  • src/frontend/src/electron/menu-i18n.js
  • src/frontend/src/i18n/index.ts
  • src/frontend/src/i18n/locales/en/about.json
  • src/frontend/src/i18n/locales/en/activity.json
  • src/frontend/src/i18n/locales/en/common.json
  • src/frontend/src/i18n/locales/en/dashboard.json
  • src/frontend/src/i18n/locales/en/mappings.json
  • src/frontend/src/i18n/locales/en/modals.json
  • src/frontend/src/i18n/locales/en/onboarding.json
  • src/frontend/src/i18n/locales/en/settings.json
  • src/frontend/src/i18n/locales/fr/about.json
  • src/frontend/src/i18n/locales/fr/activity.json
  • src/frontend/src/i18n/locales/fr/common.json
  • src/frontend/src/i18n/locales/fr/dashboard.json
  • src/frontend/src/i18n/locales/fr/mappings.json
  • src/frontend/src/i18n/locales/fr/modals.json
  • src/frontend/src/i18n/locales/fr/onboarding.json
  • src/frontend/src/i18n/locales/fr/settings.json
  • src/frontend/src/utils/logFormatters.ts

chore:

  • package-lock.json
  • src/frontend/package.json

ci:

  • .github/workflows/lint-and-test.yml

Davidnet added 19 commits July 7, 2026 10:03
The backend sends the delta window as a raw '<number><unit>' string
(e.g. '7d') that was interpolated verbatim. Localize the unit suffix so
French renders '7j' (jours) instead of '7d'; English is unchanged and
unknown units fall through untouched.
Treat PII as feminine plural throughout: the KPI card now reads
"PII protégées" (was "Éléments de PII protégés", masculine via
éléments), parallel to "PII masquées au fil du temps".
French wording:
- 'renseignements personnels' for the PII card and chart titles
- 'Types de données masquées', 'données détectées' (donut)
- 'Requêtes par fournisseur', 'Score de détection' (avoid 'Conf.')
- 'req./min', 'masquée' (agrees with requête)
- latency caption -> 'La confidentialité, sans ralentir vos requêtes.'
- entity labels: Courriel, Nom (Surname)
- delta window renders '7 j' (spaced unit)

Locale-aware number formatting in DashboardView: fmt() now groups per
active language ('1 051 ms' fr / '1,051 ms' en) and detection confidence
uses a localized decimal ('79,0 % en moy.' / '79.0% avg').
Unify the personal-information term across the French UI: replace both
the "PII" acronym and "données personnelles" with "renseignements
personnels" (masculine plural), flipping adjective/pronoun agreement
accordingly (détectées->détectés, elles->ils, fausse->faux, etc.).

Covers common (tagline, playground plurals, placeholder), settings,
onboarding, activity, mappings, modals, about, and the dashboard count
badge. The About description keeps the "(PII)" acronym note; the donut's
generic "données" labels are left as previously chosen.
- Donut title -> 'Types de renseignements masqués' for terminology
  consistency.
- The French center label ('renseignements détectés') overflows the
  small donut, so it is set to an empty string; the Donut now renders
  the center label only when non-empty (English keeps 'entities'), a
  data-driven hide that works for any locale.
Catch the capitalized playground labels missed by the earlier
(case-sensitive) sweep:
- piiExposed -> 'Renseignements personnels exposés'
- piiProtected -> 'Renseignements personnels protégés'
- personalInfoRestored -> 'Renseignements personnels restaurés'
- personalInfoRemoved -> 'Renseignements personnels masqués' (the info
  is masked/replaced, not deleted, so 'masqués' not 'supprimées')
- regex help: 'type de renseignement personnel détecté'
- Localize Playground average-confidence formatting (fr '90,9 %').
- Tabs: 'Envoyée à ...' (agrees with la requête); 'Réponse de ...' /
  'Réponse du LLM' instead of 'Renvoyé par ...'.
- 'renseignement personnel fictif reçu' instead of 'faux ... reçu'.
- Report button/title: 'Signaler une erreur de classification'.
- subtitle emphasizes the fake replacement value
- columns: Type de renseignement / Valeur originale / Remplacement /
  Première détection
- subtitle: 'Configurez les fournisseurs, la détection...'
- providers subtitle: 'identifiants de modèles' (plural)
- keychain note: 'stockées de façon sécurisée à l'aide du trousseau du système'
- save button: 'Enregistrer les paramètres des fournisseurs'
- 'certificat d'autorité (de certification)' -> 'certificat racine'
- model status 'Fonctionnel' -> 'Opérationnel'
- 'certificat CA' -> 'certificat racine' across reveal buttons, the CA
  setup modal title, and the import step.
- Certificates subtitle: 'Faites confiance...' -> 'Installez le
  certificat racine de Kiji...'.
- 'pour intercepter le trafic HTTPS' -> 'pour permettre l'interception
  du trafic HTTPS'
- 'à l'échelle du système ou par navigateur' -> 'au niveau du système
  ou dans le navigateur'
- revealHelp: 'Ouvre le dossier...' (describes the action, not an order)
@Davidnet Davidnet changed the title feat(i18n): French localization (en/fr) for the desktop UI feat: (i18n) French localization (en/fr) for the desktop UI Jul 7, 2026
@Davidnet Davidnet requested a review from hanneshapke July 7, 2026 19:19
@Davidnet

Davidnet commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Few wireframes in fr
image

image image image

@hanneshapke

Copy link
Copy Markdown
Collaborator

Language selector in Settings (LanguageSection), shown in both web and desktop builds; persistence is handled by the i18next detector's localStorage cache.

I think it works for the Electron app, but what happens if the user uses the Linux server? User A sets French (stored locally as French), next user sees English?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants