feat(i18n): 添加国际化支持和中英文切换功能
- 实现完整的中英文国际化系统,支持动态语言切换 - 新增健康数据权限说明页面,提供HealthKit数据使用说明 - 为服药记录添加庆祝动画效果,提升用户体验 - 优化药品添加页面的阴影效果和视觉层次 - 更新个人页面以支持多语言显示和语言选择模态框
This commit is contained in:
298
i18n/index.ts
Normal file
298
i18n/index.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user