From d8eb6677c658bdf94cff9e72f12f85b56c786d0a Mon Sep 17 00:00:00 2001 From: Guoguo Date: Sat, 18 Jul 2026 20:07:13 -0700 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20=E4=B8=AD=E8=8B=B1=E6=96=87?= =?UTF-8?q?=E6=A1=86=E6=9E=B6(=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4?= =?UTF-8?q?=E5=AD=97=E5=85=B8+=E8=AF=AD=E8=A8=80=E5=88=87=E6=8D=A2)+profil?= =?UTF-8?q?e=20=E6=8E=A5=E5=85=A5=E5=8F=82=E8=80=83+=E6=B3=A8=E5=86=8C3?= =?UTF-8?q?=E4=B8=AA=E6=96=B0=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- miniprogram/app.js | 3 + miniprogram/app.json | 5 +- miniprogram/i18n/index.js | 120 +++++++++++++++++++++++++ miniprogram/i18n/locales/common.js | 67 ++++++++++++++ miniprogram/i18n/locales/index.js | 27 ++++++ miniprogram/i18n/locales/lightInfo.js | 6 ++ miniprogram/i18n/locales/manual.js | 6 ++ miniprogram/i18n/locales/profile.js | 69 ++++++++++++++ miniprogram/i18n/locales/report.js | 57 ++++++++++++ miniprogram/pages/profile/profile.js | 46 +++++++--- miniprogram/pages/profile/profile.wxml | 63 +++++++------ 11 files changed, 431 insertions(+), 38 deletions(-) create mode 100644 miniprogram/i18n/index.js create mode 100644 miniprogram/i18n/locales/common.js create mode 100644 miniprogram/i18n/locales/index.js create mode 100644 miniprogram/i18n/locales/lightInfo.js create mode 100644 miniprogram/i18n/locales/manual.js create mode 100644 miniprogram/i18n/locales/profile.js create mode 100644 miniprogram/i18n/locales/report.js diff --git a/miniprogram/app.js b/miniprogram/app.js index f2297a3..a3fbd09 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -1,4 +1,5 @@ var http = require('./utils/request') +var i18n = require('./i18n/index') App({ globalData: { @@ -12,6 +13,8 @@ App({ onLaunch: function () { var sysInfo = wx.getSystemInfoSync() this.globalData.statusBarHeight = sysInfo.statusBarHeight || 44 + i18n.getLocale() + i18n.applyTabBar() this.checkLogin() }, diff --git a/miniprogram/app.json b/miniprogram/app.json index d4ca568..d82bdef 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -17,7 +17,10 @@ "pages/profile/profile", "pages/history/history", "pages/help/help", - "pages/contact/contact" + "pages/contact/contact", + "pages/light-info/light-info", + "pages/manual-treatment/manual-treatment", + "pages/scan-report/scan-report" ], "window": { "navigationBarBackgroundColor": "#ffffff", diff --git a/miniprogram/i18n/index.js b/miniprogram/i18n/index.js new file mode 100644 index 0000000..25af774 --- /dev/null +++ b/miniprogram/i18n/index.js @@ -0,0 +1,120 @@ +// Lightweight i18n for WeChat Mini Program (no official i18n API). +// +// Usage in a page: +// var i18n = require('../../i18n/index') +// onShow: function () { i18n.bind(this) } // injects `i18n` + `locale` into page data +// WXML: {{i18n.common.loading}} / {{i18n.profile.title}} +// JS strings: i18n.t('common.loading') +// +// Locale is persisted in storage; switching locale notifies listeners and the +// convention is that every page re-runs i18n.bind(this) in onShow, so tab pages +// pick up the new language the next time they are shown. + +var locales = require('./locales/index') + +var STORAGE_KEY = 'app_locale' +var DEFAULT_LOCALE = 'zh' +var SUPPORTED = ['zh', 'en'] +var LOCALE_NAMES = { zh: '中文', en: 'English' } + +var _listeners = [] +var _current = null + +function getSystemLocale() { + try { + var sys = wx.getSystemInfoSync() + var lang = (sys.language || '').toLowerCase() + return lang.indexOf('en') === 0 ? 'en' : 'zh' + } catch (e) { + return DEFAULT_LOCALE + } +} + +function getLocale() { + if (_current) return _current + var saved = '' + try { saved = wx.getStorageSync(STORAGE_KEY) } catch (e) {} + _current = SUPPORTED.indexOf(saved) !== -1 ? saved : getSystemLocale() + return _current +} + +function setLocale(locale) { + if (SUPPORTED.indexOf(locale) === -1 || locale === _current) return + _current = locale + try { wx.setStorageSync(STORAGE_KEY, locale) } catch (e) {} + applyTabBar() + for (var i = 0; i < _listeners.length; i++) { + try { _listeners[i](locale) } catch (e) {} + } +} + +function onChange(fn) { if (typeof fn === 'function') _listeners.push(fn) } +function offChange(fn) { + _listeners = _listeners.filter(function (f) { return f !== fn }) +} + +function getDict() { + var loc = getLocale() + return locales[loc] || locales[DEFAULT_LOCALE] +} + +function _lookup(dict, key) { + if (!dict) return undefined + var parts = key.split('.') + var cur = dict + for (var i = 0; i < parts.length; i++) { + if (cur == null) return undefined + cur = cur[parts[i]] + } + return cur +} + +// t('profile.title', {name: 'x'}) — falls back to zh, then to the raw key. +function t(key, params) { + var loc = getLocale() + var val = _lookup(locales[loc], key) + if (val === undefined) val = _lookup(locales[DEFAULT_LOCALE], key) + if (typeof val !== 'string') return val === undefined ? key : val + if (params) { + val = val.replace(/\{(\w+)\}/g, function (m, k) { + return params[k] !== undefined ? params[k] : m + }) + } + return val +} + +// Inject the full dictionary + current locale into a page/component's data. +function bind(page) { + if (page && typeof page.setData === 'function') { + page.setData({ i18n: getDict(), locale: getLocale() }) + } +} + +// Refresh the bottom tabBar labels to the current locale. +function applyTabBar() { + var d = getDict() + var tab = (d && d.common && d.common.tab) || {} + var items = [ + { index: 0, text: tab.home }, + { index: 1, text: tab.records }, + { index: 2, text: tab.mine } + ] + items.forEach(function (it) { + if (!it.text) return + wx.setTabBarItem({ index: it.index, text: it.text, fail: function () {} }) + }) +} + +module.exports = { + SUPPORTED: SUPPORTED, + LOCALE_NAMES: LOCALE_NAMES, + getLocale: getLocale, + setLocale: setLocale, + getSystemLocale: getSystemLocale, + onChange: onChange, + offChange: offChange, + getDict: getDict, + bind: bind, + applyTabBar: applyTabBar, + t: t +} diff --git a/miniprogram/i18n/locales/common.js b/miniprogram/i18n/locales/common.js new file mode 100644 index 0000000..23228d0 --- /dev/null +++ b/miniprogram/i18n/locales/common.js @@ -0,0 +1,67 @@ +// Shared strings used across many pages. +module.exports = { + zh: { + tab: { home: '首页', records: '记录', mine: '我的' }, + loading: '加载中...', + confirm: '确定', + cancel: '取消', + save: '保存', + saving: '保存中...', + back: '返回', + retry: '重试', + close: '关闭', + done: '完成', + start: '开始', + stop: '停止', + unknown: '未知', + saveSuccess: '保存成功', + saveFailed: '保存失败', + networkError: '网络异常,请重试', + deviceNotConnected: '设备未连接', + regions: { + right: '右区', + left: '左区', + top: '上区', + middle: '中区', + bottom: '下区' + }, + wavelengths: { + red: '红光', + infrared: '红外光', + uv: '紫外光', + yellow: '黄光' + } + }, + en: { + tab: { home: 'Home', records: 'Records', mine: 'Me' }, + loading: 'Loading...', + confirm: 'OK', + cancel: 'Cancel', + save: 'Save', + saving: 'Saving...', + back: 'Back', + retry: 'Retry', + close: 'Close', + done: 'Done', + start: 'Start', + stop: 'Stop', + unknown: 'Unknown', + saveSuccess: 'Saved', + saveFailed: 'Save failed', + networkError: 'Network error, please retry', + deviceNotConnected: 'Device not connected', + regions: { + right: 'Right', + left: 'Left', + top: 'Top', + middle: 'Center', + bottom: 'Bottom' + }, + wavelengths: { + red: 'Red', + infrared: 'Infrared', + uv: 'UV', + yellow: 'Yellow' + } + } +} diff --git a/miniprogram/i18n/locales/index.js b/miniprogram/i18n/locales/index.js new file mode 100644 index 0000000..ee859d8 --- /dev/null +++ b/miniprogram/i18n/locales/index.js @@ -0,0 +1,27 @@ +// Merges per-namespace dictionaries into { zh: {...}, en: {...} }. +// Each namespace file exports { zh: {...}, en: {...} } and owns its own strings, +// so parallel work on different features never touches the same file. + +var common = require('./common') +var profile = require('./profile') +var lightInfo = require('./lightInfo') +var manual = require('./manual') +var report = require('./report') + +var namespaces = { + common: common, + profile: profile, + lightInfo: lightInfo, + manual: manual, + report: report +} + +var zh = {} +var en = {} +for (var ns in namespaces) { + if (!namespaces.hasOwnProperty(ns)) continue + zh[ns] = namespaces[ns].zh || {} + en[ns] = namespaces[ns].en || {} +} + +module.exports = { zh: zh, en: en } diff --git a/miniprogram/i18n/locales/lightInfo.js b/miniprogram/i18n/locales/lightInfo.js new file mode 100644 index 0000000..f4d0aa3 --- /dev/null +++ b/miniprogram/i18n/locales/lightInfo.js @@ -0,0 +1,6 @@ +// Strings for the light-therapy efficacy intro page (pages/light-info). +// Owned by the light-info feature. Keep zh/en keys in sync. +module.exports = { + zh: {}, + en: {} +} diff --git a/miniprogram/i18n/locales/manual.js b/miniprogram/i18n/locales/manual.js new file mode 100644 index 0000000..411d3c4 --- /dev/null +++ b/miniprogram/i18n/locales/manual.js @@ -0,0 +1,6 @@ +// Strings for the manual treatment (direct wavelength/function select) page +// (pages/manual-treatment). Owned by the manual-mode feature. Keep zh/en in sync. +module.exports = { + zh: {}, + en: {} +} diff --git a/miniprogram/i18n/locales/profile.js b/miniprogram/i18n/locales/profile.js new file mode 100644 index 0000000..3ce4f23 --- /dev/null +++ b/miniprogram/i18n/locales/profile.js @@ -0,0 +1,69 @@ +// "我的" page strings + language switcher. +module.exports = { + zh: { + notLoggedIn: '未登录', + editProfile: '编辑资料', + editProfileTitle: '编辑资料', + changeAvatar: '点击更换头像', + nickname: '昵称', + nicknamePlaceholder: '请输入昵称', + nicknameRequired: '昵称不能为空', + smartMode: '智能模式', + smartModeOpen: '已开放', + free: '免费', + inUse: '使用中', + remainDays: '剩余 {days}天', + notSubscribed: '未订阅', + goSubscribe: '去订阅 ›', + menuDevice: '我的设备', + bound: '已绑定', + unbound: '未绑定', + menuHistory: '使用记录', + menuSubscription: '订阅管理', + menuHelp: '使用帮助', + menuContact: '联系我们', + menuLightInfo: '光疗功效介绍', + language: '语言 / Language', + logout: '退出登录', + logoutConfirmTitle: '退出登录', + logoutConfirmText: '确定要退出登录吗?', + unbindConfirmTitle: '确认解绑', + unbindConfirmText: '解绑后将无法使用该设备,确定要解绑吗?', + unbindAction: '解绑当前设备', + unbindDone: '已解绑', + unbindFailed: '解绑失败' + }, + en: { + notLoggedIn: 'Not logged in', + editProfile: 'Edit', + editProfileTitle: 'Edit Profile', + changeAvatar: 'Tap to change avatar', + nickname: 'Nickname', + nicknamePlaceholder: 'Enter nickname', + nicknameRequired: 'Nickname is required', + smartMode: 'Smart Mode', + smartModeOpen: 'Available', + free: 'Free', + inUse: 'Active', + remainDays: '{days} days left', + notSubscribed: 'Not subscribed', + goSubscribe: 'Subscribe ›', + menuDevice: 'My Device', + bound: 'Bound', + unbound: 'Not bound', + menuHistory: 'Usage Records', + menuSubscription: 'Subscription', + menuHelp: 'Help', + menuContact: 'Contact Us', + menuLightInfo: 'Light Therapy Guide', + language: '语言 / Language', + logout: 'Log Out', + logoutConfirmTitle: 'Log Out', + logoutConfirmText: 'Are you sure you want to log out?', + unbindConfirmTitle: 'Confirm Unbind', + unbindConfirmText: 'You will not be able to use this device after unbinding. Continue?', + unbindAction: 'Unbind current device', + unbindDone: 'Unbound', + unbindFailed: 'Unbind failed' + } +} diff --git a/miniprogram/i18n/locales/report.js b/miniprogram/i18n/locales/report.js new file mode 100644 index 0000000..1d57fdc --- /dev/null +++ b/miniprogram/i18n/locales/report.js @@ -0,0 +1,57 @@ +// Strings for the scan report page (pages/scan-report). +module.exports = { + zh: { + title: '肌肤扫描报告', + subtitle: '基于多光谱反射的肤质倾向参考', + scannedAt: '扫描时间', + overallTitle: '整体倾向', + regionTitle: '分区分析', + recommendTitle: '推荐护理', + noProblem: '未见明显倾向', + startTreatment: '开始推荐护理', + manualInstead: '我自己选', + disclaimer: '本报告依据光反射信号给出肤质倾向参考,不构成医疗诊断。实际护理请结合自身情况。', + conceptNotice: '当前诊断依据尚在标定中,结果仅供参考', + level: { low: '轻度', mid: '中度', high: '明显' }, + problems: { + aging: '光老化 / 暗沉', + acne: '痘痘 / 出油', + pigment: '色素 / 暗黄', + deep: '深层循环 / 疲劳' + }, + regionNames: { + right: '右脸', + left: '左脸', + top: '额头', + middle: '鼻区', + bottom: '下巴' + } + }, + en: { + title: 'Skin Scan Report', + subtitle: 'Skin-tendency reference from multispectral reflection', + scannedAt: 'Scanned at', + overallTitle: 'Overall Tendency', + regionTitle: 'Zone Analysis', + recommendTitle: 'Recommended Care', + noProblem: 'No notable tendency', + startTreatment: 'Start Recommended Care', + manualInstead: 'Choose Myself', + disclaimer: 'This report gives a skin-tendency reference from light-reflection signals and is not a medical diagnosis. Please consider your own condition.', + conceptNotice: 'Diagnosis thresholds are still being calibrated; results are for reference only', + level: { low: 'Mild', mid: 'Moderate', high: 'Notable' }, + problems: { + aging: 'Photoaging / Dullness', + acne: 'Acne / Oiliness', + pigment: 'Pigment / Sallowness', + deep: 'Deep circulation / Fatigue' + }, + regionNames: { + right: 'Right cheek', + left: 'Left cheek', + top: 'Forehead', + middle: 'Nose', + bottom: 'Chin' + } + } +} diff --git a/miniprogram/pages/profile/profile.js b/miniprogram/pages/profile/profile.js index e989c51..24d8619 100644 --- a/miniprogram/pages/profile/profile.js +++ b/miniprogram/pages/profile/profile.js @@ -1,5 +1,6 @@ var api = require('../../utils/api') var config = require('../../config/env') +var i18n = require('../../i18n/index') var app = getApp() Page({ @@ -16,9 +17,30 @@ Page({ }, onShow: function () { + i18n.bind(this) this.loadProfile() }, + onLightInfo: function () { + wx.navigateTo({ url: '/pages/light-info/light-info' }) + }, + + onSwitchLanguage: function () { + var self = this + var supported = i18n.SUPPORTED + var names = supported.map(function (l) { return i18n.LOCALE_NAMES[l] || l }) + wx.showActionSheet({ + itemList: names, + success: function (res) { + var target = supported[res.tapIndex] + if (target && target !== i18n.getLocale()) { + i18n.setLocale(target) + i18n.bind(self) + } + } + }) + }, + loadProfile: function () { var self = this self.setData({ loading: true }) @@ -33,9 +55,11 @@ Page({ }) var p2 = api.getSubscription().then(function (sub) { + var remaining = sub.remaining_days || 0 self.setData({ subscription: sub, - subRemaining: sub.remaining_days || 0 + subRemaining: remaining, + subRemainingText: i18n.t('profile.remainDays', { days: remaining }) }) }).catch(function (err) { console.error('getSubscription failed', err) @@ -50,19 +74,19 @@ Page({ var self = this if (this.data.deviceCount > 0) { wx.showActionSheet({ - itemList: ['解绑当前设备'], + itemList: [i18n.t('profile.unbindAction')], success: function (res) { if (res.tapIndex === 0) { wx.showModal({ - title: '确认解绑', - content: '解绑后将无法使用该设备,确定要解绑吗?', + title: i18n.t('profile.unbindConfirmTitle'), + content: i18n.t('profile.unbindConfirmText'), success: function (modalRes) { if (modalRes.confirm) { api.unbindDevice().then(function () { - wx.showToast({ title: '已解绑', icon: 'success' }) + wx.showToast({ title: i18n.t('profile.unbindDone'), icon: 'success' }) self.loadProfile() }).catch(function (err) { - wx.showToast({ title: err.message || '解绑失败', icon: 'none' }) + wx.showToast({ title: err.message || i18n.t('profile.unbindFailed'), icon: 'none' }) }) } } @@ -151,7 +175,7 @@ Page({ var self = this var nickname = (self.data.editNickname || '').trim() if (!nickname) { - wx.showToast({ title: '昵称不能为空', icon: 'none' }) + wx.showToast({ title: i18n.t('profile.nicknameRequired'), icon: 'none' }) return } @@ -170,18 +194,18 @@ Page({ }) }).then(function () { self.setData({ saving: false, editing: false }) - wx.showToast({ title: '保存成功', icon: 'success' }) + wx.showToast({ title: i18n.t('common.saveSuccess'), icon: 'success' }) self.loadProfile() }).catch(function (err) { self.setData({ saving: false }) - wx.showToast({ title: err.message || '保存失败', icon: 'none' }) + wx.showToast({ title: err.message || i18n.t('common.saveFailed'), icon: 'none' }) }) }, onLogout: function () { wx.showModal({ - title: '退出登录', - content: '确定要退出登录吗?', + title: i18n.t('profile.logoutConfirmTitle'), + content: i18n.t('profile.logoutConfirmText'), success: function (res) { if (res.confirm) { wx.removeStorageSync('token') diff --git a/miniprogram/pages/profile/profile.wxml b/miniprogram/pages/profile/profile.wxml index 207d8ad..dca69ff 100644 --- a/miniprogram/pages/profile/profile.wxml +++ b/miniprogram/pages/profile/profile.wxml @@ -1,7 +1,7 @@ - 加载中... + {{i18n.common.loading}} @@ -9,65 +9,76 @@ 👤 - 编辑资料 + {{i18n.profile.editProfile}} - ✨ 智能模式 - 已开放 + ✨ {{i18n.profile.smartMode}} + {{i18n.profile.smartModeOpen}} - 免费 + {{i18n.profile.free}} - ✨ 智能模式 - 剩余 {{subRemaining}}天 + ✨ {{i18n.profile.smartMode}} + {{subRemainingText}} - 使用中 + {{i18n.profile.inUse}} - 智能模式 - 未订阅 + {{i18n.profile.smartMode}} + {{i18n.profile.notSubscribed}} - 去订阅 › + {{i18n.profile.goSubscribe}} 📱 - 我的设备 - 已绑定 - 未绑定 + {{i18n.profile.menuDevice}} + {{i18n.profile.bound}} + {{i18n.profile.unbound}} 📋 - 使用记录 + {{i18n.profile.menuHistory}} 💳 - 订阅管理 + {{i18n.profile.menuSubscription}} + + + + 💡 + {{i18n.profile.menuLightInfo}} - 使用帮助 + {{i18n.profile.menuHelp}} 📞 - 联系我们 + {{i18n.profile.menuContact}} + + + + 🌐 + {{i18n.profile.language}} + {{locale === 'en' ? 'English' : '中文'}} - 退出登录 + {{i18n.profile.logout}} v0.1.8 @@ -75,19 +86,19 @@ - 编辑资料 + {{i18n.profile.editProfileTitle}} 👤 - 点击更换头像 + {{i18n.profile.changeAvatar}} - 昵称 - + {{i18n.profile.nickname}} + - - + +