Files
digital-pilates/i18n/index.ts
richarjiang 416d144387 feat(i18n): 添加国际化支持和中英文切换功能
- 实现完整的中英文国际化系统,支持动态语言切换
- 新增健康数据权限说明页面,提供HealthKit数据使用说明
- 为服药记录添加庆祝动画效果,提升用户体验
- 优化药品添加页面的阴影效果和视觉层次
- 更新个人页面以支持多语言显示和语言选择模态框
2025-11-13 09:05:23 +08:00

299 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Apples 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;