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 @@
👤
- {{userInfo.nickname || '未登录'}}
+ {{userInfo.nickname || i18n.profile.notLoggedIn}}
{{userInfo.phone || ''}}
- 编辑资料
+ {{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.logout}}
v0.1.8
@@ -75,19 +86,19 @@
- 编辑资料
+ {{i18n.profile.editProfileTitle}}
👤
- 点击更换头像
+ {{i18n.profile.changeAvatar}}
- 昵称
-
+ {{i18n.profile.nickname}}
+
-
-
+
+