- 实现完整的中英文国际化系统,支持动态语言切换 - 新增健康数据权限说明页面,提供HealthKit数据使用说明 - 为服药记录添加庆祝动画效果,提升用户体验 - 优化药品添加页面的阴影效果和视觉层次 - 更新个人页面以支持多语言显示和语言选择模态框
299 lines
10 KiB
TypeScript
299 lines
10 KiB
TypeScript
import * as Localization from 'expo-localization';
|
||
import i18n from 'i18next';
|
||
import { initReactI18next } from 'react-i18next';
|
||
|
||
import { getItemSync, setItem } from '@/utils/kvStore';
|
||
|
||
export const LANGUAGE_PREFERENCE_KEY = 'app_language_preference';
|
||
export const SUPPORTED_LANGUAGES = ['zh', 'en'] as const;
|
||
export type AppLanguage = typeof SUPPORTED_LANGUAGES[number];
|
||
|
||
const fallbackLanguage: AppLanguage = 'zh';
|
||
|
||
const personalScreenResources = {
|
||
edit: '编辑',
|
||
login: '登录',
|
||
memberNumber: '会员编号: {{number}}',
|
||
aiUsage: '免费AI次数: {{value}}',
|
||
aiUsageUnlimited: '无限',
|
||
fishRecord: '能量记录',
|
||
stats: {
|
||
height: '身高',
|
||
weight: '体重',
|
||
age: '年龄',
|
||
ageSuffix: '岁',
|
||
},
|
||
membership: {
|
||
badge: '尊享会员',
|
||
planFallback: 'VIP 会员',
|
||
expiryLabel: '会员有效期',
|
||
changeButton: '更改会员套餐',
|
||
validForever: '长期有效',
|
||
dateFormat: 'YYYY年MM月DD日',
|
||
},
|
||
sections: {
|
||
notifications: '通知',
|
||
developer: '开发者',
|
||
other: '其他',
|
||
account: '账号与安全',
|
||
language: '语言',
|
||
healthData: '健康数据授权',
|
||
},
|
||
menu: {
|
||
notificationSettings: '通知设置',
|
||
developerOptions: '开发者选项',
|
||
pushSettings: '推送通知设置',
|
||
privacyPolicy: '隐私政策',
|
||
feedback: '意见反馈',
|
||
userAgreement: '用户协议',
|
||
logout: '退出登录',
|
||
deleteAccount: '注销帐号',
|
||
healthDataPermissions: '健康数据授权说明',
|
||
},
|
||
language: {
|
||
title: '语言',
|
||
menuTitle: '界面语言',
|
||
modalTitle: '选择语言',
|
||
modalSubtitle: '选择后界面会立即更新',
|
||
cancel: '取消',
|
||
options: {
|
||
zh: {
|
||
label: '中文',
|
||
description: '推荐中文用户使用',
|
||
},
|
||
en: {
|
||
label: '英文',
|
||
description: '使用英文界面',
|
||
},
|
||
},
|
||
},
|
||
};
|
||
|
||
const healthPermissionsResources = {
|
||
title: '健康数据授权说明',
|
||
subtitle: '我们通过 Apple Health 的 HealthKit/CareKit 接口同步必要的数据,让训练、恢复和提醒更贴合你的身体状态。',
|
||
cards: {
|
||
usage: {
|
||
title: '我们会读取 / 写入的数据',
|
||
items: [
|
||
'运动与活动:步数、活动能量、锻炼记录用于生成训练表现和热力图。',
|
||
'身体指标:身高、体重、体脂率帮助制定个性化训练与营养建议。',
|
||
'睡眠与恢复:睡眠时长与阶段用于智能提醒与恢复建议。',
|
||
'水分摄入:读取与写入饮水记录,保持与「健康」App 一致。',
|
||
],
|
||
},
|
||
purpose: {
|
||
title: '使用这些数据的目的',
|
||
items: [
|
||
'提供个性化训练计划、挑战与恢复建议。',
|
||
'在统计页展示长期趋势,帮助你理解身体变化。',
|
||
'减少重复输入,在提醒与挑战中自动同步进度。',
|
||
],
|
||
},
|
||
control: {
|
||
title: '你的控制权',
|
||
items: [
|
||
'授权流程完全由 Apple Health 控制,你可随时在 iOS 设置 > 健康 > 数据访问与设备 中更改权限。',
|
||
'未授权的数据不会被访问,撤销授权后我们会清理相关缓存。',
|
||
'核心功能依旧可用,并提供手动输入等替代方案。',
|
||
],
|
||
},
|
||
privacy: {
|
||
title: '数据存储与隐私',
|
||
items: [
|
||
'健康数据仅存储在你的设备上,我们不会上传服务器或共享给第三方。',
|
||
'只有在需要同步的功能中才会保存聚合后的匿名统计值。',
|
||
'我们遵循 Apple 的审核要求,任何变更都会提前告知。',
|
||
],
|
||
},
|
||
},
|
||
callout: {
|
||
title: '未授权会怎样?',
|
||
items: [
|
||
'相关模块会提示你授权,并提供手动记录入口。',
|
||
'拒绝授权不会影响其它与健康数据无关的功能。',
|
||
],
|
||
},
|
||
contact: {
|
||
title: '需要更多帮助?',
|
||
description: '如果你对 HealthKit / CareKit 的使用方式有疑问,可通过以下邮箱或在个人中心提交反馈:',
|
||
email: 'richardwei1995@gmail.com',
|
||
},
|
||
};
|
||
|
||
const resources = {
|
||
zh: {
|
||
translation: {
|
||
personal: personalScreenResources,
|
||
healthPermissions: healthPermissionsResources,
|
||
},
|
||
},
|
||
en: {
|
||
translation: {
|
||
personal: {
|
||
edit: 'Edit',
|
||
login: 'Log in',
|
||
memberNumber: 'Member ID: {{number}}',
|
||
aiUsage: 'Free AI credits: {{value}}',
|
||
aiUsageUnlimited: 'Unlimited',
|
||
fishRecord: 'Energy log',
|
||
stats: {
|
||
height: 'Height',
|
||
weight: 'Weight',
|
||
age: 'Age',
|
||
ageSuffix: ' yrs',
|
||
},
|
||
membership: {
|
||
badge: 'Premium member',
|
||
planFallback: 'VIP Membership',
|
||
expiryLabel: 'Valid until',
|
||
changeButton: 'Change plan',
|
||
validForever: 'No expiry',
|
||
dateFormat: 'YYYY-MM-DD',
|
||
},
|
||
sections: {
|
||
notifications: 'Notifications',
|
||
developer: 'Developer',
|
||
other: 'Other',
|
||
account: 'Account & Security',
|
||
language: 'Language',
|
||
healthData: 'Health data permissions',
|
||
},
|
||
menu: {
|
||
notificationSettings: 'Notification settings',
|
||
developerOptions: 'Developer options',
|
||
pushSettings: 'Push notification settings',
|
||
privacyPolicy: 'Privacy policy',
|
||
feedback: 'Feedback',
|
||
userAgreement: 'User agreement',
|
||
logout: 'Log out',
|
||
deleteAccount: 'Delete account',
|
||
healthDataPermissions: 'Health data disclosure',
|
||
},
|
||
language: {
|
||
title: 'Language',
|
||
menuTitle: 'Display language',
|
||
modalTitle: 'Choose language',
|
||
modalSubtitle: 'Your selection applies immediately',
|
||
cancel: 'Cancel',
|
||
options: {
|
||
zh: {
|
||
label: 'Chinese',
|
||
description: 'Use the Chinese interface',
|
||
},
|
||
en: {
|
||
label: 'English',
|
||
description: 'Use the app in English',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
healthPermissions: {
|
||
title: 'Health data disclosure',
|
||
subtitle: 'We integrate with Apple Health through HealthKit and CareKit to deliver precise training, recovery, and reminder experiences.',
|
||
cards: {
|
||
usage: {
|
||
title: 'Data we read or write',
|
||
items: [
|
||
'Activity: steps, active energy, and workouts fuel performance charts and rings.',
|
||
'Body metrics: height, weight, and body fat keep plans and nutrition tips personalized.',
|
||
'Sleep & recovery: duration and stages unlock recovery advice and reminders.',
|
||
'Hydration: we read and write water intake so Health and the app stay in sync.',
|
||
],
|
||
},
|
||
purpose: {
|
||
title: 'Why we need it',
|
||
items: [
|
||
'Generate adaptive training plans, challenges, and recovery nudges.',
|
||
'Display long-term trends so you can understand progress at a glance.',
|
||
'Reduce manual input by syncing reminders and challenge progress automatically.',
|
||
],
|
||
},
|
||
control: {
|
||
title: 'Your control',
|
||
items: [
|
||
'Permissions are granted inside Apple Health; change them anytime under iOS Settings > Health > Data Access & Devices.',
|
||
'We never access data you do not authorize, and cached values are removed if you revoke access.',
|
||
'Core functionality keeps working and offers manual input alternatives.',
|
||
],
|
||
},
|
||
privacy: {
|
||
title: 'Storage & privacy',
|
||
items: [
|
||
'Health data stays on your device — we do not upload it or share it with third parties.',
|
||
'Only aggregated, anonymized stats are synced when absolutely necessary.',
|
||
'We follow Apple’s review requirements and will notify you before any changes.',
|
||
],
|
||
},
|
||
},
|
||
callout: {
|
||
title: 'What if I skip authorization?',
|
||
items: [
|
||
'The related modules will ask for permission and provide manual logging options.',
|
||
'Declining does not break other areas of the app that do not rely on Health data.',
|
||
],
|
||
},
|
||
contact: {
|
||
title: 'Need help?',
|
||
description: 'Questions about HealthKit or CareKit? Reach out via email or the in-app feedback form:',
|
||
email: 'richardwei1995@gmail.com',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
};
|
||
|
||
export const isSupportedLanguage = (language?: string | null): language is AppLanguage => {
|
||
if (!language) return false;
|
||
return SUPPORTED_LANGUAGES.some((code) => language === code || language.startsWith(`${code}-`));
|
||
};
|
||
|
||
export const getNormalizedLanguage = (language?: string | null): AppLanguage => {
|
||
if (!language) return fallbackLanguage;
|
||
const normalized = SUPPORTED_LANGUAGES.find((code) => language === code || language.startsWith(`${code}-`));
|
||
return normalized ?? fallbackLanguage;
|
||
};
|
||
|
||
const getStoredLanguage = (): AppLanguage | null => {
|
||
try {
|
||
const stored = getItemSync?.(LANGUAGE_PREFERENCE_KEY) as AppLanguage | null | undefined;
|
||
if (stored && isSupportedLanguage(stored)) {
|
||
return stored;
|
||
}
|
||
} catch (error) {
|
||
// ignore storage errors and fall back to device preference
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const getDeviceLanguage = (): AppLanguage | null => {
|
||
try {
|
||
const locales = Localization.getLocales();
|
||
const preferred = locales.find((locale) => locale.languageCode && isSupportedLanguage(locale.languageCode));
|
||
return preferred?.languageCode as AppLanguage | undefined || null;
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
const initialLanguage = getStoredLanguage() ?? getDeviceLanguage() ?? fallbackLanguage;
|
||
|
||
void i18n.use(initReactI18next).init({
|
||
compatibilityJSON: 'v4',
|
||
resources,
|
||
lng: initialLanguage,
|
||
fallbackLng: fallbackLanguage,
|
||
interpolation: {
|
||
escapeValue: false,
|
||
},
|
||
returnNull: false,
|
||
});
|
||
|
||
export const changeAppLanguage = async (language: AppLanguage) => {
|
||
const nextLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
|
||
await i18n.changeLanguage(nextLanguage);
|
||
await setItem(LANGUAGE_PREFERENCE_KEY, nextLanguage);
|
||
};
|
||
|
||
export default i18n;
|