Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/i18n/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@
"default": "./src/react.tsx"
}
},
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
},
"peerDependencies": {
"react": "^19.0.0"
},
"dependencies": {
"i18next": "^24.2.2",
"i18next-browser-languagedetector": "^8.0.2",
"react-i18next": "^15.3.4"
},
"devDependencies": {
"vitest": "^4.1.6"
}
}
89 changes: 88 additions & 1 deletion packages/i18n/src/chrome-storage-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,108 @@ declare const chrome:
) => void;
};
};
i18n?: {
getUILanguage(): string;
};
}
| undefined;

function getChromeLocal(): ChromeStorageLocal | undefined {
return typeof chrome !== "undefined" ? chrome.storage?.local : undefined;
}

/** Regions whose written Chinese defaults to Traditional. */
const TRADITIONAL_CHINESE_REGIONS = new Set(["tw", "hk", "mo"]);

/**
* Language subtag → shipped resource key, for every locale we translate.
*
* Keys are bare language subtags so regional variants share one bundle:
* `fr-CA`, `fr-BE` and `fr` all resolve to `fr`. Chinese is handled separately
* in `normalizeLanguageCode`, because Simplified and Traditional differ by
* script rather than by region.
*/
const RESOURCE_BY_LANGUAGE: Record<string, string> = {
en: "en-US",
ja: "ja",
ko: "ko",
fr: "fr",
it: "it",
de: "de",
es: "es",
};

/**
* Normalize a BCP 47 language tag to one of the shipped resource keys.
*
* | detected tag | resource | rule |
* | ------------------------ | -------- | ------------------ |
* | `en`, `en-GB`, `en-CN` | `en-US` | language subtag |
* | `zh-Hans*` | `zh-CN` | script subtag wins |
* | `zh-Hant*` | `zh-TW` | script subtag wins |
* | `zh-CN`, `zh-SG`, `zh` | `zh-CN` | region / default |
* | `zh-TW`, `zh-HK`, `zh-MO`| `zh-TW` | region subtag |
* | `ja*`, `ko*`, `fr*` | `ja`, `ko`, `fr` | language subtag |
* | `it*`, `de*`, `es*` | `it`, `de`, `es` | language subtag |
* | anything else | unchanged| → `fallbackLng` |
*
* Simplified vs Traditional is decided by the script subtag (`Hans` / `Hant`)
* when present, then by the region subtag — Chrome reports `zh-CN` / `zh-TW`
* rather than script subtags in practice.
*/
export function normalizeLanguageCode(code: string): string {
const parts = code.split(/[-_]/).map((part) => part.toLowerCase());
const primary = parts[0];

// Chinese needs script-level resolution; every other locale maps by subtag.
if (primary === "zh") {
// 1. Script subtag is the most accurate signal (zh-Hans / zh-Hant).
if (parts.includes("hans")) {
return "zh-CN";
}
if (parts.includes("hant")) {
return "zh-TW";
}
// 2. Fall back to the region subtag, which is what Chrome actually reports.
// Skip parts[0] — the primary subtag `zh` is also two characters long.
const region = parts.slice(1).find((part) => part.length === 2);
if (region && TRADITIONAL_CHINESE_REGIONS.has(region)) {
return "zh-TW";
}
// 3. Bare `zh` and any other region default to Simplified.
return "zh-CN";
}

// No translation shipped: leave untouched so i18next applies `fallbackLng`.
return RESOURCE_BY_LANGUAGE[primary] ?? code;
}

/**
* i18next detector that prefers `chrome.i18n.getUILanguage()`, Chrome's
* authoritative source for the extension UI language. The popup runs on a
* `chrome-extension://` origin where `navigator.language` can disagree with it.
*/
export const chromeUiLanguageDetector = {
name: "chromeUILanguage",
lookup(): string | undefined {
if (typeof chrome !== "undefined" && chrome.i18n?.getUILanguage) {
return chrome.i18n.getUILanguage();
}
return undefined;
},
};

/** Avoid page-origin localStorage; use extension storage + navigator only. */
export function getLanguageDetectionOptions(): {
order: string[];
caches: string[];
convertDetectedLanguage: (lng: string) => string;
} {
return {
order: ["navigator"],
order: ["chromeUILanguage", "navigator"],
caches: [],
// Normalise the detected tag before i18next resolves it against `resources`.
convertDetectedLanguage: (lng: string) => normalizeLanguageCode(lng),
};
}

Expand Down
75 changes: 69 additions & 6 deletions packages/i18n/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,92 @@ import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";

import { bindChromeStorageLanguageSync, getLanguageDetectionOptions } from "./chrome-storage-sync";
import {
bindChromeStorageLanguageSync,
chromeUiLanguageDetector,
getLanguageDetectionOptions,
} from "./chrome-storage-sync";
import deCommon from "./locales/de/common.json";
import deExtension from "./locales/de/extension.json";
import enUSCommon from "./locales/en-US/common.json";
import enUSExtension from "./locales/en-US/extension.json";
import esCommon from "./locales/es/common.json";
import esExtension from "./locales/es/extension.json";
import frCommon from "./locales/fr/common.json";
import frExtension from "./locales/fr/extension.json";
import itCommon from "./locales/it/common.json";
import itExtension from "./locales/it/extension.json";
import jaCommon from "./locales/ja/common.json";
import jaExtension from "./locales/ja/extension.json";
import koCommon from "./locales/ko/common.json";
import koExtension from "./locales/ko/extension.json";
import zhCNCommon from "./locales/zh-CN/common.json";
import zhCNExtension from "./locales/zh-CN/extension.json";
import zhTWCommon from "./locales/zh-TW/common.json";
import zhTWExtension from "./locales/zh-TW/extension.json";

const resources = {
"en-US": {
common: enUSCommon,
extension: enUSExtension,
},
"zh-CN": {
common: zhCNCommon,
extension: zhCNExtension,
},
"en-US": {
common: enUSCommon,
extension: enUSExtension,
"zh-TW": {
common: zhTWCommon,
extension: zhTWExtension,
},
ja: {
common: jaCommon,
extension: jaExtension,
},
ko: {
common: koCommon,
extension: koExtension,
},
fr: {
common: frCommon,
extension: frExtension,
},
it: {
common: itCommon,
extension: itExtension,
},
de: {
common: deCommon,
extension: deExtension,
},
es: {
common: esCommon,
extension: esExtension,
},
} as const;

const languageDetector = new LanguageDetector();
languageDetector.addDetector(chromeUiLanguageDetector);

i18n
.use(LanguageDetector)
.use(languageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: "zh-CN",
// Per-language fallback chain. English is the international default, so any
// locale we do not ship still lands on English rather than Chinese. Each
// translated language points at its own bundle; `zh` keeps zh-CN because
// normalizeLanguageCode already routes Traditional variants to zh-TW.
fallbackLng: {
en: ["en-US"],
zh: ["zh-CN"],
ja: ["ja"],
ko: ["ko"],
fr: ["fr"],
it: ["it"],
de: ["de"],
es: ["es"],
default: ["en-US"],
},
defaultNS: "common",
ns: ["common", "extension"],

Expand Down
6 changes: 6 additions & 0 deletions packages/i18n/src/locales/de/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"app": {
"name": "browser-skill",
"description": "KI-Agenten über die bsk-CLI deinen angemeldeten Browser steuern lassen"
}
}
94 changes: 94 additions & 0 deletions packages/i18n/src/locales/de/extension.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
{
"popup": {
"brandCategory": "Browser Automation",
"brandName": "BrowserSkill",
"stateLabel": {
"disconnected": "Nicht verbunden",
"connecting": "Verbinden…",
"connected": "Verbunden",
"version_skew": "Verbunden",
"disabled": "Verbindung aus"
},
"stateDetail": {
"disconnected": "Öffne BrowserSkill, um eine Verbindung herzustellen.",
"connecting": "Verbindung mit BrowserSkill wird hergestellt…",
"connected": "Bereit für Automatisierungsaufgaben.",
"version_skew": "Protokollversionen weichen ab. Bitte aktualisieren."
},
"stateBadge": {
"disconnected": "Standby",
"connecting": "Igniting",
"connected": "Ready",
"version_skew": "Aktualisierbar",
"disabled": "Off"
},
"connectionToggleTitle": "BrowserSkill-Verbindung",
"controlHintsToggleTitle": "Steuerungshinweise",
"controlHintsToggleHint": "Zeigt das Statusfeld und den orangen Schein, während der Agent eine Seite steuert.",
"controlHintsInfoLabel": "Über Steuerungshinweise",
"versionSkewWarning": "Erweiterungsprotokoll v{{extensionProtocol}}, CLI-Protokoll v{{cliProtocol}}. Die Versionen weichen ab – bitte aktualisieren.",
"upgradeAvailable": "Aktualisierbar",
"launcher": {
"title": "Schnellaktionen"
},
"back": "Zurück",
"runtimeTitle": "Laufzeit",
"daemonVersion": "daemon v{{version}}",
"extensionVersion": "Erweiterung v{{version}}",
"extensionVersionHint": "Erweiterungsversion",
"daemonVersionHint": "bsk-Version",
"protocolVersion": "Protokoll v{{version}}",
"extensionProtocol": "Erweiterungsprotokoll v{{version}}",
"daemonProtocol": "Daemon-Protokoll v{{version}}",
"instanceTitle": "Instanz-ID",
"instanceDescription": "Eindeutige Verbindungskennung dieses Browsers",
"copy": "Kopieren",
"copied": "Kopiert",
"copyInstanceId": "Instanz-ID kopieren",
"record": {
"sectionTitle": "Aktionsaufzeichnung",
"cardDesc": "Nimm deine Aktionen als Referenz für den Agenten auf",
"sectionHint": "Erzeugt eine Aufzeichnungsanweisung – sende sie an deinen Agenten, um die Aufzeichnung deiner Aktionen zu starten; der Agent kann das erzeugte trace-Bündel anschließend nutzen, um ähnliche Aufgaben zu automatisieren.",
"topicLabel": "Thema (optional)",
"topicPlaceholder": "z. B. Artikel veröffentlichen",
"startUrlLabel": "Start-URL (optional)",
"startUrlPlaceholder": "https://…",
"copyButton": "Aufzeichnungsanweisung kopieren",
"copied": "Kopiert",
"hintDisconnected": "Nach der Verbindung verfügbar",
"promptTemplate": "Verwende die `bsk`-CLI von BrowserSkill, um die Browseraktionen aufzuzeichnen, die ich gleich zeige.\n\nSchritte:\n1. Ausführen: {{command}}\n2. Sobald du den Start bestätigt hast, bediene ich das geöffnete Fenster selbst.\n3. Nachdem ich auf Fertig geklickt habe, lies das geschriebene `./trace`-Bündel (`trace.json` + `states/`) und fasse zusammen, was ich getan habe."
}
},
"controlOverlay": {
"status": "Agent steuert",
"interrupt": "Unterbrechen",
"interrupting": "Wird unterbrochen…"
},
"borrowConfirmation": {
"title": "Tab-Ausleihe erlauben?",
"titleInactive": "Agent möchte einen Tab ausleihen",
"body": "Ein KI-Agent möchte diesen Tab in seinen Arbeitsbereich verschieben. Du kannst die Ausleihe ablehnen oder erlauben.",
"targetTab": "Ziel-Tab:",
"autoDeny": "Anfrage läuft in {{count}} s ab",
"deny": "Ablehnen",
"allow": "Erlauben",
"notificationTitle": "BrowserSkill: Agent möchte einen Tab ausleihen",
"notificationBody": "Genehmige oder ablehne die Ausleihe von „{{tabTitle}}“ in deinem Browser."
},
"helpRequest": {
"title": "Agent benötigt deine Hilfe",
"compactStatus": "Der Agent wartet weiterhin darauf, dass du diesen Schritt abschließt",
"noteLabel": "Optionale Notiz an den Agenten",
"notePlaceholder": "Notiz für den Agenten hinzufügen (optional)",
"continue": "Fertig, Steuerung zurückgeben",
"cancel": "Anfrage abbrechen",
"collapse": "Einklappen",
"expand": "Ausklappen",
"dragHandle": "Zum Verschieben ziehen",
"notificationTitle": "BrowserSkill: Agent benötigt deine Hilfe"
},
"recordOverlay": {
"recording": "Deine Aktionen werden aufgezeichnet",
"finish": "Fertig"
}
}
6 changes: 6 additions & 0 deletions packages/i18n/src/locales/es/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"app": {
"name": "browser-skill",
"description": "Permite que los agentes de IA controlen tu navegador con sesión iniciada mediante la CLI bsk"
}
}
Loading