From 6bcc7948f32d909fbaec251e2199a0e5ba1405b7 Mon Sep 17 00:00:00 2001 From: Aqil Aziz Date: Fri, 22 May 2026 20:30:16 +0700 Subject: [PATCH] ci(i18n): add zh-CN desktop bundle guard (#2403) Co-authored-by: Aqil Aziz --- app/src/components/LanguageSelect.tsx | 2 +- package.json | 1 + scripts/verify-i18n-bundle.mjs | 85 +++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 scripts/verify-i18n-bundle.mjs diff --git a/app/src/components/LanguageSelect.tsx b/app/src/components/LanguageSelect.tsx index ebd6e5405..a6e68579a 100644 --- a/app/src/components/LanguageSelect.tsx +++ b/app/src/components/LanguageSelect.tsx @@ -8,7 +8,7 @@ import { setLocale } from '../store/localeSlice'; const LOCALE_OPTIONS: Array<{ value: Locale; flag: string; label: string }> = [ { value: 'en', flag: '🇬🇧', label: 'English' }, { value: 'ko', flag: '🇰🇷', label: '한국어' }, - { value: 'zh-CN', flag: '🇨🇳', label: '中文' }, + { value: 'zh-CN', flag: '🇨🇳', label: '简体中文' }, { value: 'hi', flag: '🇮🇳', label: 'हिन्दी' }, { value: 'es', flag: '🇪🇸', label: 'Español' }, { value: 'ar', flag: '🇸🇦', label: 'العربية' }, diff --git a/package.json b/package.json index 3f170e3a3..dc67d0cef 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "rust:check": "pnpm --filter openhuman-app rust:check", "typecheck": "pnpm --filter openhuman-app compile", "i18n:check": "tsx scripts/i18n-coverage.ts", + "i18n:bundle:check": "node scripts/verify-i18n-bundle.mjs", "i18n:dump": "tsx scripts/i18n-coverage.ts --no-unused --out tmp/i18n-coverage" }, "devDependencies": { diff --git a/scripts/verify-i18n-bundle.mjs b/scripts/verify-i18n-bundle.mjs new file mode 100644 index 000000000..2e02d0e34 --- /dev/null +++ b/scripts/verify-i18n-bundle.mjs @@ -0,0 +1,85 @@ +#!/usr/bin/env node +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +let distDir = resolve(root, "app/dist"); + +for (let i = 2; i < process.argv.length; i += 1) { + const arg = process.argv[i]; + if (arg === "--dist") { + const value = process.argv[i + 1]; + if (!value) { + console.error("verify-i18n-bundle: --dist requires a path"); + process.exit(2); + } + distDir = resolve(process.cwd(), value); + i += 1; + } else if (arg === "--help" || arg === "-h") { + console.log("Usage: node scripts/verify-i18n-bundle.mjs [--dist app/dist]"); + process.exit(0); + } else { + console.error(`verify-i18n-bundle: unknown argument: ${arg}`); + process.exit(2); + } +} + +function listJsFiles(dir) { + const out = []; + for (const entry of readdirSync(dir)) { + const path = join(dir, entry); + const stat = statSync(path); + if (stat.isDirectory()) { + out.push(...listJsFiles(path)); + } else if (entry.endsWith(".js")) { + out.push(path); + } + } + return out; +} + +const requiredMarkers = [ + { + label: "zh-CN locale key", + needles: ["zh-CN"], + }, + { + label: "Simplified Chinese picker label", + needles: ["\u7b80\u4f53\u4e2d\u6587", "\\u7b80\\u4f53\\u4e2d\\u6587"], + }, +]; + +if (!existsSync(distDir) || !statSync(distDir).isDirectory()) { + console.error( + `verify-i18n-bundle: dist directory does not exist or is not a directory: ${distDir}`, + ); + process.exit(1); +} + +const files = listJsFiles(distDir); +if (files.length === 0) { + console.error( + `verify-i18n-bundle: no JavaScript assets found under ${distDir}`, + ); + process.exit(1); +} + +const bundle = files.map((file) => readFileSync(file, "utf8")).join("\n"); +const missing = requiredMarkers.filter( + (marker) => !marker.needles.some((needle) => bundle.includes(needle)), +); + +if (missing.length > 0) { + console.error( + "verify-i18n-bundle: production bundle is missing i18n markers:", + ); + for (const marker of missing) { + console.error(` - ${marker.label}`); + } + process.exit(1); +} + +console.log( + `verify-i18n-bundle: found ${requiredMarkers.length} required markers in ${files.length} JS assets`, +);