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;