mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-04 05:18:13 +02:00
i18n: add language toggle, neutrality policy, and codeowner gate (#238)
PR #226 landed the i18n infrastructure and Chinese (zh-CN) translations. This follow-up adds the safeguards that make accepting community translations sustainable without exposing the project to subtle state-aligned framing in future translation PRs. Changes: frontend/src/i18n/index.tsx (renamed from .ts) - Add LOCALES registry: a single source of truth for available languages and their NATIVE display names ("English", "中文 (简体)"). Adding a new language is now a one-entry change here plus a JSON file. - Add isLocale() guard so an unknown value in localStorage falls through to navigator.language detection instead of corrupting state. - File renamed to .tsx because it contains JSX. Next.js tolerated JSX in .ts but Vite/Oxc (used by vitest) does not. frontend/src/components/SettingsPanel.tsx Add a UI language picker to the Settings header — a small <select> populated from LOCALES. Users no longer need the dev console to switch languages. Locale change remains 100% client-side (localStorage), no network call, no telemetry. CONTRIBUTING.md (new) Documents the translation-neutrality requirement that applies symmetrically to all source countries: - Translations must be technically faithful to the English source. - Substitutions aligned with state propaganda from ANY country (PRC, Russia, US, EU, etc.) will be rejected. - The test is: "would a translator working strictly from the English source produce this rendering?" Also explains how translation PRs are reviewed and how to add a new language. .github/CODEOWNERS (new) Auto-requests maintainer review on: - /frontend/src/i18n/ (translation safety) - /backend/auth.py, /backend/routers/wormhole.py, /backend/services/mesh/, /backend/services/fetchers/ (the same paths recent security audits flagged as sensitive) - /.github/workflows/, /.gitlab-ci.yml, /docker-compose*.yml, /helm/ (build/deploy) - /CONTRIBUTING.md, /.github/CODEOWNERS (policy itself) frontend/src/__tests__/i18n/i18nProvider.test.tsx (new, 8 tests) Locks in the i18n contract: - LOCALES has both en and zh-CN with non-empty native labels - Default English when navigator is English - Auto-detect zh-CN when navigator language starts with "zh" - localStorage preference overrides auto-detect - setLocale persists to localStorage - Unknown stored locale falls back to auto-detect - Renders a real zh-CN translation (catches large-scale translation removal in future PRs) - Missing key falls back to the key itself Note: i18n/index.tsx, the language toggle UI, the translation policy, and the test suite together form a defense-in-depth setup. The structural safety guarantee (no network calls, static JSON bundled at build) is intact; this PR makes the social contract around translations explicit and enforceable via branch protection on CODEOWNERS-marked paths. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -95,7 +95,7 @@ import {
|
||||
setPrivacyStrictPreference,
|
||||
setSessionModePreference,
|
||||
} from '@/lib/privacyBrowserStorage';
|
||||
import { useTranslation } from '@/i18n';
|
||||
import { useTranslation, LOCALES, type Locale } from '@/i18n';
|
||||
|
||||
interface ApiEntry {
|
||||
id: string;
|
||||
@@ -246,7 +246,7 @@ const SettingsPanel = React.memo(function SettingsPanel({
|
||||
// settings are authenticated through Rust-side admin-key ownership. The
|
||||
// browser admin-session flow is unnecessary and unavailable in packaged mode.
|
||||
const nativeProtected = isNativeProtectedSettingsReady();
|
||||
const { t } = useTranslation();
|
||||
const { t, locale, setLocale } = useTranslation();
|
||||
|
||||
// --- Admin Key (for protected endpoints) ---
|
||||
const [adminKey, setAdminKey] = useState('');
|
||||
@@ -1136,12 +1136,40 @@ const SettingsPanel = React.memo(function SettingsPanel({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-8 h-8 border border-[var(--border-primary)] hover:border-red-500/50 flex items-center justify-center text-[var(--text-muted)] hover:text-red-400 transition-all hover:bg-red-950/20"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
{/*
|
||||
UI language toggle. Locale change is purely client-side
|
||||
(persists to localStorage('sb_locale')) — no network call,
|
||||
no telemetry. See frontend/src/i18n/index.ts for the list
|
||||
of available locales and CONTRIBUTING.md for the
|
||||
translation-neutrality policy.
|
||||
*/}
|
||||
<label
|
||||
htmlFor="sb-locale-select"
|
||||
className="text-[11px] tracking-[0.18em] uppercase text-[var(--text-muted)] font-mono"
|
||||
>
|
||||
LANG
|
||||
</label>
|
||||
<select
|
||||
id="sb-locale-select"
|
||||
value={locale}
|
||||
onChange={(e) => setLocale(e.target.value as Locale)}
|
||||
aria-label="UI language"
|
||||
className="h-8 px-2 border border-[var(--border-primary)] bg-[var(--bg-primary)]/60 text-[12px] font-mono text-[var(--text-secondary)] tracking-wider hover:border-cyan-500/50 focus:outline-none focus:border-cyan-500/80 transition-colors"
|
||||
>
|
||||
{LOCALES.map((entry) => (
|
||||
<option key={entry.code} value={entry.code}>
|
||||
{entry.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-8 h-8 border border-[var(--border-primary)] hover:border-red-500/50 flex items-center justify-center text-[var(--text-muted)] hover:text-red-400 transition-all hover:bg-red-950/20"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Operator Tools */}
|
||||
|
||||
Reference in New Issue
Block a user