mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-11 01:01:13 +00:00
feat(ui): add control ui locale sync pipeline
This commit is contained in:
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -743,6 +743,11 @@ jobs:
|
||||
continue-on-error: true
|
||||
run: pnpm lint:ui:no-raw-window-open
|
||||
|
||||
- name: Check control UI locale sync
|
||||
id: control_ui_i18n
|
||||
continue-on-error: true
|
||||
run: pnpm ui:i18n:check
|
||||
|
||||
- name: Run gateway watch regression harness
|
||||
id: gateway_watch_regression
|
||||
continue-on-error: true
|
||||
@@ -775,6 +780,7 @@ jobs:
|
||||
EXTENSION_PLUGIN_SDK_INTERNAL_BOUNDARY_OUTCOME: ${{ steps.extension_plugin_sdk_internal_boundary.outcome }}
|
||||
EXTENSION_RELATIVE_OUTSIDE_PACKAGE_BOUNDARY_OUTCOME: ${{ steps.extension_relative_outside_package_boundary.outcome }}
|
||||
NO_RAW_WINDOW_OPEN_OUTCOME: ${{ steps.no_raw_window_open.outcome }}
|
||||
CONTROL_UI_I18N_OUTCOME: ${{ steps.control_ui_i18n.outcome }}
|
||||
GATEWAY_WATCH_REGRESSION_OUTCOME: ${{ steps.gateway_watch_regression.outcome }}
|
||||
run: |
|
||||
failures=0
|
||||
@@ -795,6 +801,7 @@ jobs:
|
||||
"extension-plugin-sdk-internal-boundary|$EXTENSION_PLUGIN_SDK_INTERNAL_BOUNDARY_OUTCOME" \
|
||||
"extension-relative-outside-package-boundary|$EXTENSION_RELATIVE_OUTSIDE_PACKAGE_BOUNDARY_OUTCOME" \
|
||||
"lint:ui:no-raw-window-open|$NO_RAW_WINDOW_OPEN_OUTCOME" \
|
||||
"ui:i18n:check|$CONTROL_UI_I18N_OUTCOME" \
|
||||
"gateway-watch-regression|$GATEWAY_WATCH_REGRESSION_OUTCOME"; do
|
||||
name="${result%%|*}"
|
||||
outcome="${result#*|}"
|
||||
|
||||
66
.github/workflows/control-ui-locale-refresh.yml
vendored
Normal file
66
.github/workflows/control-ui-locale-refresh.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Control UI Locale Refresh
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
schedule:
|
||||
- cron: "23 4 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: control-ui-locale-refresh
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
refresh:
|
||||
if: github.repository == 'openclaw/openclaw'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: true
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Ensure translation provider secrets exist
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENCLAW_CONTROL_UI_I18N_OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.OPENCLAW_CONTROL_UI_I18N_ANTHROPIC_API_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${OPENAI_API_KEY:-}" ] && [ -z "${ANTHROPIC_API_KEY:-}" ]; then
|
||||
echo "Missing control UI i18n provider secret."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Refresh control UI locale files
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENCLAW_CONTROL_UI_I18N_OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.OPENCLAW_CONTROL_UI_I18N_ANTHROPIC_API_KEY }}
|
||||
run: pnpm ui:i18n:sync
|
||||
|
||||
- name: Commit and push locale updates
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git diff --quiet; then
|
||||
echo "No control UI locale changes."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add ui/src/i18n package.json scripts/control-ui-i18n.ts
|
||||
git commit -m "chore(ui): refresh control UI locale files"
|
||||
git push origin HEAD:"${TARGET_BRANCH}"
|
||||
@@ -1072,7 +1072,7 @@
|
||||
"test:startup:bench:smoke": "node --import tsx scripts/bench-cli-startup.ts --preset real --case gatewayStatusJson --runs 1 --warmup 0 --output .artifacts/cli-startup-bench-smoke.json",
|
||||
"test:startup:bench:update": "node scripts/test-update-cli-startup-bench.mjs",
|
||||
"test:startup:memory": "node scripts/check-cli-startup-memory.mjs",
|
||||
"test:ui": "pnpm lint:ui:no-raw-window-open && pnpm --dir ui test",
|
||||
"test:ui": "pnpm ui:i18n:check && pnpm lint:ui:no-raw-window-open && pnpm --dir ui test",
|
||||
"test:voicecall:closedloop": "node scripts/test-voicecall-closedloop.mjs",
|
||||
"test:watch": "node scripts/run-vitest.mjs --config vitest.config.ts",
|
||||
"tool-display:check": "node --import tsx scripts/tool-display.ts --check",
|
||||
@@ -1083,6 +1083,8 @@
|
||||
"tui:dev": "OPENCLAW_PROFILE=dev node scripts/run-node.mjs --dev tui",
|
||||
"ui:build": "node scripts/ui.js build",
|
||||
"ui:dev": "node scripts/ui.js dev",
|
||||
"ui:i18n:check": "node --import tsx scripts/control-ui-i18n.ts check",
|
||||
"ui:i18n:sync": "node --import tsx scripts/control-ui-i18n.ts sync --write",
|
||||
"ui:install": "node scripts/ui.js install"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
1092
scripts/control-ui-i18n.ts
Normal file
1092
scripts/control-ui-i18n.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,16 @@ type LazyLocaleRegistration = {
|
||||
|
||||
export const DEFAULT_LOCALE: Locale = "en";
|
||||
|
||||
const LAZY_LOCALES: readonly LazyLocale[] = ["zh-CN", "zh-TW", "pt-BR", "de", "es"];
|
||||
const LAZY_LOCALES: readonly LazyLocale[] = [
|
||||
"zh-CN",
|
||||
"zh-TW",
|
||||
"pt-BR",
|
||||
"de",
|
||||
"es",
|
||||
"ja-JP",
|
||||
"ko",
|
||||
"fr",
|
||||
];
|
||||
|
||||
const LAZY_LOCALE_REGISTRY: Record<LazyLocale, LazyLocaleRegistration> = {
|
||||
"zh-CN": {
|
||||
@@ -33,6 +42,18 @@ const LAZY_LOCALE_REGISTRY: Record<LazyLocale, LazyLocaleRegistration> = {
|
||||
exportName: "es",
|
||||
loader: () => import("../locales/es.ts"),
|
||||
},
|
||||
"ja-JP": {
|
||||
exportName: "ja_JP",
|
||||
loader: () => import("../locales/ja-JP.ts"),
|
||||
},
|
||||
ko: {
|
||||
exportName: "ko",
|
||||
loader: () => import("../locales/ko.ts"),
|
||||
},
|
||||
fr: {
|
||||
exportName: "fr",
|
||||
loader: () => import("../locales/fr.ts"),
|
||||
},
|
||||
};
|
||||
|
||||
export const SUPPORTED_LOCALES: ReadonlyArray<Locale> = [DEFAULT_LOCALE, ...LAZY_LOCALES];
|
||||
@@ -58,6 +79,15 @@ export function resolveNavigatorLocale(navLang: string): Locale {
|
||||
if (navLang.startsWith("es")) {
|
||||
return "es";
|
||||
}
|
||||
if (navLang.startsWith("ja")) {
|
||||
return "ja-JP";
|
||||
}
|
||||
if (navLang.startsWith("ko")) {
|
||||
return "ko";
|
||||
}
|
||||
if (navLang.startsWith("fr")) {
|
||||
return "fr";
|
||||
}
|
||||
return DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type TranslationMap = { [key: string]: string | TranslationMap };
|
||||
|
||||
export type Locale = "en" | "zh-CN" | "zh-TW" | "pt-BR" | "de" | "es";
|
||||
export type Locale = "en" | "zh-CN" | "zh-TW" | "pt-BR" | "de" | "es" | "ja-JP" | "ko" | "fr";
|
||||
|
||||
export interface I18nConfig {
|
||||
locale: Locale;
|
||||
|
||||
@@ -399,6 +399,9 @@ export const en: TranslationMap = {
|
||||
ptBR: "Português (Brazilian Portuguese)",
|
||||
de: "Deutsch (German)",
|
||||
es: "Español (Spanish)",
|
||||
jaJP: "日本語 (Japanese)",
|
||||
ko: "한국어 (Korean)",
|
||||
fr: "Français (French)",
|
||||
},
|
||||
cron: {
|
||||
summary: {
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createStorageMock } from "../../test-helpers/storage.ts";
|
||||
import * as translate from "../lib/translate.ts";
|
||||
import { de } from "../locales/de.ts";
|
||||
import { en } from "../locales/en.ts";
|
||||
import { es } from "../locales/es.ts";
|
||||
import { fr } from "../locales/fr.ts";
|
||||
import { ja_JP } from "../locales/ja-JP.ts";
|
||||
import { ko } from "../locales/ko.ts";
|
||||
import { pt_BR } from "../locales/pt-BR.ts";
|
||||
import { zh_CN } from "../locales/zh-CN.ts";
|
||||
import { zh_TW } from "../locales/zh-TW.ts";
|
||||
|
||||
describe("i18n", () => {
|
||||
function flatten(value: Record<string, string | Record<string, unknown>>, prefix = ""): string[] {
|
||||
return Object.entries(value).flatMap(([key, nested]) => {
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
if (typeof nested === "string") {
|
||||
return [fullKey];
|
||||
}
|
||||
return flatten(nested as Record<string, string | Record<string, unknown>>, fullKey);
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.stubGlobal("localStorage", createStorageMock());
|
||||
vi.stubGlobal("navigator", { language: "en-US" } as Navigator);
|
||||
@@ -80,8 +96,31 @@ describe("i18n", () => {
|
||||
});
|
||||
|
||||
it("keeps the version label available in shipped locales", () => {
|
||||
expect((de.common as { version?: string }).version).toBeTruthy();
|
||||
expect((es.common as { version?: string }).version).toBeTruthy();
|
||||
expect((fr.common as { version?: string }).version).toBeTruthy();
|
||||
expect((ja_JP.common as { version?: string }).version).toBeTruthy();
|
||||
expect((ko.common as { version?: string }).version).toBeTruthy();
|
||||
expect((pt_BR.common as { version?: string }).version).toBeTruthy();
|
||||
expect((zh_CN.common as { version?: string }).version).toBeTruthy();
|
||||
expect((zh_TW.common as { version?: string }).version).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps shipped locales structurally aligned with English", () => {
|
||||
const englishKeys = flatten(en);
|
||||
for (const [locale, value] of Object.entries({
|
||||
de,
|
||||
es,
|
||||
fr,
|
||||
ja_JP,
|
||||
ko,
|
||||
pt_BR,
|
||||
zh_CN,
|
||||
zh_TW,
|
||||
})) {
|
||||
expect(flatten(value as Record<string, string | Record<string, unknown>>), locale).toEqual(
|
||||
englishKeys,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user