From 888cc37ca380a28983207bc181a0cd7eee3a6d61 Mon Sep 17 00:00:00 2001 From: Davidnet Date: Mon, 6 Jul 2026 19:31:45 -0400 Subject: [PATCH 01/28] feat(i18n): set up i18next infra and extract core UI + settings strings (en/fr) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- package-lock.json | 105 +++++++++- src/frontend/index.js | 1 + src/frontend/package.json | 5 +- src/frontend/src/components/ErrorBoundary.tsx | 35 ++-- .../src/components/privacy-proxy-ui.tsx | 186 +++++++++++------- .../components/settings/AdvancedSection.tsx | 55 ++++-- .../settings/CertificatesSection.tsx | 23 ++- .../src/components/settings/PIISection.tsx | 112 ++++------- .../components/settings/ProvidersSection.tsx | 111 +++++++---- .../src/components/settings/SettingsView.tsx | 12 +- src/frontend/src/i18n/index.ts | 89 +++++++++ src/frontend/src/i18n/locales/en/about.json | 1 + .../src/i18n/locales/en/activity.json | 1 + src/frontend/src/i18n/locales/en/common.json | 102 ++++++++++ .../src/i18n/locales/en/dashboard.json | 1 + .../src/i18n/locales/en/mappings.json | 1 + .../src/i18n/locales/en/onboarding.json | 1 + .../src/i18n/locales/en/settings.json | 152 ++++++++++++++ src/frontend/src/i18n/locales/fr/about.json | 1 + .../src/i18n/locales/fr/activity.json | 1 + src/frontend/src/i18n/locales/fr/common.json | 102 ++++++++++ .../src/i18n/locales/fr/dashboard.json | 1 + .../src/i18n/locales/fr/mappings.json | 1 + .../src/i18n/locales/fr/onboarding.json | 1 + .../src/i18n/locales/fr/settings.json | 152 ++++++++++++++ 25 files changed, 1009 insertions(+), 243 deletions(-) create mode 100644 src/frontend/src/i18n/index.ts create mode 100644 src/frontend/src/i18n/locales/en/about.json create mode 100644 src/frontend/src/i18n/locales/en/activity.json create mode 100644 src/frontend/src/i18n/locales/en/common.json create mode 100644 src/frontend/src/i18n/locales/en/dashboard.json create mode 100644 src/frontend/src/i18n/locales/en/mappings.json create mode 100644 src/frontend/src/i18n/locales/en/onboarding.json create mode 100644 src/frontend/src/i18n/locales/en/settings.json create mode 100644 src/frontend/src/i18n/locales/fr/about.json create mode 100644 src/frontend/src/i18n/locales/fr/activity.json create mode 100644 src/frontend/src/i18n/locales/fr/common.json create mode 100644 src/frontend/src/i18n/locales/fr/dashboard.json create mode 100644 src/frontend/src/i18n/locales/fr/mappings.json create mode 100644 src/frontend/src/i18n/locales/fr/onboarding.json create mode 100644 src/frontend/src/i18n/locales/fr/settings.json diff --git a/package-lock.json b/package-lock.json index 068a9207..8503e47f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1700,8 +1700,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -9086,6 +9087,15 @@ "node": ">= 12" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-webpack-plugin": { "version": "5.6.7", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz", @@ -9261,6 +9271,43 @@ "node": ">=10.18" } }, + "node_modules/i18next": { + "version": "26.3.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.4.tgz", + "integrity": "sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", @@ -11723,6 +11770,33 @@ "react": "^18.3.1" } }, + "node_modules/react-i18next": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", + "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "dev": true, @@ -13439,7 +13513,7 @@ }, "node_modules/typescript": { "version": "5.9.3", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -13568,6 +13642,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", @@ -13624,6 +13707,15 @@ "node": ">=0.6.0" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/watchpack": { "version": "2.5.1", "dev": true, @@ -14190,7 +14282,7 @@ }, "src/frontend": { "name": "kiji-privacy-proxy", - "version": "1.4.1", + "version": "1.5.0", "dependencies": { "@sentry/electron": "^7.13.0", "@types/dompurify": "^3.2.0", @@ -14198,9 +14290,12 @@ "electron-updater": "^6.8.9", "express": "^5.2.1", "http-proxy-middleware": "^3.0.5", + "i18next": "^26.3.4", + "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^1.7.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-i18next": "^17.0.8" }, "devDependencies": { "@babel/core": "^7.29.7", diff --git a/src/frontend/index.js b/src/frontend/index.js index 93d5a280..96b3e538 100644 --- a/src/frontend/index.js +++ b/src/frontend/index.js @@ -1,6 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import "./src/styles/styles.css"; +import "./src/i18n"; import AppShell from "./src/components/AppShell.tsx"; import ErrorBoundary from "./src/components/ErrorBoundary.tsx"; import * as Sentry from "@sentry/electron/renderer"; diff --git a/src/frontend/package.json b/src/frontend/package.json index da470477..4810070e 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -28,9 +28,12 @@ "electron-updater": "^6.8.9", "express": "^5.2.1", "http-proxy-middleware": "^3.0.5", + "i18next": "^26.3.4", + "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^1.7.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-i18next": "^17.0.8" }, "devDependencies": { "@babel/core": "^7.29.7", diff --git a/src/frontend/src/components/ErrorBoundary.tsx b/src/frontend/src/components/ErrorBoundary.tsx index ddeb8053..ebdbcd53 100644 --- a/src/frontend/src/components/ErrorBoundary.tsx +++ b/src/frontend/src/components/ErrorBoundary.tsx @@ -1,7 +1,8 @@ import { Component, ErrorInfo, ReactNode } from "react"; import { AlertCircle, RefreshCw } from "lucide-react"; +import { withTranslation, WithTranslation } from "react-i18next"; -interface Props { +interface Props extends WithTranslation { children: ReactNode; } @@ -54,6 +55,7 @@ class ErrorBoundary extends Component { }; render(): ReactNode { + const { t } = this.props; if (this.state.hasError) { return (
@@ -63,12 +65,8 @@ class ErrorBoundary extends Component {
-

- Oops! Something went wrong -

-

- The application encountered an unexpected error -

+

{t("error.title")}

+

{t("error.subtitle")}

@@ -78,7 +76,7 @@ class ErrorBoundary extends Component { {this.state.error && (

- Error Message: + {t("error.messageLabel")}

{this.state.error.message} @@ -89,7 +87,7 @@ class ErrorBoundary extends Component { {this.state.errorInfo && (

- Stack Trace (click to expand) + {t("error.stackTrace")}
                     {this.state.errorInfo.componentStack}
@@ -104,29 +102,26 @@ class ErrorBoundary extends Component {
                   className="flex items-center gap-2 px-6 py-3 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors font-medium"
                 >
                   
-                  Try Again
+                  {t("error.tryAgain")}
                 
                 
               
{/* Help Text */}

- What can you do? + {t("error.whatCanYouDo")}

    -
  • Click "Try Again" to reset the component
  • -
  • Click "Reload Application" to refresh the entire app
  • -
  • - If the problem persists, check the browser console for more - details -
  • -
  • Consider reporting this issue to the development team
  • +
  • {t("error.help.tryAgain")}
  • +
  • {t("error.help.reload")}
  • +
  • {t("error.help.console")}
  • +
  • {t("error.help.report")}
@@ -139,4 +134,4 @@ class ErrorBoundary extends Component { } } -export default ErrorBoundary; +export default withTranslation("common")(ErrorBoundary); diff --git a/src/frontend/src/components/privacy-proxy-ui.tsx b/src/frontend/src/components/privacy-proxy-ui.tsx index 497b96a5..7d884b56 100644 --- a/src/frontend/src/components/privacy-proxy-ui.tsx +++ b/src/frontend/src/components/privacy-proxy-ui.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useRef } from "react"; +import { useTranslation, Trans } from "react-i18next"; import { Send, AlertCircle, @@ -58,6 +59,8 @@ export default function PrivacyProxyUI({ onRequestSettings, onRequestAbout, }: PrivacyProxyUIProps = {}) { + const { t } = useTranslation("common"); + // UI toggle state (simple enough to stay in the component) const [isMenuOpen, setIsMenuOpen] = useState(false); const [isScrolled, setIsScrolled] = useState(false); @@ -150,13 +153,13 @@ export default function PrivacyProxyUI({
Kiji mascot
- Protecting your data… + {t("playground.loadingOverlay")}
@@ -181,7 +184,7 @@ export default function PrivacyProxyUI({ @@ -195,7 +198,7 @@ export default function PrivacyProxyUI({ className="w-full text-left px-3 py-2.5 text-sm text-stone-700 hover:bg-brand-50 hover:text-brand-800 rounded-lg transition-colors flex items-center gap-2.5" > - Settings + {t("actions.settings")}
)} @@ -219,7 +222,7 @@ export default function PrivacyProxyUI({
Kiji logo

- PII detection & masking proxy + {t("tagline")}

- Local proxy + {t("badges.localProxy")} - On-device detection + {t("badges.onDeviceDetection")}
@@ -256,7 +259,7 @@ export default function PrivacyProxyUI({

- Model is unhealthy + {t("modelHealth.unhealthy")}

{serverHealth.modelError && (

@@ -264,17 +267,20 @@ export default function PrivacyProxyUI({

)}

- Check the model configuration in{" "} - {isElectron ? ( - - ) : ( - "Settings" - )} + onRequestSettings?.()} + className="underline font-semibold" + /> + ) : ( + + ), + }} + />

)} @@ -284,14 +290,15 @@ export default function PrivacyProxyUI({

- {PROVIDER_NAMES[settings.activeProvider]} API key not - configured.{" "} + {t("apiKey.notConfigured", { + provider: PROVIDER_NAMES[settings.activeProvider], + })}{" "}

@@ -302,9 +309,13 @@ export default function PrivacyProxyUI({ {/* Input Section */}
- Your request + + {t("playground.yourRequest")} +
- +