From 584f698329cacac1f73e8f84b79f293e81e0abc5 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Thu, 4 Sep 2025 10:24:52 +0800 Subject: [PATCH] bump version to 10.31.0-beta.0 and integrate i18n for improved localization support --- Example/testHotUpdate/bun.lock | 4 +- Example/testHotUpdate/ios/Podfile.lock | 35 +------- Example/testHotUpdate/package.json | 2 +- package.json | 2 +- src/client.ts | 53 ++++++++---- src/i18n.ts | 108 +++++++++++++++++++++++++ src/locales/en.ts | 70 ++++++++++++++++ src/locales/zh.ts | 67 +++++++++++++++ src/provider.tsx | 59 ++++++++------ src/utils.ts | 20 +++-- 10 files changed, 335 insertions(+), 85 deletions(-) create mode 100644 src/i18n.ts create mode 100644 src/locales/en.ts create mode 100644 src/locales/zh.ts diff --git a/Example/testHotUpdate/bun.lock b/Example/testHotUpdate/bun.lock index 19e00ae..893c523 100644 --- a/Example/testHotUpdate/bun.lock +++ b/Example/testHotUpdate/bun.lock @@ -12,7 +12,7 @@ "react-native-paper": "^5.14.5", "react-native-safe-area-context": "^5.5.0", "react-native-svg": "^15.12.0", - "react-native-update": "^10.29.4", + "react-native-update": "^10.31.0-beta.0", "react-native-vector-icons": "^10.2.0", }, "devDependencies": { @@ -1420,7 +1420,7 @@ "react-native-svg": ["react-native-svg@15.12.0", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-iE25PxIJ6V0C6krReLquVw6R0QTsRTmEQc4K2Co3P6zsimU/jltcDBKYDy1h/5j9S/fqmMeXnpM+9LEWKJKI6A=="], - "react-native-update": ["react-native-update@10.29.4", "", { "dependencies": { "nanoid": "^3.3.3", "react-native-url-polyfill": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-native": ">=0.59.0" } }, "sha512-leQX3dq4yBi/oFn0l06nXd7OOFnZnlcMIrAXH7vgTRsqXCdYLoSsZXXkcSYxncn8tBqzh02w4880mlqouve6Sg=="], + "react-native-update": ["react-native-update@10.31.0-beta.0", "", { "dependencies": { "nanoid": "^3.3.3", "react-native-url-polyfill": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-native": ">=0.59.0" } }, "sha512-kQJxvc0q+acBfdBfeSjs8tq+0B79c+9QZUrHgLX9lWhnIl4qZ17QZ/WJOdkpPq6asPjUiCrUxdEhfp0lr50p4w=="], "react-native-url-polyfill": ["react-native-url-polyfill@2.0.0", "", { "dependencies": { "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "react-native": "*" } }, "sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA=="], diff --git a/Example/testHotUpdate/ios/Podfile.lock b/Example/testHotUpdate/ios/Podfile.lock index 8729223..7d7c8ae 100644 --- a/Example/testHotUpdate/ios/Podfile.lock +++ b/Example/testHotUpdate/ios/Podfile.lock @@ -1407,7 +1407,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-update (10.29.4): + - react-native-update (10.31.0-beta.0): - DoubleConversion - glog - hermes-engine @@ -1423,8 +1423,7 @@ PODS: - React-hermes - React-ImageManager - React-jsi - - react-native-update/HDiffPatch (= 10.29.4) - - react-native-update/RCTPushy (= 10.29.4) + - react-native-update/RCTPushy (= 10.31.0-beta.0) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -1435,33 +1434,7 @@ PODS: - ReactCommon/turbomodule/core - SSZipArchive - Yoga - - react-native-update/HDiffPatch (10.29.4): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.11.18.00) - - RCTRequired - - RCTTypeSafety - - React - - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-hermes - - React-ImageManager - - React-jsi - - React-NativeModulesApple - - React-RCTFabric - - React-renderercss - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - SSZipArchive - - Yoga - - react-native-update/RCTPushy (10.29.4): + - react-native-update/RCTPushy (10.31.0-beta.0): - DoubleConversion - glog - hermes-engine @@ -2187,7 +2160,7 @@ SPEC CHECKSUMS: React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468 React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6 react-native-safe-area-context: 11d29ae675265669f498d7d9de2341087e8fe162 - react-native-update: 6d3a3eb322cbc382ad78853cb52e44e8c93e8072 + react-native-update: 25c349c4edf9dc895beeb1281cccc9d93f1cb3be React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d diff --git a/Example/testHotUpdate/package.json b/Example/testHotUpdate/package.json index dae055f..eed2ec8 100644 --- a/Example/testHotUpdate/package.json +++ b/Example/testHotUpdate/package.json @@ -22,7 +22,7 @@ "react-native-paper": "^5.14.5", "react-native-safe-area-context": "^5.5.0", "react-native-svg": "^15.12.0", - "react-native-update": "^10.29.4", + "react-native-update": "^10.31.0-beta.0", "react-native-vector-icons": "^10.2.0" }, "devDependencies": { diff --git a/package.json b/package.json index 13857c1..8f8eed6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-update", - "version": "10.30.4", + "version": "10.31.0-beta.0", "description": "react-native hot update", "main": "src/index", "scripts": { diff --git a/src/client.ts b/src/client.ts index f8ea4c2..014420e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -28,6 +28,7 @@ import { promiseAny, testUrls, } from './utils'; +import i18n from './i18n'; const SERVER_PRESETS = { // cn @@ -107,13 +108,18 @@ export class Pushy { })(); constructor(options: ClientOptions, clientType?: 'Pushy' | 'Cresc') { - if (Platform.OS === 'ios' || Platform.OS === 'android') { - if (!options.appKey) { - throw new Error('appKey is required'); - } - } this.clientType = clientType || 'Pushy'; this.options.server = SERVER_PRESETS[this.clientType]; + + // Initialize i18n based on clientType + i18n.setLocale(this.clientType === 'Pushy' ? 'zh' : 'en'); + + if (Platform.OS === 'ios' || Platform.OS === 'android') { + if (!options.appKey) { + throw new Error(i18n.t('error_appkey_required')); + } + } + this.setOptions(options); if (isRolledBack) { this.report({ @@ -136,6 +142,16 @@ export class Pushy { } }; + /** + * Get translated text based on current clientType + * @param key - Translation key + * @param values - Values for interpolation (optional) + * @returns Translated string + */ + t = (key: string, values?: Record) => { + return i18n.t(key as any, values); + }; + report = async ({ type, message = '', @@ -175,11 +191,7 @@ export class Pushy { }; assertDebug = (matter: string) => { if (__DEV__ && !this.options.debug) { - console.info( - `You are currently in the development environment and have not enabled debug mode. - ${matter} will not be performed. - If you need to debug ${matter} in the development environment, please set debug to true in the client.`, - ); + console.info(this.t('dev_debug_disabled', { matter })); return false; } return true; @@ -270,7 +282,7 @@ export class Pushy { } catch (e: any) { this.report({ type: 'errorChecking', - message: `Can not connect to update server: ${e.message}. Trying backup endpoints.`, + message: this.t('error_cannot_connect_backup', { message: e.message }), }); const backupEndpoints = await this.getBackupEndpoints(); if (backupEndpoints) { @@ -290,14 +302,17 @@ export class Pushy { if (!resp) { this.report({ type: 'errorChecking', - message: 'Can not connect to update server. Please check your network.', + message: this.t('error_cannot_connect_server'), }); this.throwIfEnabled(new Error('errorChecking')); return this.lastRespJson ? await this.lastRespJson : emptyObj; } if (resp.status !== 200) { - const errorMessage = `${resp.status}: ${resp.statusText}`; + const errorMessage = this.t('error_http_status', { + status: resp.status, + statusText: resp.statusText, + }); this.report({ type: 'errorChecking', message: errorMessage, @@ -416,7 +431,9 @@ export class Pushy { }); succeeded = 'diff'; } catch (e: any) { - const errorMessage = `diff error: ${e.message}`; + const errorMessage = this.t('error_diff_failed', { + message: e.message, + }); errorMessages.push(errorMessage); lastError = new Error(errorMessage); log(errorMessage); @@ -433,7 +450,9 @@ export class Pushy { }); succeeded = 'pdiff'; } catch (e: any) { - const errorMessage = `pdiff error: ${e.message}`; + const errorMessage = this.t('error_pdiff_failed', { + message: e.message, + }); errorMessages.push(errorMessage); lastError = new Error(errorMessage); log(errorMessage); @@ -451,7 +470,9 @@ export class Pushy { }); succeeded = 'full'; } catch (e: any) { - const errorMessage = `full patch error: ${e.message}`; + const errorMessage = this.t('error_full_patch_failed', { + message: e.message, + }); errorMessages.push(errorMessage); lastError = new Error(errorMessage); log(errorMessage); diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..097933d --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,108 @@ +import zhTranslations from './locales/zh'; +import enTranslations from './locales/en'; + +type TranslationKey = keyof typeof zhTranslations | keyof typeof enTranslations; +type TranslationValues = Record; + +class I18n { + private currentLocale: 'zh' | 'en' = 'en'; + private translations = { + zh: zhTranslations, + en: enTranslations, + }; + + /** + * Set locale directly + * @param locale - 'zh' or 'en' + */ + setLocale(locale: 'zh' | 'en') { + this.currentLocale = locale; + } + + /** + * Get current locale + */ + getLocale(): 'zh' | 'en' { + return this.currentLocale; + } + + /** + * Translate a key with optional interpolation + * @param key - Translation key + * @param values - Values for interpolation (optional) + * @returns Translated string with interpolated values + */ + t(key: TranslationKey, values?: TranslationValues): string { + const translation = + this.translations[this.currentLocale][ + key as keyof (typeof this.translations)[typeof this.currentLocale] + ]; + + if (!translation) { + // Fallback to the other locale if key not found + const fallbackLocale = this.currentLocale === 'zh' ? 'en' : 'zh'; + const fallbackTranslation = + this.translations[fallbackLocale][ + key as keyof (typeof this.translations)[typeof fallbackLocale] + ]; + + if (!fallbackTranslation) { + // If still not found, return the key itself + return String(key); + } + + return this.interpolate(fallbackTranslation, values); + } + + return this.interpolate(translation, values); + } + + /** + * Interpolate values into a string template + * Supports {{key}} syntax + * @param template - String template with {{key}} placeholders + * @param values - Values to interpolate + * @returns Interpolated string + */ + private interpolate(template: string, values?: TranslationValues): string { + if (!values) { + return template; + } + + return template.replace(/\{\{(\w+)\}\}/g, (match, key) => { + const value = values[key]; + return value !== undefined ? String(value) : match; + }); + } + + /** + * Add or update translations for a specific locale + * @param locale - Target locale + * @param translations - Translation object to merge + */ + addTranslations(locale: 'zh' | 'en', translations: Record) { + this.translations[locale] = { + ...this.translations[locale], + ...translations, + }; + } +} + +// Create singleton instance +const i18n = new I18n(); + +// Export both the instance and the class for flexibility +export { i18n, I18n }; +export default i18n; + +/** + * Usage examples: + * + * // Direct locale setting (new preferred method) + * i18n.setLocale('zh'); // Chinese + * i18n.setLocale('en'); // English + * + * // Get translations + * i18n.t('checking_update'); // Based on current locale + * i18n.t('download_progress', { progress: 50 }); // With interpolation + */ diff --git a/src/locales/en.ts b/src/locales/en.ts new file mode 100644 index 0000000..e4797a2 --- /dev/null +++ b/src/locales/en.ts @@ -0,0 +1,70 @@ +export default { + // Common messages + checking_update: 'Checking for updates...', + downloading_update: 'Downloading update package...', + installing_update: 'Installing update...', + update_available: 'Update available', + update_downloaded: 'Update downloaded successfully', + update_installed: 'Update installed successfully', + no_update_available: 'You are up to date', + update_failed: 'Update failed', + network_error: 'Network connection error', + download_failed: 'Download failed', + install_failed: 'Installation failed', + + // Progress messages with interpolation + download_progress: 'Download progress: {{progress}}%', + download_speed: 'Download speed: {{speed}}/s', + file_size: 'File size: {{size}}', + time_remaining: 'Time remaining: {{time}}', + + // Error messages + error_code: 'Error code: {{code}}', + error_message: 'Error message: {{message}}', + retry_count: 'Retry attempt: {{count}}/{{max}}', + + // Update info + version_info: 'Version {{version}} ({{build}})', + release_notes: 'Release notes: {{notes}}', + update_size: 'Update size: {{size}}MB', + + // Alert messages + alert_title: 'Notice', + alert_update_ready: 'Download completed. Update now?', + alert_next_time: 'Later', + alert_update_now: 'Update Now', + alert_app_updated: + 'Your app version has been updated. Click update to download and install the new version', + alert_update_button: 'Update', + alert_cancel: 'Cancel', + alert_confirm: 'OK', + alert_info: 'Info', + alert_no_update_wait: + 'No update found, please wait 10s for the server to generate the patch package', + + // Error messages + error_appkey_required: 'appKey is required', + error_update_check_failed: 'Update check failed', + error_cannot_connect_server: + 'Can not connect to update server. Please check your network.', + error_cannot_connect_backup: + 'Can not connect to update server: {{message}}. Trying backup endpoints.', + error_diff_failed: 'diff error: {{message}}', + error_pdiff_failed: 'pdiff error: {{message}}', + error_full_patch_failed: 'full patch error: {{message}}', + error_all_promises_rejected: 'All promises were rejected', + error_ping_failed: 'Ping failed', + error_ping_timeout: 'Ping timeout', + error_http_status: '{{status}} {{statusText}}', + + // Development messages + dev_debug_disabled: + 'You are currently in the development environment and have not enabled debug mode. {{matter}} will not be performed. If you need to debug {{matter}} in the development environment, please set debug to true in the client.', + dev_log_prefix: 'react-native-update: ', + dev_web_not_supported: + 'react-native-update does not support the Web platform and will not perform any operations', + + // More alert messages + alert_new_version_found: + 'New version {{name}} found. Download now?\n{{description}}', +}; diff --git a/src/locales/zh.ts b/src/locales/zh.ts new file mode 100644 index 0000000..2d6e96a --- /dev/null +++ b/src/locales/zh.ts @@ -0,0 +1,67 @@ +export default { + // Common messages + checking_update: '正在检查更新...', + downloading_update: '正在下载更新包...', + installing_update: '正在安装更新...', + update_available: '发现新版本', + update_downloaded: '更新包下载完成', + update_installed: '更新安装完成', + no_update_available: '已是最新版本', + update_failed: '更新失败', + network_error: '网络连接错误', + download_failed: '下载失败', + install_failed: '安装失败', + + // Progress messages with interpolation + download_progress: '下载进度: {{progress}}%', + download_speed: '下载速度: {{speed}}/s', + file_size: '文件大小: {{size}}', + time_remaining: '剩余时间: {{time}}', + + // Error messages + error_code: '错误代码: {{code}}', + error_message: '错误信息: {{message}}', + retry_count: '重试次数: {{count}}/{{max}}', + + // Update info + version_info: '版本 {{version}} ({{build}})', + release_notes: '更新说明: {{notes}}', + update_size: '更新包大小: {{size}}MB', + + // Alert messages + alert_title: '提示', + alert_update_ready: '下载完毕,是否立即更新?', + alert_next_time: '下次再说', + alert_update_now: '立即更新', + alert_app_updated: '您的应用版本已更新,点击更新下载安装新版本', + alert_update_button: '更新', + alert_cancel: '取消', + alert_confirm: '确定', + alert_info: '信息', + alert_no_update_wait: '未发现更新,请等待10秒让服务器生成补丁包', + + // Error messages + error_appkey_required: '需要提供 appKey', + error_update_check_failed: '更新检查失败', + error_cannot_connect_server: '无法连接到更新服务器。请检查网络连接。', + error_cannot_connect_backup: + '无法连接到更新服务器: {{message}}。正在尝试备用端点。', + error_diff_failed: 'diff 错误: {{message}}', + error_pdiff_failed: 'pdiff 错误: {{message}}', + error_full_patch_failed: '完整补丁错误: {{message}}', + error_all_promises_rejected: '所有请求都被拒绝', + error_ping_failed: 'Ping 失败', + error_ping_timeout: 'Ping 超时', + error_http_status: '{{status}} {{statusText}}', + + // Development messages + dev_debug_disabled: + '您当前处于开发环境且未启用调试模式。{{matter}} 将不会执行。如需在开发环境中调试 {{matter}},请在客户端中将 debug 设为 true。', + dev_log_prefix: 'react-native-update: ', + dev_web_not_supported: + 'react-native-update 不支持 Web 平台,不会执行任何操作', + + // More alert messages + alert_new_version_found: + '检查到新的版本{{name}},是否下载?\n{{description}}', +}; diff --git a/src/provider.tsx b/src/provider.tsx index 6b1e28c..7aedd29 100644 --- a/src/provider.tsx +++ b/src/provider.tsx @@ -120,16 +120,16 @@ export const UpdateProvider = ({ client.switchVersionLater(hash); return true; } - alertUpdate('提示', '下载完毕,是否立即更新?', [ + alertUpdate(client.t('alert_title'), client.t('alert_update_ready'), [ { - text: '下次再说', + text: client.t('alert_next_time'), style: 'cancel', onPress: () => { client.switchVersionLater(hash); }, }, { - text: '立即更新', + text: client.t('alert_update_now'), style: 'default', onPress: () => { client.switchVersion(hash); @@ -139,7 +139,7 @@ export const UpdateProvider = ({ return true; } catch (e: any) { setLastError(e); - alertError('更新失败', e.message); + alertError(client.t('update_failed'), e.message); throwErrorIfEnabled(e); return false; } @@ -168,7 +168,7 @@ export const UpdateProvider = ({ rootInfo = await client.checkUpdate(extra); } catch (e: any) { setLastError(e); - alertError('更新检查失败', e.message); + alertError(client.t('error_update_check_failed'), e.message); throwErrorIfEnabled(e); return; } @@ -211,21 +211,25 @@ export const UpdateProvider = ({ } return info; } - alertUpdate('提示', '您的应用版本已更新,点击更新下载安装新版本', [ - { - text: '更新', - onPress: () => { - if ( - Platform.OS === 'android' && - downloadUrl.endsWith('.apk') - ) { - downloadAndInstallApk(downloadUrl); - } else { - Linking.openURL(downloadUrl); - } + alertUpdate( + client.t('alert_title'), + client.t('alert_app_updated'), + [ + { + text: client.t('alert_update_button'), + onPress: () => { + if ( + Platform.OS === 'android' && + downloadUrl.endsWith('.apk') + ) { + downloadAndInstallApk(downloadUrl); + } else { + Linking.openURL(downloadUrl); + } + }, }, - }, - ]); + ], + ); } } else if (info.update) { if ( @@ -236,12 +240,15 @@ export const UpdateProvider = ({ return info; } alertUpdate( - '提示', - '检查到新的版本' + info.name + ',是否下载?\n' + info.description, + client.t('alert_title'), + client.t('alert_new_version_found', { + name: info.name, + description: info.description, + }), [ - { text: '取消', style: 'cancel' }, + { text: client.t('alert_cancel'), style: 'cancel' }, { - text: '确定', + text: client.t('alert_confirm'), style: 'default', onPress: () => { downloadUpdate(); @@ -313,8 +320,8 @@ export const UpdateProvider = ({ checkUpdate({ extra: { toHash: payload.data } }).then(() => { if (updateInfoRef.current && updateInfoRef.current.upToDate) { Alert.alert( - 'Info', - 'No update found, please wait 10s for the server to generate the patch package', + client.t('alert_info'), + client.t('alert_no_update_wait'), ); } options.logger = logger; @@ -324,7 +331,7 @@ export const UpdateProvider = ({ } return false; }, - [checkUpdate, options], + [checkUpdate, options, client], ); const parseTestQrCode = useCallback( diff --git a/src/utils.ts b/src/utils.ts index 0e3546c..8134f07 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,8 @@ import { Platform } from 'react-native'; +import i18n from './i18n'; export function log(...args: any[]) { - console.log('react-native-update: ', ...args); + console.log(i18n.t('dev_log_prefix'), ...args); } export function promiseAny(promises: Promise[]) { @@ -14,7 +15,7 @@ export function promiseAny(promises: Promise[]) { .catch(() => { count++; if (count === promises.length) { - reject(new Error('All promises were rejected')); + reject(new Error(i18n.t('error_all_promises_rejected'))); } }); }); @@ -49,7 +50,7 @@ const ping = return finalUrl; } log('ping failed', url, status, statusText); - throw new Error('Ping failed'); + throw new Error(i18n.t('error_ping_failed')); }) .catch(e => { pingFinished = true; @@ -58,7 +59,7 @@ const ping = }), new Promise((_, reject) => setTimeout(() => { - reject(new Error('Ping timeout')); + reject(new Error(i18n.t('error_ping_timeout'))); if (!pingFinished) { log('ping timeout', url); } @@ -91,9 +92,7 @@ export const testUrls = async (urls?: string[]) => { export const assertWeb = () => { if (Platform.OS === 'web') { - console.warn( - 'react-native-update does not support the Web platform and will not perform any operations', - ); + console.warn(i18n.t('dev_web_not_supported')); return false; } return true; @@ -115,7 +114,12 @@ export const enhancedFetch = async ( if (r.ok) { return r; } - throw new Error(`${r.status} ${r.statusText}`); + throw new Error( + i18n.t('error_http_status', { + status: r.status, + statusText: r.statusText, + }), + ); }) .catch(e => { log('fetch error', url, e);