From 9cb8d458ad1445353fd5c9cabf83abd985acb5ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B3=A2=E4=BB=94=E7=B3=95?= Date: Sun, 13 Apr 2025 17:02:52 +0800 Subject: [PATCH] add logic to support multi language (#11) * add logic to support multi language * Update en.ts * Update en.ts --------- Co-authored-by: Sunny Luo --- src/bundle.ts | 42 +++++++++++--------------- src/locales/en.ts | 48 ++++++++++++++++++++++++++++++ src/locales/zh.ts | 48 ++++++++++++++++++++++++++++++ src/package.ts | 15 +++++----- src/user.ts | 5 ++-- src/utils/add-gitignore.ts | 3 +- src/utils/check-plugin.ts | 5 ++-- src/versions.ts | 61 +++++++++++++++++++++++++------------- 8 files changed, 170 insertions(+), 57 deletions(-) diff --git a/src/bundle.ts b/src/bundle.ts index 2f41187..418806c 100644 --- a/src/bundle.ts +++ b/src/bundle.ts @@ -214,17 +214,13 @@ async function runReactNativeBundleCommand({ reactNativeBundleProcess.on('close', async (exitCode) => { if (exitCode) { - reject( - new Error( - `"react-native bundle" command exited with code ${exitCode}.`, - ), - ); + reject(new Error(t('bundleCommandError', { code: exitCode }))); } else { let hermesEnabled: boolean | undefined = false; if (disableHermes) { hermesEnabled = false; - console.log('Hermes disabled'); + console.log(t('hermesDisabled')); } else if (platform === 'android') { const gradlePropeties = await new Promise<{ hermesEnabled?: boolean; @@ -283,8 +279,8 @@ async function copyHarmonyBundle(outputFolder: string) { await fs.ensureDir(outputFolder); await fs.copy(harmonyRawPath, outputFolder); } catch (error: any) { - console.error('copyHarmonyBundle 错误:', error); - throw new Error(`复制文件失败: ${error.message}`); + console.error(t('copyHarmonyBundleError', { error })); + throw new Error(t('copyFileFailed', { error: error.message })); } } @@ -355,7 +351,7 @@ async function compileHermesByteCode( ); args.push('-output-source-map'); } - console.log(`Running hermesc: ${hermesCommand} ${args.join(' ')}`); + console.log(t('runningHermesc', { command: hermesCommand, args: args.join(' ') })); spawnSync(hermesCommand, args, { stdio: 'ignore', }); @@ -365,7 +361,7 @@ async function compileHermesByteCode( if (!fs.existsSync(composerPath)) { return; } - console.log('Composing source map'); + console.log(t('composingSourceMap')); spawnSync( 'node', [ @@ -400,16 +396,14 @@ async function copyDebugidForSentry( }, ); } catch (error) { - console.error( - '无法找到 Sentry copy-debugid.js 脚本文件,请确保已正确安装 @sentry/react-native', - ); + console.error(t('sentryCliNotFound')); return; } if (!fs.existsSync(copyDebugidPath)) { return; } - console.log('Copying debugid'); + console.log(t('copyingDebugId')); spawnSync( 'node', [ @@ -453,9 +447,9 @@ async function uploadSourcemapForSentry( stdio: 'inherit', }, ); - console.log(`Sentry release created for version: ${version}`); + console.log(t('sentryReleaseCreated', { version })); - console.log('Uploading sourcemap'); + console.log(t('uploadingSourcemap')); spawnSync( 'node', [ @@ -479,7 +473,7 @@ async function uploadSourcemapForSentry( const ignorePackingFileNames = ['.', '..', 'index.bundlejs.map']; const ignorePackingExtensions = ['DS_Store','txt.map']; async function pack(dir: string, output: string) { - console.log('Packing'); + console.log(t('packing')); fs.ensureDirSync(path.dirname(output)); await new Promise((resolve, reject) => { const zipfile = new ZipFile(); @@ -516,7 +510,7 @@ async function pack(dir: string, output: string) { }); zipfile.end(); }); - console.log(t('ppkPackageGenerated', { output })); + console.log(t('fileGenerated', { file: output })); } export function readEntire(entry: string, zipFile: ZipFile) { @@ -671,7 +665,7 @@ async function diffFromPPK(origin: string, next: string, output: string) { for (const k in originEntries) { if (!newEntries[k]) { - console.log(`Delete ${k}`); + console.log(t('deleteFile', { file: k })); deletes[k] = 1; } } @@ -844,7 +838,7 @@ export async function enumZipEntries( await result; } } catch (error) { - console.error('处理文件时出错:', error); + console.error(t('processingError', { error })); } zipfile.readEntry(); @@ -860,7 +854,7 @@ function diffArgsCheck(args, options, diffFn) { const [origin, next] = args; if (!origin || !next) { - console.error(`Usage: pushy ${diffFn} `); + console.error(t('usageDiff', { command: diffFn })); process.exit(1); } @@ -927,7 +921,7 @@ export const commands = { const realOutput = output.replace(/\$\{time\}/g, `${Date.now()}`); if (!platform) { - throw new Error('Platform must be specified.'); + throw new Error(t('platformRequired')); } console.log(`Bundling with react-native: ${depVersions['react-native']}`); @@ -972,14 +966,14 @@ export const commands = { async diff({ args, options }) { const { origin, next, realOutput } = diffArgsCheck(args, options, 'diff'); - await diffFromPPK(origin, next, realOutput, 'index.bundlejs'); + await diffFromPPK(origin, next, realOutput); console.log(`${realOutput} generated.`); }, async hdiff({ args, options }) { const { origin, next, realOutput } = diffArgsCheck(args, options, 'hdiff'); - await diffFromPPK(origin, next, realOutput, 'index.bundlejs'); + await diffFromPPK(origin, next, realOutput); console.log(`${realOutput} generated.`); }, diff --git a/src/locales/en.ts b/src/locales/en.ts index 2e365f0..9aab227 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -40,4 +40,52 @@ This can reduce the risk of inconsistent dependencies and supply chain attacks. operationSuccess: 'Operation successful', failedToParseUpdateJson: 'Failed to parse file `update.json`. Try to remove it manually.', ppkPackageGenerated: 'ppk package generated and saved to: {{- output}}', + Message: 'Welcome to Cresc hot update service, {{name}}.', + loggedOut: 'Logged out', + usageUnderDevelopment: 'Usage is under development now.', + hermesDisabled: 'Hermes disabled', + hermesEnabled: 'Hermes enabled, now compiling to hermes bytecode:\n', + runningHermesc: 'Running hermesc: {{command}} {{args}}', + composingSourceMap: 'Composing source map', + copyingDebugId: 'Copying debugid', + sentryCliNotFound: 'Cannot find Sentry CLI tool, please make sure @sentry/cli is properly installed', + sentryReleaseCreated: 'Sentry release created for version: {{version}}', + uploadingSourcemap: 'Uploading sourcemap', + packing: 'Packing', + deletingFile: 'Delete {{file}}', + bundlingWithRN: 'Bundling with react-native: {{version}}', + fileGenerated: '{{file}} generated.', + processingError: 'Error processing file: {{error}}', + usageDiff: 'Usage: cresc {{command}} ', + pluginDetected: 'detected {{name}} plugin', + pluginDetectionError: 'error while detecting {{name}} plugin: {{error}}', + addedToGitignore: 'Added {{line}} to .gitignore', + processingStringPool: 'Processing the string pool ...', + processingPackage: 'Processing the package {{count}} ...', + typeStrings: 'Type strings:', + keyStrings: 'Key strings:', + failedToParseIcon: '[Warning] failed to parse icon: {{error}}', + errorInHarmonyApp: 'Error in getEntryFromHarmonyApp: {{error}}', + totalPackages: 'Total {{count}} packages', + usageUploadIpa: 'Usage: cresc uploadIpa ', + usageUploadApk: 'Usage: cresc uploadApk ', + usageUploadApp: 'Usage: cresc uploadApp ', + usageParseApp: 'Usage: cresc parseApp ', + usageParseIpa: 'Usage: cresc parseIpa ', + usageParseApk: 'Usage: cresc parseApk ', + offset: 'Offset {{offset}}', + packageUploadSuccess: 'Successfully uploaded new hot update package (id: {{id}})', + rolloutRangeError: 'rollout must be an integer between 1-100', + nativeVersionNotFound: 'No native version found >= {{version}}', + nativeVersionNotFoundLess: 'No native version found <= {{version}}', + nativeVersionNotFoundMatch: 'No matching native version found: {{version}}', + packageIdRequired: 'Please provide packageId or packageVersion parameter', + operationComplete: 'Operation complete, bound to {{count}} native versions', + platformRequired: 'Platform must be specified.', + bundleCommandError: '"react-native bundle" command exited with code {{code}}.', + copyHarmonyBundleError: 'Error copying Harmony bundle: {{error}}', + copyFileFailed: 'Failed to copy file: {{error}}', + deleteFile: 'Delete {{file}}', + rolloutConfigSet: 'Set {{rollout}}% rollout for version {{version}} on native version(s) {{versions}}', + versionBind: 'Bound version {{version}} to native version {{nativeVersion}} (id: {{id}})', }; diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 46ebfaa..34343a3 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -38,4 +38,52 @@ export default { operationSuccess: '操作成功', failedToParseUpdateJson: '无法解析文件 `update.json`。请手动删除它。', ppkPackageGenerated: 'ppk 热更包已生成并保存到: {{- output}}', + welcomeMessage: '欢迎使用 pushy 热更新服务,{{name}}。', + loggedOut: '已退出登录', + usageUnderDevelopment: '使用说明正在开发中。', + hermesDisabled: 'Hermes 已禁用', + hermesEnabled: 'Hermes 已启用,正在编译为 hermes 字节码:\n', + runningHermesc: '运行 hermesc:{{command}} {{args}}', + composingSourceMap: '正在生成 source map', + copyingDebugId: '正在复制 debugid', + sentryCliNotFound: '无法找到 Sentry CLI 工具,请确保已正确安装 @sentry/cli', + sentryReleaseCreated: '已为版本 {{version}} 创建 Sentry release', + uploadingSourcemap: '正在上传 sourcemap', + packing: '正在打包', + deletingFile: '删除 {{file}}', + bundlingWithRN: '正在使用 react-native {{version}} 打包', + fileGenerated: '已生成 {{file}}。', + processingError: '处理文件时出错:{{error}}', + usageDiff: '用法:pushy {{command}} ', + pluginDetected: '检测到 {{name}} 插件', + pluginDetectionError: '检测 {{name}} 插件时出错:{{error}}', + addedToGitignore: '已将 {{line}} 添加到 .gitignore', + processingStringPool: '正在处理字符串池...', + processingPackage: '正在处理包 {{count}}...', + typeStrings: '类型字符串:', + keyStrings: '键字符串:', + failedToParseIcon: '[警告] 解析图标失败:{{error}}', + errorInHarmonyApp: '获取 Harmony 应用入口时出错:{{error}}', + totalPackages: '共 {{count}} 个包', + usageUploadIpa: '使用方法: pushy uploadIpa ipa后缀文件', + usageUploadApk: '使用方法: pushy uploadApk apk后缀文件', + usageUploadApp: '使用方法: pushy uploadApp app后缀文件', + usageParseApp: '使用方法: pushy parseApp app后缀文件', + usageParseIpa: '使用方法: pushy parseIpa ipa后缀文件', + usageParseApk: '使用方法: pushy parseApk apk后缀文件', + offset: '偏移量 {{offset}}', + packageUploadSuccess: '已成功上传新热更包(id: {{id}})', + rolloutRangeError: 'rollout 必须是 1-100 的整数', + nativeVersionNotFound: '未查询到 >= {{version}} 的原生版本', + nativeVersionNotFoundLess: '未查询到 <= {{version}} 的原生版本', + nativeVersionNotFoundMatch: '未查询到匹配原生版本:{{version}}', + packageIdRequired: '请提供 packageId 或 packageVersion 参数', + operationComplete: '操作完成,共已绑定 {{count}} 个原生版本', + platformRequired: '必须指定平台。', + bundleCommandError: '"react-native bundle" 命令退出,代码为 {{code}}。', + copyHarmonyBundleError: '复制 Harmony bundle 错误:{{error}}', + copyFileFailed: '复制文件失败:{{error}}', + deleteFile: '删除 {{file}}', + rolloutConfigSet: '已在原生版本 {{versions}} 上设置灰度发布 {{rollout}}% 热更版本 {{version}}', + versionBind: '已将热更版本 {{version}} 绑定到原生版本 {{nativeVersion}} (id: {{id}})', }; diff --git a/src/package.ts b/src/package.ts index 66e6af6..b7bc8cf 100644 --- a/src/package.ts +++ b/src/package.ts @@ -1,5 +1,6 @@ import { get, post, uploadFile } from './api'; import { question, saveToLocal } from './utils'; +import { t } from './utils/i18n'; import { checkPlatform, getSelectedApp } from './app'; @@ -34,7 +35,7 @@ export async function listPackage(appId: string) { } console.log(Table(header, rows).render()); - console.log(`\n共 ${data.length} 个包`); + console.log(t('totalPackages', { count: data.length })); return data; } @@ -54,7 +55,7 @@ export const commands = { uploadIpa: async ({ args }: { args: string[] }) => { const fn = args[0]; if (!fn || !fn.endsWith('.ipa')) { - throw new Error('使用方法: pushy uploadIpa ipa后缀文件'); + throw new Error(t('usageUploadIpa')); } const { versionName, @@ -93,7 +94,7 @@ export const commands = { uploadApk: async ({ args }: { args: string[] }) => { const fn = args[0]; if (!fn || !fn.endsWith('.apk')) { - throw new Error('使用方法: pushy uploadApk apk后缀文件'); + throw new Error(t('usageUploadApk')); } const { versionName, @@ -132,7 +133,7 @@ export const commands = { uploadApp: async ({ args }: { args: string[] }) => { const fn = args[0]; if (!fn || !fn.endsWith('.app')) { - throw new Error('使用方法: pushy uploadApp app后缀文件'); + throw new Error(t('usageUploadApp')); } const { versionName, @@ -171,21 +172,21 @@ export const commands = { parseApp: async ({ args }: { args: string[] }) => { const fn = args[0]; if (!fn || !fn.endsWith('.app')) { - throw new Error('使用方法: pushy parseApp app后缀文件'); + throw new Error(t('usageParseApp')); } console.log(await getAppInfo(fn)); }, parseIpa: async ({ args }: { args: string[] }) => { const fn = args[0]; if (!fn || !fn.endsWith('.ipa')) { - throw new Error('使用方法: pushy parseIpa ipa后缀文件'); + throw new Error(t('usageParseIpa')); } console.log(await getIpaInfo(fn)); }, parseApk: async ({ args }: { args: string[] }) => { const fn = args[0]; if (!fn || !fn.endsWith('.apk')) { - throw new Error('使用方法: pushy parseApk apk后缀文件'); + throw new Error(t('usageParseApk')); } console.log(await getApkInfo(fn)); }, diff --git a/src/user.ts b/src/user.ts index 9079bd9..e877080 100644 --- a/src/user.ts +++ b/src/user.ts @@ -1,6 +1,7 @@ import { question } from './utils'; import { post, get, replaceSession, saveSession, closeSession } from './api'; import crypto from 'node:crypto'; +import { t } from './utils/i18n'; function md5(str: string) { return crypto.createHash('md5').update(str).digest('hex'); @@ -16,11 +17,11 @@ export const commands = { }); replaceSession({ token }); await saveSession(); - console.log(`欢迎使用 pushy 热更新服务, ${info.name}.`); + console.log(t('welcomeMessage', { name: info.name })); }, logout: async () => { await closeSession(); - console.log('已退出登录'); + console.log(t('loggedOut')); }, me: async () => { const me = await get('/user/me'); diff --git a/src/utils/add-gitignore.ts b/src/utils/add-gitignore.ts index 41e5a82..8e26e76 100644 --- a/src/utils/add-gitignore.ts +++ b/src/utils/add-gitignore.ts @@ -1,6 +1,7 @@ import fs from 'node:fs'; // import path from 'node:path'; import { credentialFile, tempDir } from './constants'; +import { t } from './i18n'; export function addGitIgnore() { const shouldIgnore = [credentialFile, tempDir]; @@ -26,7 +27,7 @@ export function addGitIgnore() { gitignoreLines.push('# react-native-update'); for (const line of shouldIgnore) { gitignoreLines.push(line); - console.log(`Added ${line} to .gitignore`); + console.log(t('addedToGitignore', { line })); } fs.writeFileSync(gitignorePath, gitignoreLines.join('\n')); diff --git a/src/utils/check-plugin.ts b/src/utils/check-plugin.ts index fa42c4e..840fe9c 100644 --- a/src/utils/check-plugin.ts +++ b/src/utils/check-plugin.ts @@ -1,4 +1,5 @@ import { plugins } from './plugin-config'; +import { t } from './i18n'; interface BundleParams { sentry: boolean; @@ -17,10 +18,10 @@ export async function checkPlugins(): Promise { const isEnabled = await plugin.detect(); if (isEnabled && plugin.bundleParams) { Object.assign(params, plugin.bundleParams); - console.log(`detected ${plugin.name} plugin`); + console.log(t('pluginDetected', { name: plugin.name })); } } catch (err) { - console.warn(`error while detecting ${plugin.name} plugin:`, err); + console.warn(t('pluginDetectionError', { name: plugin.name, error: err })); } } diff --git a/src/versions.ts b/src/versions.ts index b1f0b06..732f108 100644 --- a/src/versions.ts +++ b/src/versions.ts @@ -1,5 +1,6 @@ import { get, post, put, uploadFile } from './api'; import { question, saveToLocal } from './utils'; +import { t } from './utils/i18n'; import { checkPlatform, getSelectedApp } from './app'; import { choosePackage } from './package'; @@ -35,7 +36,7 @@ interface CommandOptions { async function showVersion(appId: string, offset: number) { const { data, count } = await get(`/app/${appId}/version/list`); - console.log(`Offset ${offset}`); + console.log(t('offset', { offset })); for (const version of data) { const pkgCount = version.packages?.length || 0; let packageInfo = ''; @@ -149,7 +150,7 @@ export const commands = { }); // TODO local diff saveToLocal(fn, `${appId}/ppk/${id}.ppk`); - console.log(`已成功上传新热更包(id: ${id})`); + console.log(t('packageUploadSuccess', { id })); const v = await question('是否现在将此热更应用到原生包上?(Y/N)'); if (v.toLowerCase() === 'y') { @@ -192,10 +193,10 @@ export const commands = { try { rollout = Number.parseInt(options.rollout); } catch (e) { - throw new Error('rollout 必须是 1-100 的整数'); + throw new Error(t('rolloutRangeError')); } if (rollout < 1 || rollout > 100) { - throw new Error('rollout 必须是 1-100 的整数'); + throw new Error(t('rolloutRangeError')); } } @@ -206,7 +207,7 @@ export const commands = { compare(pkg.name, minPkgVersion, '>='), ); if (pkgs.length === 0) { - throw new Error(`未查询到 >= ${minPkgVersion} 的原生版本`); + throw new Error(t('nativeVersionNotFound', { version: minPkgVersion })); } if (rollout !== undefined) { const rolloutConfig: Record = {}; @@ -219,9 +220,10 @@ export const commands = { }, }); console.log( - `已在原生版本 ${pkgs - .map((pkg: Package) => pkg.name) - .join(', ')} 上设置灰度发布 ${rollout}% 热更版本 ${versionId}`, + `${t('rolloutConfigSet', { + versions: pkgs.map((pkg: Package) => pkg.name).join(', '), + rollout: rollout, + })}`, ); } for (const pkg of pkgs) { @@ -229,10 +231,14 @@ export const commands = { versionId, }); console.log( - `已将热更版本 ${versionId} 绑定到原生版本 ${pkg.name} (id: ${pkg.id})`, + `${t('versionBind', { + version: versionId, + nativeVersion: pkg.name, + id: pkg.id, + })}`, ); } - console.log(`操作完成,共已绑定 ${pkgs.length} 个原生版本`); + console.log(t('operationComplete', { count: pkgs.length })); return; } if (maxPkgVersion) { @@ -242,7 +248,7 @@ export const commands = { compare(pkg.name, maxPkgVersion, '<='), ); if (pkgs.length === 0) { - throw new Error(`未查询到 <= ${maxPkgVersion} 的原生版本`); + throw new Error(t('nativeVersionNotFoundLess', { version: maxPkgVersion })); } if (rollout !== undefined) { const rolloutConfig: Record = {}; @@ -255,9 +261,10 @@ export const commands = { }, }); console.log( - `已在原生版本 ${pkgs - .map((pkg: Package) => pkg.name) - .join(', ')} 上设置灰度发布 ${rollout}% 热更版本 ${versionId}`, + `${t('rolloutConfigSet', { + versions: pkgs.map((pkg: Package) => pkg.name).join(', '), + rollout: rollout, + })}`, ); } for (const pkg of pkgs) { @@ -265,10 +272,14 @@ export const commands = { versionId, }); console.log( - `已将热更版本 ${versionId} 绑定到原生版本 ${pkg.name} (id: ${pkg.id})`, + `${t('versionBind', { + version: versionId, + nativeVersion: pkg.name, + id: pkg.id, + })}`, ); } - console.log(`操作完成,共已绑定 ${pkgs.length} 个原生版本`); + console.log(t('operationComplete', { count: pkgs.length })); return; } @@ -279,7 +290,7 @@ export const commands = { if (pkg) { pkgId = pkg.id; } else { - throw new Error(`未查询到匹配原生版本:${pkgVersion}`); + throw new Error(t('nativeVersionNotFoundMatch', { version: pkgVersion })); } } if (!pkgId) { @@ -287,7 +298,7 @@ export const commands = { } if (!pkgId) { - throw new Error('请提供 packageId 或 packageVersion 参数'); + throw new Error(t('packageIdRequired')); } if (!pkgVersion) { @@ -306,7 +317,10 @@ export const commands = { }, }); console.log( - `已将在原生版本 ${pkgVersion} (id: ${pkgId}) 上设置灰度发布 ${rollout}% 热更版本 ${versionId} `, + `${t('rolloutConfigSet', { + versions: pkgVersion, + rollout: rollout, + })}`, ); } @@ -315,9 +329,14 @@ export const commands = { versionId, }); console.log( - `已将热更版本 ${versionId} 绑定到原生版本 ${pkgVersion} (id: ${pkgId})`, + `${t('versionBind', { + version: versionId, + nativeVersion: pkgVersion, + id: pkgId, + })}`, ); } + console.log(t('operationSuccess')); }, updateVersionInfo: async ({ args, @@ -339,6 +358,6 @@ export const commands = { if (options.metaInfo) updateParams.metaInfo = options.metaInfo; await put(`/app/${appId}/version/${versionId}`, updateParams); - console.log('操作成功'); + console.log(t('operationSuccess')); }, };