feat(i18n): 全面实现应用核心功能模块的国际化支持

- 新增 i18n 翻译资源,覆盖睡眠、饮水、体重、锻炼、用药 AI 识别、步数、健身圆环、基础代谢及设置等核心模块
- 重构相关页面及组件(如 SleepDetail, WaterDetail, WorkoutHistory 等)使用 `useI18n` 钩子替换硬编码文本
- 升级 `utils/date` 工具库与 `DateSelector` 组件,支持基于语言环境的日期格式化与显示
- 完善登录页、注销流程及权限申请弹窗的双语提示信息
- 优化部分页面的 UI 细节与字体样式以适配多语言显示
This commit is contained in:
richarjiang
2025-11-27 17:54:36 +08:00
parent 08adf0f20d
commit fbe0c92f0f
26 changed files with 2508 additions and 1622 deletions

View File

@@ -13,6 +13,7 @@ import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree';
import { Colors } from '@/constants/Colors';
import { useAppDispatch } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useI18n } from '@/hooks/useI18n';
import { fetchMyProfile, login } from '@/store/userSlice';
import Toast from 'react-native-toast-message';
@@ -23,6 +24,7 @@ export default function LoginScreen() {
const color = Colors[scheme];
const pageBackground = scheme === 'light' ? color.pageBackgroundEmphasis : color.background;
const dispatch = useAppDispatch();
const { t } = useI18n();
const AnimatedLinear = useMemo(() => Animated.createAnimatedComponent(LinearGradient), []);
// 背景动效:轻微平移/旋转与呼吸动画
@@ -79,12 +81,12 @@ export default function LoginScreen() {
const guardAgreement = useCallback((action: () => void) => {
if (!hasAgreed) {
Alert.alert(
'请先阅读并同意',
'继续登录前,请阅读并勾选《隐私政策》和《用户协议》。点击“同意并继续”将默认勾选并继续登录。',
t('login.agreement.alert.title'),
t('login.agreement.alert.message'),
[
{ text: '取消', style: 'cancel' },
{ text: t('login.agreement.alert.cancel'), style: 'cancel' },
{
text: '同意并继续',
text: t('login.agreement.alert.confirm'),
onPress: () => {
setHasAgreed(true);
setTimeout(() => action(), 0);
@@ -96,7 +98,7 @@ export default function LoginScreen() {
return;
}
action();
}, [hasAgreed]);
}, [hasAgreed, t]);
const onAppleLogin = useCallback(async () => {
if (!appleAvailable) return;
@@ -110,7 +112,7 @@ export default function LoginScreen() {
});
const identityToken = (credential as any)?.identityToken;
if (!identityToken || typeof identityToken !== 'string') {
throw new Error('未获取到 Apple 身份令牌');
throw new Error(t('login.errors.appleIdentityTokenMissing'));
}
await dispatch(login({ appleIdentityToken: identityToken })).unwrap();
@@ -118,7 +120,7 @@ export default function LoginScreen() {
await dispatch(fetchMyProfile())
Toast.show({
text1: '登录成功',
text1: t('login.success.loginSuccess'),
type: 'success',
});
// 登录成功后处理重定向
@@ -145,12 +147,12 @@ export default function LoginScreen() {
console.log('err.code', err.code);
if (err?.code === 'ERR_CANCELED' || err?.code === 'ERR_REQUEST_CANCELED') return;
const message = err?.message || '登录失败,请稍后再试';
Alert.alert('登录失败', message);
const message = err?.message || t('login.errors.loginFailed');
Alert.alert(t('login.errors.loginFailedTitle'), message);
} finally {
setLoading(false);
}
}, [appleAvailable, router, searchParams?.redirectParams, searchParams?.redirectTo]);
}, [appleAvailable, router, searchParams?.redirectParams, searchParams?.redirectTo, dispatch, t]);
// 登录按钮不再因未勾选协议而禁用,仅在加载中禁用
@@ -244,14 +246,14 @@ export default function LoginScreen() {
<Ionicons name="chevron-back" size={24} color={scheme === 'dark' ? '#ECEDEE' : '#192126'} />
</TouchableOpacity>
)}
<Text style={[styles.headerTitle, { color: color.text }]}></Text>
<Text style={[styles.headerTitle, { color: color.text }]}>{t('login.title')}</Text>
<View style={{ width: 32 }} />
</View>
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
<View style={styles.headerWrap}>
<ThemedText style={[styles.title, { color: color.text }]}>Out Live</ThemedText>
<ThemedText style={[styles.subtitle, { color: color.textMuted }]}></ThemedText>
<ThemedText style={[styles.subtitle, { color: color.textMuted }]}>{t('login.subtitle')}</ThemedText>
</View>
{/* Apple 登录 */}
@@ -276,12 +278,12 @@ export default function LoginScreen() {
color="#FFFFFF"
style={{ marginRight: 10 }}
/>
<Text style={styles.appleText}>...</Text>
<Text style={styles.appleText}>{t('login.loggingIn')}</Text>
</>
) : (
<>
<Ionicons name="logo-apple" size={22} color="#FFFFFF" style={{ marginRight: 10 }} />
<Text style={styles.appleText}>使 Apple </Text>
<Text style={styles.appleText}>{t('login.appleLogin')}</Text>
</>
)}
</GlassView>
@@ -294,12 +296,12 @@ export default function LoginScreen() {
color="#FFFFFF"
style={{ marginRight: 10 }}
/>
<Text style={styles.appleText}>...</Text>
<Text style={styles.appleText}>{t('login.loggingIn')}</Text>
</>
) : (
<>
<Ionicons name="logo-apple" size={22} color="#FFFFFF" style={{ marginRight: 10 }} />
<Text style={styles.appleText}>使 Apple </Text>
<Text style={styles.appleText}>{t('login.appleLogin')}</Text>
</>
)}
</View>
@@ -319,13 +321,13 @@ export default function LoginScreen() {
{hasAgreed && <Ionicons name="checkmark" size={14} color={color.onPrimary} />}
</View>
</Pressable>
<Text style={[styles.agreementText, { color: color.textMuted }]}></Text>
<Text style={[styles.agreementText, { color: color.textMuted }]}>{t('login.agreement.readAndAgree')}</Text>
<Pressable onPress={() => Linking.openURL(PRIVACY_POLICY_URL)}>
<Text style={[styles.link, { color: color.primary }]}></Text>
<Text style={[styles.link, { color: color.primary }]}>{t('login.agreement.privacyPolicy')}</Text>
</Pressable>
<Text style={[styles.agreementText, { color: color.textMuted }]}></Text>
<Text style={[styles.agreementText, { color: color.textMuted }]}>{t('login.agreement.and')}</Text>
<Pressable onPress={() => Linking.openURL(USER_AGREEMENT_URL)}>
<Text style={[styles.link, { color: color.primary }]}></Text>
<Text style={[styles.link, { color: color.primary }]}>{t('login.agreement.userAgreement')}</Text>
</Pressable>
</View>