diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index ad36e6494c..79e0844b03 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -33,8 +33,8 @@ en: 372297005b57a08b zh: 074a635ac17e4362 /deepseek-harness-desktop/installation-ownership: - en: b3cf09d7c901d727 - zh: 2ec34045a201d8e9 + en: ae0384d699f55cea + zh: ffb879b1d4b08bae /deepseek-harness-desktop/installation-ownership/runtime-and-plugin-activation: en: a1a47b09f4a72ba4 zh: 7b1cf27f0b72cf31 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index b48f281820..64d9383f82 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -150,7 +150,7 @@ The product UI retains Web actions, including "Open In..." through the shared au Update copy preserves the complete version, including prerelease suffixes, without adding v or V. When no update is available, the dialog title reports the result and the body shows the current version. -Electron chooses typed English, Chinese, or Japanese shell copy from its application locale and falls back to English. The macOS application bundle declares English, Simplified Chinese, and Japanese in `CFBundleLocalizations`, allowing macOS to match the initial application language to the user’s preferred languages. Saved Client UI language preferences still take precedence for the main interface. On Windows, the main document's language updates desktop menus, recovery and update prompts. The repository Client UI i18n gate checks desktop sources. +Electron chooses typed English, Chinese, or Japanese shell copy from its application locale and falls back to English. The macOS application bundle declares English, Simplified Chinese, and Japanese in `CFBundleLocalizations`, allowing macOS to match the initial application language to the user’s preferred languages. Saved Client UI language preferences still take precedence for the main interface. Windows NSIS installer resources live in one JSON file per language under `installer/locales/`; packaging generates NSIS strings and discovers the installer language list from these files. English, Simplified Chinese, and Japanese resources must have matching keys and format placeholders; `pnpm run check:i18n` validates them. The native installer test builds and exercises each configured language. Platform account pages and onboarding artwork use English for languages other than Chinese. On Windows, the main document's language updates desktop menus, recovery and update prompts. The repository Client UI i18n gate checks desktop sources. Windows and macOS relay native fullscreen transitions and the state after each page load to the renderer, so overlays can adapt their caption clearance. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 2cd7db2ae2..c01ee79e40 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -150,7 +150,7 @@ Electron 拥有 `$DSH_HOME/profiles/desktop`。其 `dependencies` 包含 pnpm 升级文案完整展示版本号(包括预发布后缀),不额外添加 v 或 V。无可用更新时,弹窗标题显示检查结果,正文显示当前版本。 -Electron 根据应用语言选择类型化的英文、中文或日文 shell 文案,并回退到英文。macOS 应用包通过 `CFBundleLocalizations` 声明支持英语、简体中文和日语,让 macOS 根据用户的首选语言匹配初始应用语言。主界面仍优先使用已保存的 Client UI 语言偏好。在 Windows 上,主文档的语言会更新桌面菜单、恢复与更新提示。仓库 Client UI i18n 检查覆盖桌面端源码。 +Electron 根据应用语言选择类型化的英文、中文或日文 shell 文案,并回退到英文。macOS 应用包通过 `CFBundleLocalizations` 声明支持英语、简体中文和日语,让 macOS 根据用户的首选语言匹配初始应用语言。主界面仍优先使用已保存的 Client UI 语言偏好。 Windows NSIS 安装器在 `installer/locales/` 下按语言保存独立 JSON 资源;打包从这些文件生成 NSIS 文案和安装器语言清单。英文、简体中文和日语资源的键与格式占位符必须一致,`pnpm run check:i18n` 负责检查。原生安装器测试逐个构建并验证配置中的语言。账户平台页面和引导插图对中文以外的语言使用英文。在 Windows 上,主文档的语言会更新桌面菜单、恢复与更新提示。仓库 Client UI i18n 检查覆盖桌面端源码。 Windows 和 macOS 会在原生全屏切换及每次页面加载后向渲染器同步全屏状态,让浮层调整顶栏避让距离。 diff --git a/apps/desktop/electron-builder.config.d.mts b/apps/desktop/electron-builder.config.d.mts index 45e90a4ae4..2973a6eacc 100644 --- a/apps/desktop/electron-builder.config.d.mts +++ b/apps/desktop/electron-builder.config.d.mts @@ -53,7 +53,7 @@ export interface DesktopElectronBuilderConfig { readonly perMachine: false readonly allowElevation: false readonly allowToChangeInstallationDirectory: false - readonly installerLanguages: readonly ['en_US', 'zh_CN'] + readonly installerLanguages: readonly string[] } readonly beforeBuild: () => Promise readonly beforePack: (context: BeforePackContext) => Promise diff --git a/apps/desktop/installer/locales/en_US.json b/apps/desktop/installer/locales/en_US.json new file mode 100644 index 0000000000..f522ad9b6d --- /dev/null +++ b/apps/desktop/installer/locales/en_US.json @@ -0,0 +1,40 @@ +{ + "nsisLanguage": "ENGLISH", + "strings": { + "INSTALLER_INSTALL": "Install now", + "INSTALLER_FINISH": "Finish", + "INSTALLER_NATIVE_PROGRESS": "Preparing installation… %d%%", + "INSTALLER_PROGRESS_EXTRACT": "Extracting files… %d%%", + "INSTALLER_PROGRESS_COPY": "Installing files… %d%%", + "INSTALLER_PROGRESS_REGISTER": "Finishing installation… %d%%", + "INSTALLER_PROGRESS_CLEAN": "Cleaning temporary files… %d%%", + "INSTALLER_CHOOSE_PATH": "Choose location", + "INSTALLER_BROWSE": "Browse", + "INSTALLER_LAUNCH": "Launch now", + "INSTALLER_LAUNCH_FAILED": "DeepSeek Harness could not be launched. Retry, or clear Launch now and close the installer.", + "INSTALLER_UI_ERROR": "The installer could not initialize its window. Close it and try again.", + "INSTALLER_THEME_ERROR": "Use /THEME=auto, /THEME=light or /THEME=dark.", + "INSTALLER_FAILED": "Installation did not finish. Close this installer and retry. If the problem persists, download the installer again.", + "INSTALLER_RUNNING": "DeepSeek Harness is running. Quit it from the system tray and try again.", + "INSTALLER_DIRECTORY_FAILED": "The installation directory could not be replaced.", + "INSTALLER_DIRECTORY_HINT": "Check access to the installation directory and close programs using its files, then run the installer again. Copy the error details if the problem persists.", + "INSTALLER_DIRECTORY_RESTORE_HINT": "The previous installation could not be restored. Keep the backup directory shown in the error details and contact support before installing again.", + "INSTALLER_PATH_INVALID": "Choose a full local folder path. Drive roots, system folders, links and special characters are not supported.", + "INSTALLER_PATH_OWNERSHIP": "Choose an empty folder or the registered DeepSeek Harness installation folder.", + "INSTALLER_PATH_WRITABLE": "This location is not writable. Choose a folder available to the current user.", + "INSTALLER_DISK_SPACE": "There is not enough free disk space. Choose another location.", + "INSTALLER_PER_USER": "This installer supports the current user only. Uninstall the existing all-users installation first.", + "INSTALLER_EXTRACT_FAILED": "The application files could not be extracted.", + "INSTALLER_EXTRACT_HINT": "Security software may have blocked a file, or the disk may be full. Close this installer and try again. If it keeps failing, send us the error details below.", + "INSTALLER_EXTRACT_COPY": "Copy error details", + "INSTALLER_EXTRACT_EXPAND": "Show details", + "INSTALLER_EXTRACT_COLLAPSE": "Hide details", + "INSTALLER_EXTRACT_SAVED": "The full report was saved to %s", + "INSTALLER_EXTRACT_UNSAVED": "The full report could not be saved. Use Copy error details to keep it.", + "INSTALLER_EXTRACT_COPIED": "Error details were copied to the clipboard." + }, + "uninstaller": { + "welcome": "Welcome", + "finished": "has been uninstalled" + } +} diff --git a/apps/desktop/installer/locales/ja_JP.json b/apps/desktop/installer/locales/ja_JP.json new file mode 100644 index 0000000000..ee614789c5 --- /dev/null +++ b/apps/desktop/installer/locales/ja_JP.json @@ -0,0 +1,40 @@ +{ + "nsisLanguage": "JAPANESE", + "strings": { + "INSTALLER_INSTALL": "今すぐインストール", + "INSTALLER_FINISH": "完了", + "INSTALLER_NATIVE_PROGRESS": "インストールを準備しています… %d%%", + "INSTALLER_PROGRESS_EXTRACT": "ファイルを展開しています… %d%%", + "INSTALLER_PROGRESS_COPY": "ファイルをインストールしています… %d%%", + "INSTALLER_PROGRESS_REGISTER": "インストールを完了しています… %d%%", + "INSTALLER_PROGRESS_CLEAN": "一時ファイルを削除しています… %d%%", + "INSTALLER_CHOOSE_PATH": "インストール先を選択", + "INSTALLER_BROWSE": "参照", + "INSTALLER_LAUNCH": "今すぐ起動", + "INSTALLER_LAUNCH_FAILED": "DeepSeek Harness を起動できませんでした。再試行するか、「今すぐ起動」のチェックを外してインストーラーを閉じてください。", + "INSTALLER_UI_ERROR": "インストーラーのウィンドウを初期化できませんでした。閉じてから再試行してください。", + "INSTALLER_THEME_ERROR": "/THEME=auto、/THEME=light、/THEME=dark のいずれかを指定してください。", + "INSTALLER_FAILED": "インストールが完了しませんでした。インストーラーを閉じて再試行してください。問題が解決しない場合は、インストーラーを再度ダウンロードしてください。", + "INSTALLER_RUNNING": "DeepSeek Harness が実行中です。システムトレイから終了して再試行してください。", + "INSTALLER_DIRECTORY_FAILED": "インストール先のディレクトリを置き換えられませんでした。", + "INSTALLER_DIRECTORY_HINT": "インストール先へのアクセス権限を確認し、そのファイルを使用しているプログラムを閉じてから、インストーラーを再実行してください。問題が続く場合はエラーの詳細をコピーしてください。", + "INSTALLER_DIRECTORY_RESTORE_HINT": "以前のインストールを復元できませんでした。エラーの詳細に表示されたバックアップディレクトリを保持し、再インストール前にサポートへお問い合わせください。", + "INSTALLER_PATH_INVALID": "ローカルフォルダーの完全なパスを選択してください。ドライブのルート、システムフォルダー、リンク、特殊文字を含むパスは使用できません。", + "INSTALLER_PATH_OWNERSHIP": "空のフォルダー、または登録済みの DeepSeek Harness インストール先を選択してください。", + "INSTALLER_PATH_WRITABLE": "この場所には書き込めません。現在のユーザーが書き込めるフォルダーを選択してください。インストーラーは管理者権限を要求しません。", + "INSTALLER_DISK_SPACE": "ディスクの空き容量が不足しています。別の場所を選択してください。", + "INSTALLER_PER_USER": "このインストーラーは現在のユーザーのみを対象としています。先にすべてのユーザー向けの既存のインストールを削除してください。", + "INSTALLER_EXTRACT_FAILED": "アプリケーションのファイルを展開できませんでした。", + "INSTALLER_EXTRACT_HINT": "セキュリティソフトがファイルをブロックしたか、ディスクの空き容量が不足している可能性があります。インストーラーを閉じて再試行してください。問題が続く場合は、以下のエラーの詳細をお送りください。", + "INSTALLER_EXTRACT_COPY": "エラーの詳細をコピー", + "INSTALLER_EXTRACT_EXPAND": "詳細を表示", + "INSTALLER_EXTRACT_COLLAPSE": "詳細を非表示", + "INSTALLER_EXTRACT_SAVED": "完全なレポートを %s に保存しました", + "INSTALLER_EXTRACT_UNSAVED": "完全なレポートを保存できませんでした。「エラーの詳細をコピー」で保存してください。", + "INSTALLER_EXTRACT_COPIED": "エラーの詳細をクリップボードにコピーしました。" + }, + "uninstaller": { + "welcome": "アンインストール ウィザードへようこそ", + "finished": "アンインストールされました" + } +} diff --git a/apps/desktop/installer/locales/zh_CN.json b/apps/desktop/installer/locales/zh_CN.json new file mode 100644 index 0000000000..e3f40f13be --- /dev/null +++ b/apps/desktop/installer/locales/zh_CN.json @@ -0,0 +1,40 @@ +{ + "nsisLanguage": "SIMPCHINESE", + "strings": { + "INSTALLER_INSTALL": "立即安装", + "INSTALLER_FINISH": "完成", + "INSTALLER_NATIVE_PROGRESS": "正在准备安装… %d%%", + "INSTALLER_PROGRESS_EXTRACT": "正在解压文件… %d%%", + "INSTALLER_PROGRESS_COPY": "正在安装文件… %d%%", + "INSTALLER_PROGRESS_REGISTER": "正在完成安装… %d%%", + "INSTALLER_PROGRESS_CLEAN": "正在清理临时文件… %d%%", + "INSTALLER_CHOOSE_PATH": "选择安装位置", + "INSTALLER_BROWSE": "选择", + "INSTALLER_LAUNCH": "立即启动", + "INSTALLER_LAUNCH_FAILED": "无法启动 DeepSeek Harness。请重试,或取消勾选立即启动后关闭安装程序。", + "INSTALLER_UI_ERROR": "无法初始化安装窗口,请关闭后重试。", + "INSTALLER_THEME_ERROR": "主题参数须为 /THEME=auto、/THEME=light 或 /THEME=dark。", + "INSTALLER_FAILED": "安装未完成,请关闭安装程序后重试。如仍然失败,请重新下载安装包。", + "INSTALLER_RUNNING": "DeepSeek Harness 正在运行,请先在系统托盘中退出应用后重试。", + "INSTALLER_DIRECTORY_FAILED": "无法替换安装目录。", + "INSTALLER_DIRECTORY_HINT": "请检查安装目录的访问权限,关闭占用其中内容的程序后重新运行安装器。如仍失败,请复制错误详情。", + "INSTALLER_DIRECTORY_RESTORE_HINT": "未能恢复原安装目录。请保留错误详情中列出的备份目录,联系支持人员后再安装。", + "INSTALLER_PATH_INVALID": "请选择本地磁盘上的完整文件夹路径,不能使用磁盘根目录、系统目录、链接目录或包含特殊字符的路径。", + "INSTALLER_PATH_OWNERSHIP": "请选择空文件夹,或 DeepSeek Harness 原来的安装目录。", + "INSTALLER_PATH_WRITABLE": "无法写入此位置。请选择当前用户可写入的文件夹;安装程序不会申请管理员权限。", + "INSTALLER_DISK_SPACE": "此磁盘的可用空间不足,请选择其他位置。", + "INSTALLER_PER_USER": "此安装程序仅支持当前用户。请先卸载已有的所有用户安装版本。", + "INSTALLER_EXTRACT_FAILED": "无法解压应用程序文件。", + "INSTALLER_EXTRACT_HINT": "可能是安全软件拦截了文件,或磁盘空间不足。请关闭安装程序后重试;如仍然失败,请将下方错误信息发送给我们。", + "INSTALLER_EXTRACT_COPY": "复制错误信息", + "INSTALLER_EXTRACT_EXPAND": "显示详情", + "INSTALLER_EXTRACT_COLLAPSE": "隐藏详情", + "INSTALLER_EXTRACT_SAVED": "完整错误报告已保存到 %s", + "INSTALLER_EXTRACT_UNSAVED": "无法保存完整错误报告,请使用“复制错误信息”保留。", + "INSTALLER_EXTRACT_COPIED": "错误信息已复制到剪贴板。" + }, + "uninstaller": { + "welcome": "欢迎", + "finished": "已从你" + } +} diff --git a/apps/desktop/installer/strings.nsh b/apps/desktop/installer/strings.nsh deleted file mode 100644 index a1ac41b418..0000000000 --- a/apps/desktop/installer/strings.nsh +++ /dev/null @@ -1,63 +0,0 @@ -; NSIS owns installer localization independently of the Electron application locale. -LangString INSTALLER_INSTALL ${LANG_ENGLISH} "Install now" -LangString INSTALLER_INSTALL ${LANG_SIMPCHINESE} "立即安装" -LangString INSTALLER_FINISH ${LANG_ENGLISH} "Finish" -LangString INSTALLER_FINISH ${LANG_SIMPCHINESE} "完成" -LangString INSTALLER_NATIVE_PROGRESS ${LANG_ENGLISH} "Preparing installation… %d%%" -LangString INSTALLER_NATIVE_PROGRESS ${LANG_SIMPCHINESE} "正在准备安装… %d%%" -LangString INSTALLER_PROGRESS_EXTRACT ${LANG_ENGLISH} "Extracting files… %d%%" -LangString INSTALLER_PROGRESS_EXTRACT ${LANG_SIMPCHINESE} "正在解压文件… %d%%" -LangString INSTALLER_PROGRESS_COPY ${LANG_ENGLISH} "Installing files… %d%%" -LangString INSTALLER_PROGRESS_COPY ${LANG_SIMPCHINESE} "正在安装文件… %d%%" -LangString INSTALLER_PROGRESS_REGISTER ${LANG_ENGLISH} "Finishing installation… %d%%" -LangString INSTALLER_PROGRESS_REGISTER ${LANG_SIMPCHINESE} "正在完成安装… %d%%" -LangString INSTALLER_PROGRESS_CLEAN ${LANG_ENGLISH} "Cleaning temporary files… %d%%" -LangString INSTALLER_PROGRESS_CLEAN ${LANG_SIMPCHINESE} "正在清理临时文件… %d%%" -LangString INSTALLER_CHOOSE_PATH ${LANG_ENGLISH} "Choose location" -LangString INSTALLER_CHOOSE_PATH ${LANG_SIMPCHINESE} "选择安装位置" -LangString INSTALLER_BROWSE ${LANG_ENGLISH} "Browse" -LangString INSTALLER_BROWSE ${LANG_SIMPCHINESE} "选择" -LangString INSTALLER_LAUNCH ${LANG_ENGLISH} "Launch now" -LangString INSTALLER_LAUNCH ${LANG_SIMPCHINESE} "立即启动" -LangString INSTALLER_LAUNCH_FAILED ${LANG_ENGLISH} "DeepSeek Harness could not be launched. Retry, or clear Launch now and close the installer." -LangString INSTALLER_LAUNCH_FAILED ${LANG_SIMPCHINESE} "无法启动 DeepSeek Harness。请重试,或取消勾选立即启动后关闭安装程序。" -LangString INSTALLER_UI_ERROR ${LANG_ENGLISH} "The installer could not initialize its window. Close it and try again." -LangString INSTALLER_UI_ERROR ${LANG_SIMPCHINESE} "无法初始化安装窗口,请关闭后重试。" -LangString INSTALLER_THEME_ERROR ${LANG_ENGLISH} "Use /THEME=auto, /THEME=light or /THEME=dark." -LangString INSTALLER_THEME_ERROR ${LANG_SIMPCHINESE} "主题参数须为 /THEME=auto、/THEME=light 或 /THEME=dark。" -LangString INSTALLER_FAILED ${LANG_ENGLISH} "Installation did not finish. Close this installer and retry. If the problem persists, download the installer again." -LangString INSTALLER_FAILED ${LANG_SIMPCHINESE} "安装未完成,请关闭安装程序后重试。如仍然失败,请重新下载安装包。" -LangString INSTALLER_RUNNING ${LANG_ENGLISH} "DeepSeek Harness is running. Quit it from the system tray and try again." -LangString INSTALLER_RUNNING ${LANG_SIMPCHINESE} "DeepSeek Harness 正在运行,请先在系统托盘中退出应用后重试。" -LangString INSTALLER_DIRECTORY_FAILED ${LANG_ENGLISH} "The installation directory could not be replaced." -LangString INSTALLER_DIRECTORY_FAILED ${LANG_SIMPCHINESE} "无法替换安装目录。" -LangString INSTALLER_DIRECTORY_HINT ${LANG_ENGLISH} "Check access to the installation directory and close programs using its files, then run the installer again. Copy the error details if the problem persists." -LangString INSTALLER_DIRECTORY_HINT ${LANG_SIMPCHINESE} "请检查安装目录的访问权限,关闭占用其中内容的程序后重新运行安装器。如仍失败,请复制错误详情。" -LangString INSTALLER_DIRECTORY_RESTORE_HINT ${LANG_ENGLISH} "The previous installation could not be restored. Keep the backup directory shown in the error details and contact support before installing again." -LangString INSTALLER_DIRECTORY_RESTORE_HINT ${LANG_SIMPCHINESE} "未能恢复原安装目录。请保留错误详情中列出的备份目录,联系支持人员后再安装。" -LangString INSTALLER_PATH_INVALID ${LANG_ENGLISH} "Choose a full local folder path. Drive roots, system folders, links and special characters are not supported." -LangString INSTALLER_PATH_INVALID ${LANG_SIMPCHINESE} "请选择本地磁盘上的完整文件夹路径,不能使用磁盘根目录、系统目录、链接目录或包含特殊字符的路径。" -LangString INSTALLER_PATH_OWNERSHIP ${LANG_ENGLISH} "Choose an empty folder or the registered DeepSeek Harness installation folder." -LangString INSTALLER_PATH_OWNERSHIP ${LANG_SIMPCHINESE} "请选择空文件夹,或 DeepSeek Harness 原来的安装目录。" -LangString INSTALLER_PATH_WRITABLE ${LANG_ENGLISH} "This location is not writable. Choose a folder available to the current user." -LangString INSTALLER_PATH_WRITABLE ${LANG_SIMPCHINESE} "无法写入此位置。请选择当前用户可写入的文件夹;安装程序不会申请管理员权限。" -LangString INSTALLER_DISK_SPACE ${LANG_ENGLISH} "There is not enough free disk space. Choose another location." -LangString INSTALLER_DISK_SPACE ${LANG_SIMPCHINESE} "此磁盘的可用空间不足,请选择其他位置。" -LangString INSTALLER_PER_USER ${LANG_ENGLISH} "This installer supports the current user only. Uninstall the existing all-users installation first." -LangString INSTALLER_PER_USER ${LANG_SIMPCHINESE} "此安装程序仅支持当前用户。请先卸载已有的所有用户安装版本。" -LangString INSTALLER_EXTRACT_FAILED ${LANG_ENGLISH} "The application files could not be extracted." -LangString INSTALLER_EXTRACT_FAILED ${LANG_SIMPCHINESE} "无法解压应用程序文件。" -LangString INSTALLER_EXTRACT_HINT ${LANG_ENGLISH} "Security software may have blocked a file, or the disk may be full. Close this installer and try again. If it keeps failing, send us the error details below." -LangString INSTALLER_EXTRACT_HINT ${LANG_SIMPCHINESE} "可能是安全软件拦截了文件,或磁盘空间不足。请关闭安装程序后重试;如仍然失败,请将下方错误信息发送给我们。" -LangString INSTALLER_EXTRACT_COPY ${LANG_ENGLISH} "Copy error details" -LangString INSTALLER_EXTRACT_COPY ${LANG_SIMPCHINESE} "复制错误信息" -LangString INSTALLER_EXTRACT_EXPAND ${LANG_ENGLISH} "Show details" -LangString INSTALLER_EXTRACT_EXPAND ${LANG_SIMPCHINESE} "显示详情" -LangString INSTALLER_EXTRACT_COLLAPSE ${LANG_ENGLISH} "Hide details" -LangString INSTALLER_EXTRACT_COLLAPSE ${LANG_SIMPCHINESE} "隐藏详情" -LangString INSTALLER_EXTRACT_SAVED ${LANG_ENGLISH} "The full report was saved to %s" -LangString INSTALLER_EXTRACT_SAVED ${LANG_SIMPCHINESE} "完整错误报告已保存到 %s" -LangString INSTALLER_EXTRACT_UNSAVED ${LANG_ENGLISH} "The full report could not be saved. Use Copy error details to keep it." -LangString INSTALLER_EXTRACT_UNSAVED ${LANG_SIMPCHINESE} "无法保存完整错误报告,请使用“复制错误信息”保留。" -LangString INSTALLER_EXTRACT_COPIED ${LANG_ENGLISH} "Error details were copied to the clipboard." -LangString INSTALLER_EXTRACT_COPIED ${LANG_SIMPCHINESE} "错误信息已复制到剪贴板。" diff --git a/apps/desktop/scripts/electron-builder-config.mjs b/apps/desktop/scripts/electron-builder-config.mjs index 7f225bebf7..4444b68ae1 100644 --- a/apps/desktop/scripts/electron-builder-config.mjs +++ b/apps/desktop/scripts/electron-builder-config.mjs @@ -1,7 +1,8 @@ +import { readInstallerLocales, renderInstallerStrings } from './installer-locales.mjs' import { officePackageDirectories } from '../../../scripts/libreoffice-packages.mjs' import { X509Certificate } from 'node:crypto' import { readFileSync } from 'node:fs' -import { readFile } from 'node:fs/promises' +import { readFile, writeFile } from 'node:fs/promises' import { join, relative, sep } from 'node:path' import { fileURLToPath } from 'node:url' import { execFile } from 'node:child_process' @@ -129,6 +130,7 @@ export function createElectronBuilderConfig( '-OutputDirectory', join(buildPaths.root, 'installer-ui')], { env: scrubWindowsSigningEnvironment(env), windowsHide: true, }) + await writeFile(join(buildPaths.root, 'installer-ui', 'strings.nsh'), renderInstallerStrings(readInstallerLocales()), 'utf8') if (windowsSigner !== undefined) { await windowsSigner({ path: join(buildPaths.root, 'installer-ui', 'window-frame.dll'), hash: 'sha256', isNest: false }) } @@ -262,7 +264,7 @@ export function createElectronBuilderConfig( perMachine: false, allowElevation: false, allowToChangeInstallationDirectory: false, - installerLanguages: ['en_US', 'zh_CN'], + installerLanguages: readInstallerLocales().map(locale => locale.id), differentialPackage: true, }, detectUpdateChannel: false, diff --git a/apps/desktop/scripts/installer-locales.d.mts b/apps/desktop/scripts/installer-locales.d.mts new file mode 100644 index 0000000000..1cb1b8ad93 --- /dev/null +++ b/apps/desktop/scripts/installer-locales.d.mts @@ -0,0 +1,25 @@ +/** One installer language's compiler identifier, UI strings and NSIS Modern UI test expectations. */ +export interface InstallerLocale { + /** electron-builder language and resource filename without its suffix. */ + id: string + /** NSIS LANG_ constant suffix. */ + nsisLanguage: string + /** Custom installer messages, with Windows printf placeholders preserved. */ + strings: Record + /** Substrings of the upstream Modern UI uninstaller's welcome and finished pages. */ + uninstaller: Record +} + +/** + * Read complete per-language JSON resources and reject inconsistent keys or placeholders. + * @param directory - Resource directory, defaulting to Desktop installer/locales. + * @returns Validated resources, ordered by filename. + */ +export function readInstallerLocales(directory?: string): InstallerLocale[] + +/** + * Generate NSIS LangString declarations from validated resources. + * @param locales - All configured languages or one isolated native-test language. + * @returns UTF-8 NSIS source with literal text escaped. + */ +export function renderInstallerStrings(locales: InstallerLocale[]): string diff --git a/apps/desktop/scripts/installer-locales.mjs b/apps/desktop/scripts/installer-locales.mjs new file mode 100644 index 0000000000..f9f9b55138 --- /dev/null +++ b/apps/desktop/scripts/installer-locales.mjs @@ -0,0 +1,75 @@ +/** Per-language JSON resources shared by NSIS packaging, native UI tests and the i18n check. */ +import { readFileSync, readdirSync } from 'node:fs' +import { fileURLToPath } from 'node:url' + +const defaultDirectory = fileURLToPath(new URL('../installer/locales/', import.meta.url)) + +/** + * Read and validate installer translations before generating compiler input. + * @param {string} directory - Directory containing one JSON file per installer language. + * @returns {{id: string, nsisLanguage: string, strings: Record, uninstaller: Record}[]} Complete language resources. + */ +export function readInstallerLocales(directory = defaultDirectory) { + const locales = readdirSync(directory).filter(name => name.endsWith('.json')).sort().map(name => { + if (!/^[a-z]{2}_[A-Z]{2}\.json$/u.test(name)) throw new Error(`${name}: invalid installer language filename`) + /** @type {unknown} */ + const resource = JSON.parse(readFileSync(`${directory}/${name}`, 'utf8')) + if (typeof resource !== 'object' || resource === null || !('nsisLanguage' in resource) + || typeof resource.nsisLanguage !== 'string' || !/^[A-Z]+$/u.test(resource.nsisLanguage)) { + throw new Error(`${name}: invalid NSIS language identifier`) + } + /** @param {unknown} value - Parsed JSON dictionary. @returns {Record} Nonempty text values. */ + const dictionary = (value) => { + if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error(`${name}: expected text dictionary`) + /** @type {Record} */ + const result = {} + for (const [key, text] of Object.entries(value)) { + if (typeof text !== 'string' || text.trim() === '') throw new Error(`${name}: ${key} must be nonempty text`) + result[key] = text + } + return result + } + const strings = dictionary('strings' in resource ? resource.strings : undefined) + if (Object.keys(strings).some(key => !/^INSTALLER_[A-Z_]+$/u.test(key))) throw new Error(`${name}: invalid installer string key`) + return { id: name.slice(0, -5), nsisLanguage: resource.nsisLanguage, strings, + uninstaller: dictionary('uninstaller' in resource ? resource.uninstaller : undefined) } + }) + for (const id of ['en_US', 'zh_CN', 'ja_JP']) { + if (!locales.some(locale => locale.id === id)) throw new Error(`installer locales: missing ${id}.json`) + } + const english = locales.find(locale => locale.id === 'en_US') + if (!english || Object.keys(english.strings).length === 0) throw new Error('installer locales: missing English strings') + if (Object.keys(english.uninstaller).sort().join(',') !== 'finished,welcome') throw new Error('installer locales: expected uninstaller welcome and finished text') + const languages = new Set() + for (const locale of locales) { + if (languages.has(locale.nsisLanguage)) throw new Error(`${locale.id}: duplicate NSIS language ${locale.nsisLanguage}`) + languages.add(locale.nsisLanguage) + for (const field of ['strings', 'uninstaller']) { + const expected = english[field] + const actual = locale[field] + if (Object.keys(expected).sort().join(',') !== Object.keys(actual).sort().join(',')) throw new Error(`${locale.id}: ${field} keys differ from English`) + for (const [key, text] of Object.entries(expected)) { + const placeholders = (/** @type {string} */ value) => (value.match(/%.?/gu) ?? []).join(',') + if (placeholders(text) !== placeholders(actual[key])) throw new Error(`${locale.id}: ${key} changes format placeholders`) + } + } + } + return locales +} + +/** + * Generate guarded NSIS LangString declarations from validated JSON resources. + * @param {ReturnType} locales - Resources for all languages or one isolated native-test build. + * @returns {string} UTF-8 NSIS source, with literal dollar signs, quotes and line breaks escaped. + */ +export function renderInstallerStrings(locales) { + const escape = (/** @type {string} */ text) => text.replace(/\$|"|\r|\n/gu, character => ({ '$': '$$', '"': '$\\"', '\r': '$\\r', '\n': '$\\n' })[character]) + return '; Generated from installer/locales/*.json.\n' + locales.map(locale => + `!ifdef LANG_${locale.nsisLanguage}\n` + Object.entries(locale.strings).map(([key, text]) => + `LangString ${key} \${LANG_${locale.nsisLanguage}} "${escape(text)}"\n`).join('') + '!endif\n').join('') +} + +if (import.meta.main) { + const locales = readInstallerLocales() + console.log(`verify-installer-locales: ${locales.length} languages have matching keys and format placeholders.`) +} diff --git a/apps/desktop/scripts/installer.nsh b/apps/desktop/scripts/installer.nsh index 5961c071ba..43c6a4c7d7 100644 --- a/apps/desktop/scripts/installer.nsh +++ b/apps/desktop/scripts/installer.nsh @@ -9,7 +9,7 @@ ManifestDPIAware true !endif !macro customHeader - !define /ifndef INSTALLER_STRINGS_FILE "${INSTALLER_SOURCE_DIR}\strings.nsh" + !define /ifndef INSTALLER_STRINGS_FILE "${INSTALLER_BUILD_DIR}\strings.nsh" !include "${INSTALLER_STRINGS_FILE}" !ifdef BUILD_UNINSTALLER BrandingText " " diff --git a/apps/desktop/scripts/test-windows-installer.mjs b/apps/desktop/scripts/test-windows-installer.mjs index 27c6381a95..1cb7a5aae6 100644 --- a/apps/desktop/scripts/test-windows-installer.mjs +++ b/apps/desktop/scripts/test-windows-installer.mjs @@ -1,4 +1,5 @@ /** Builds an isolated native payload through the production NSIS configuration and exercises its UI. */ +import { readInstallerLocales, renderInstallerStrings } from './installer-locales.mjs' import { execFile } from 'node:child_process' import { randomUUID } from 'node:crypto' import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises' @@ -107,8 +108,8 @@ SectionEnd const compiler = await getMakeNsisPath() await execute(compiler.path, ['/V2', payloadSource], { ...childOptions, env: { ...childOptions.env, ...compiler.env } }) if (sign) await sign({ path: join(payload, `${productName}.exe`), hash: 'sha256', isNest: false }) - const sourceStrings = await readFile(join(appRoot, 'installer', 'strings.nsh'), 'utf8') - for (const language of ['en_US', 'zh_CN']) { + for (const locale of readInstallerLocales()) { + const language = locale.id const config = createElectronBuilderConfig() if (sign) { config.win.forceCodeSigning = true @@ -117,9 +118,7 @@ SectionEnd const languageOutput = join(output, language) await mkdir(languageOutput) const strings = join(languageOutput, 'strings.nsh') - const languageId = language === 'en_US' ? 'ENGLISH' : 'SIMPCHINESE' - await writeFile(strings, sourceStrings.split('\n').filter((line) => - !line.startsWith('LangString ') || line.includes(`\${LANG_${languageId}}`)).join('\n')) + await writeFile(strings, renderInstallerStrings([locale]), 'utf8') const include = join(languageOutput, 'include.nsh') await writeFile(include, `!define INSTALLER_BUILD_DIR "${join(output, 'ui')}"\n!define INSTALLER_STRINGS_FILE "${strings}"\n!include "${join(appRoot, 'scripts', 'installer.nsh')}"\n`) await build({ projectDir: appRoot, prepackaged: payload, targets: Platform.WINDOWS.createTarget(['nsis'], Arch.x64), publish: 'never', @@ -132,7 +131,7 @@ SectionEnd join(appRoot, 'tests', uninstallOnly ? 'windows-uninstall-smoke.ps1' : 'windows-installer-smoke.ps1'), '-Installer', join(languageOutput, 'installer-test.exe'), '-ProductName', productName, '-RegistryKey', guid, '-OutputDirectory', languageOutput, - ...uninstallOnly ? ['-Language', languageId, '-PackageName', packageName] : []], childOptions) + '-Language', language, ...uninstallOnly ? ['-PackageName', packageName] : []], childOptions) process.stdout.write(`${language}\n${result.stdout}`) } succeeded = true diff --git a/apps/desktop/tests/installer-locales.spec.ts b/apps/desktop/tests/installer-locales.spec.ts new file mode 100644 index 0000000000..4dea51a414 --- /dev/null +++ b/apps/desktop/tests/installer-locales.spec.ts @@ -0,0 +1,56 @@ +/** Per-language installer resources feed both the packaging language list and the NSIS compiler. */ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { expect, it, onTestFinished } from 'vitest' +import { readInstallerLocales, renderInstallerStrings } from '../scripts/installer-locales.mjs' + +function fixture(): string { + const directory = mkdtempSync(join(tmpdir(), 'dsh-installer-locales-')) + onTestFinished(() => { rmSync(directory, { recursive: true, force: true }) }) + for (const id of ['en_US', 'zh_CN', 'ja_JP']) { + writeFileSync(join(directory, `${id}.json`), readFileSync(new URL(`../installer/locales/${id}.json`, import.meta.url))) + } + return directory +} + +it('generates Japanese NSIS strings from the same files that select packaged languages', () => { + const locales = readInstallerLocales() + expect(locales.map(locale => locale.id)).toEqual(['en_US', 'ja_JP', 'zh_CN']) + const source = renderInstallerStrings(locales) + expect(source).toContain('LangString INSTALLER_INSTALL ${LANG_JAPANESE} "今すぐインストール"') + expect(source).toContain('LangString INSTALLER_NATIVE_PROGRESS ${LANG_JAPANESE} "インストールを準備しています… %d%%"') + expect(source).toContain('!ifdef LANG_JAPANESE\n') + expect(source.match(/^LangString /gmu)).toHaveLength(93) + const japanese = locales.filter(locale => locale.id === 'ja_JP') + expect(renderInstallerStrings(japanese)).not.toContain('${LANG_ENGLISH}') +}) + +it.each(['en_US', 'zh_CN', 'ja_JP'])('rejects a missing %s configuration', (id) => { + const directory = fixture() + rmSync(join(directory, `${id}.json`)) + expect(() => readInstallerLocales(directory)).toThrow(`missing ${id}.json`) +}) + +it.each([ + ['missing key', (resource: ReturnType[number]) => { delete resource.strings.INSTALLER_INSTALL }, 'keys differ'], + ['extra key', (resource: ReturnType[number]) => { resource.strings.INSTALLER_EXTRA = 'extra' }, 'keys differ'], + ['empty text', (resource: ReturnType[number]) => { resource.strings.INSTALLER_INSTALL = '' }, 'nonempty text'], + ['changed placeholder', (resource: ReturnType[number]) => { resource.strings.INSTALLER_NATIVE_PROGRESS = '%s%%' }, 'format placeholders'], + ['missing percent escape', (resource: ReturnType[number]) => { resource.strings.INSTALLER_NATIVE_PROGRESS = '%d%' }, 'format placeholders'], + ['extra format specifier', (resource: ReturnType[number]) => { resource.strings.INSTALLER_NATIVE_PROGRESS = '%d%% %q' }, 'format placeholders'], + ['duplicate language', (resource: ReturnType[number]) => { resource.nsisLanguage = 'ENGLISH' }, 'duplicate NSIS language'], + ['invalid compiler token', (resource: ReturnType[number]) => { resource.nsisLanguage = 'JA\n!error' }, 'invalid NSIS language'], +])('rejects %s', (_name, mutate, diagnostic) => { + const directory = fixture() + const resource = readInstallerLocales(directory).find(locale => locale.id === 'ja_JP')! + mutate(resource) + writeFileSync(join(directory, 'ja_JP.json'), JSON.stringify(resource)) + expect(() => readInstallerLocales(directory)).toThrow(diagnostic) +}) + +it('escapes literal NSIS syntax without changing translated text', () => { + const locale = readInstallerLocales()[0]! + expect(renderInstallerStrings([{ ...locale, strings: { INSTALLER_INSTALL: '"$name"\nnext\rline' } }])) + .toContain('"$\\"$$name$\\"$\\nnext$\\rline"') +}) diff --git a/apps/desktop/tests/installer-packaging.spec.ts b/apps/desktop/tests/installer-packaging.spec.ts index d971bb262d..57da6058df 100644 --- a/apps/desktop/tests/installer-packaging.spec.ts +++ b/apps/desktop/tests/installer-packaging.spec.ts @@ -5,13 +5,17 @@ import { AppInfo, Packager } from 'app-builder-lib' import { expandMacro } from 'app-builder-lib/out/util/macroExpander.js' import { describe, expect, it, vi } from 'vitest' -const { execute } = vi.hoisted(() => ({ execute: vi.fn(async () => undefined) })) +const { execute, write } = vi.hoisted(() => ({ execute: vi.fn(async () => undefined), write: vi.fn(async () => undefined) })) vi.mock('node:child_process', async (importOriginal) => { const original = await importOriginal() const { promisify } = await import('node:util') return { ...original, execFile: Object.assign(vi.fn(), { [promisify.custom]: execute }) } }) +vi.mock('node:fs/promises', async importOriginal => ({ + ...await importOriginal(), writeFile: write, +})) + describe('installer preparation preserves application dependencies', () => { it.each(['win32', 'darwin'] as const)('rejects a missing production policy before signing on %s', async (platform) => { const { createElectronBuilderConfig } = await import('../scripts/electron-builder-config.mjs') @@ -22,6 +26,7 @@ describe('installer preparation preserves application dependencies', () => { }) it.each(['win32', 'darwin'] as const)('preserves installer metadata and dependency ownership on %s', async (platform) => { execute.mockClear() + write.mockClear() const env = { DSH_DESKTOP_APP_ID: 'com.example.installer', DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN: 'https://policy.example.com', @@ -49,6 +54,7 @@ describe('installer preparation preserves application dependencies', () => { expect(appInfo.copyright).toBe(`Copyright © ${new Date().getFullYear()} DeepSeek Harness`) if (platform === 'win32') { expect(expandMacro(config.nsis.uninstallDisplayName, null, appInfo, {}, false)).toBe('DeepSeek Harness') + expect(config.nsis.installerLanguages).toEqual(['en_US', 'ja_JP', 'zh_CN']) } const aboutIcon = config.extraResources.find(resource => resource.to === 'icon.png') expect(aboutIcon).toBeDefined() @@ -71,6 +77,11 @@ describe('installer preparation preserves application dependencies', () => { await packager.installAppDependencies(process.platform === 'win32' ? Platform.LINUX : Platform.WINDOWS, Arch.x64) expect(packager.areNodeModulesHandledExternally).toBe(false) expect(execute).toHaveBeenCalledTimes(platform === 'win32' ? 1 : 0) + expect(write).toHaveBeenCalledTimes(platform === 'win32' ? 1 : 0) + if (platform === 'win32') { + expect(write).toHaveBeenCalledWith(expect.stringMatching(/installer-ui[\\/]strings\.nsh$/u), + expect.stringContaining('LangString INSTALLER_INSTALL ${LANG_JAPANESE}'), 'utf8') + } } finally { vi.unstubAllEnvs() vi.restoreAllMocks() diff --git a/apps/desktop/tests/windows-installer-smoke.ps1 b/apps/desktop/tests/windows-installer-smoke.ps1 index 796f885bb4..ea917263b9 100644 --- a/apps/desktop/tests/windows-installer-smoke.ps1 +++ b/apps/desktop/tests/windows-installer-smoke.ps1 @@ -2,7 +2,7 @@ [CmdletBinding()] param([Parameter(Mandatory)][string]$Installer, [Parameter(Mandatory)][string]$ProductName, [Parameter(Mandatory)][string]$RegistryKey, - [Parameter(Mandatory)][string]$OutputDirectory) + [Parameter(Mandatory)][string]$OutputDirectory, [Parameter(Mandatory)][string]$Language) $ErrorActionPreference = 'Stop' . (Join-Path $PSScriptRoot 'windows-installer-ui.ps1') Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes @@ -14,12 +14,8 @@ $uninstaller = Join-Path $installPath ('Uninstall ' + $ProductName + '.exe') $processes = [Collections.Generic.List[Diagnostics.Process]]::new() $results = [Collections.Generic.List[string]]::new() $expected = Get-Content (Join-Path $PSScriptRoot 'expected/windows-installer.json') -Raw | ConvertFrom-Json -$localizedCopy = @{ ENGLISH = @{}; SIMPCHINESE = @{} } -Get-Content (Join-Path $PSScriptRoot '../installer/strings.nsh') -Encoding UTF8 | ForEach-Object { - if ($_ -match '^LangString (INSTALLER_\w+) \$\{LANG_(ENGLISH|SIMPCHINESE)\} "(.*)"$') { - $localizedCopy[$Matches[2]][$Matches[1]] = $Matches[3] - } -} +$resource = Get-Content (Join-Path $PSScriptRoot "../installer/locales/$Language.json") -Encoding UTF8 -Raw | ConvertFrom-Json +$localizedCopy = @{ $Language = $resource.strings } function Wait-Control([Diagnostics.Process]$Process, [string]$Text, [switch]$Dialog) { $timer = [Diagnostics.Stopwatch]::StartNew() do { diff --git a/apps/desktop/tests/windows-uninstall-smoke.ps1 b/apps/desktop/tests/windows-uninstall-smoke.ps1 index 556dace4da..dad594f7c3 100644 --- a/apps/desktop/tests/windows-uninstall-smoke.ps1 +++ b/apps/desktop/tests/windows-uninstall-smoke.ps1 @@ -4,6 +4,7 @@ param([Parameter(Mandatory)][string]$Installer, [Parameter(Mandatory)][string]$P [Parameter(Mandatory)][string]$RegistryKey, [Parameter(Mandatory)][string]$OutputDirectory, [Parameter(Mandatory)][string]$Language, [Parameter(Mandatory)][string]$PackageName) $ErrorActionPreference = 'Stop' +$copy = (Get-Content (Join-Path $PSScriptRoot "../installer/locales/$Language.json") -Encoding UTF8 -Raw | ConvertFrom-Json).uninstaller $OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory) . (Join-Path $PSScriptRoot 'windows-installer-ui.ps1') [InstallerCapture]::Initialize() @@ -69,12 +70,12 @@ try { if ($mode -eq 'home-inside') { $env:DSH_HOME = Join-Path $userDataPath 'home' } else { Remove-Item Env:DSH_HOME -ErrorAction SilentlyContinue } $process = Start-Process -FilePath $uninstaller -ArgumentList $arguments -PassThru -WindowStyle ($(if ($mode -eq 'interactive') { 'Normal' } else { 'Hidden' })) if ($mode -eq 'interactive') { - $welcomeText = if ($Language -eq 'ENGLISH') { 'Welcome' } else { -join [char[]]@(0x6B22, 0x8FCE) } + $welcomeText = $copy.welcome $welcome = Wait-Text $welcomeText $window = [InstallerCapture]::TopLevel($welcome) [void][InstallerCapture]::SaveNative($window, (Join-Path $OutputDirectory 'uninstall-welcome.png'), $false) [InstallerCapture]::Click([InstallerCapture]::GetDlgItem($window, 1)) - $finishedText = if ($Language -eq 'ENGLISH') { 'has been uninstalled' } else { -join [char[]]@(0x5DF2, 0x4ECE, 0x4F60) } + $finishedText = $copy.finished [void](Wait-Text $finishedText) [InstallerCapture]::Click([InstallerCapture]::GetDlgItem($window, 1)) } diff --git a/package.json b/package.json index b2dc54bd77..cf59063403 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,7 @@ "verify-npm-install-layout": "tsx scripts/verify-npm-install-layout.ts", "verify-client-packages": "tsx scripts/verify-client-packages.ts", "check:i18n": "tsx scripts/run-gates.ts i18n", + "verify-installer-locales": "node apps/desktop/scripts/installer-locales.mjs", "verify-locale-metadata": "tsx scripts/verify-locale-metadata.ts", "verify-client-ui-i18n": "tsx scripts/verify-client-ui-i18n.ts", "verify-client-route-resolution": "tsx scripts/verify-client-route-resolution.ts", diff --git a/scripts/run-gates.spec.ts b/scripts/run-gates.spec.ts index 40ee62f364..e13778cdc1 100644 --- a/scripts/run-gates.spec.ts +++ b/scripts/run-gates.spec.ts @@ -370,7 +370,7 @@ describe('gate graph validation', () => { expect(ids).toEqual([ 'rescope-vendor', 'publint', 'constraints', 'default-product-isolation', 'product-use', 'official-bundle-catalog', 'provider-data', 'package-dependencies', 'application-entrypoints', 'dsh-package-licenses', 'node-next-types', - 'optional-dependency-imports', 'client-packages', 'locale-metadata', 'locale-dictionaries', 'client-ui-i18n', 'client-route-resolution', 'no-bare-dispatcher', + 'optional-dependency-imports', 'client-packages', 'installer-locales', 'locale-metadata', 'locale-dictionaries', 'client-ui-i18n', 'client-route-resolution', 'no-bare-dispatcher', 'plugin-record-callers', 'no-unknown-casts', 'no-top-level-await', 'cordis-config', 'runtime-closure', ]) @@ -379,10 +379,10 @@ describe('gate graph validation', () => { it('includes metadata and dictionary completeness in local and CI aggregates', () => { for (const mode of ['i18n', 'check-all', 'ci-static', 'hygiene'] as const) { const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(gate => gate.id)) - expect(ids).toEqual(expect.arrayContaining(['locale-metadata', 'locale-dictionaries'])) + expect(ids).toEqual(expect.arrayContaining(['installer-locales', 'locale-metadata', 'locale-dictionaries'])) } expect(withPnpmEntrypoint(() => gatesForMode('i18n').map(gate => gate.id))) - .toEqual(['locale-metadata', 'locale-dictionaries']) + .toEqual(['installer-locales', 'locale-metadata', 'locale-dictionaries']) }) it('caps the local hygiene aggregate at eight workers', () => { diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 65e2d13d75..1041f2abb1 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -391,6 +391,7 @@ function ciSharedStaticGates(): Gate[] { function i18nGates(): Gate[] { return [ + pnpmScript('installer-locales', 'verify-installer-locales', { label: 'Windows installer locales' }), pnpmScript('locale-metadata', 'verify-locale-metadata', { label: 'Japanese plugin metadata' }), pnpmExec('locale-dictionaries', ['vitest', 'run', 'scripts/locale-dictionary-parity.spec.ts'], { label: 'locale dictionaries' }), ]