feat(client): i18n copy + reco lang refresh + home polish

This commit is contained in:
1
2026-02-02 20:11:09 +08:00
parent 64b8352ad3
commit 996999453a
19 changed files with 594 additions and 188 deletions

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import SheetModal from '@/components/ui/SheetModal';
import { MOCK_CONTENT } from '@/src/constants/mockContent';
import { getFavorites } from '@/src/storage/appStorage';
import { getFavorites, getRecoFeedCache, type FavoriteItem } from '@/src/storage/appStorage';
type Props = {
visible: boolean;
@@ -13,25 +13,50 @@ type Props = {
export default function FavoritesModal({ visible, onClose }: Props) {
const { t } = useTranslation();
const [ids, setIds] = useState<string[]>([]);
const [items, setItems] = useState<(FavoriteItem & { text: string })[]>([]);
// 每次打开时刷新一次,确保展示最新“喜欢”
useEffect(() => {
if (!visible) return;
let cancelled = false;
(async () => {
const list = await getFavorites();
if (!cancelled) setIds(list);
const [favList, cache] = await Promise.all([
getFavorites(),
getRecoFeedCache()
]);
console.log('FavoritesModal: Loaded favList', favList.length, 'items');
console.log('FavoritesModal: Loaded cache items', cache?.items?.length || 0);
if (cancelled) return;
// 建立文案查找表
const textMap = new Map<string, string>();
// 1. 放入 Mock 数据
MOCK_CONTENT.forEach(c => textMap.set(String(c.id), t(c.textKey)));
// 2. 放入缓存数据
if (cache?.items) {
cache.items.forEach(c => textMap.set(String(c.content_id), c.text));
}
// 3. 组装最终展示列表
const enriched = favList.map(fav => {
const favIdStr = String(fav.id);
const text = fav.text || textMap.get(favIdStr);
console.log(`FavoritesModal: Matching fav.id=${favIdStr}, found text=${!!text}, textValue=${text?.substring(0, 10)}...`);
return {
...fav,
text: text || t('favorites.unknownText')
};
});
setItems(enriched);
})();
return () => {
cancelled = true;
};
}, [visible]);
const items = useMemo(() => {
const map = new Map(MOCK_CONTENT.map((c) => [c.id, c]));
return ids.map((id) => map.get(id)).filter(Boolean) as { id: string; text: string }[];
}, [ids]);
}, [visible, t]);
return (
<SheetModal visible={visible} title={t('profile.favorites')} onClose={onClose}>
@@ -41,7 +66,7 @@ export default function FavoritesModal({ visible, onClose }: Props) {
) : (
<FlatList
data={items}
keyExtractor={(it) => it.id}
keyExtractor={(it) => it.favId}
contentContainerStyle={styles.list}
renderItem={({ item }) => (
<View style={styles.row}>

View File

@@ -22,6 +22,7 @@ import {
getFavorites,
setDailyReminderSettings,
removeFavorite,
getRecoFeedCache,
getUserProfile,
type DailyReminderSettings,
type FavoriteItem,
@@ -271,15 +272,21 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
async function refreshFavorites() {
const storedFavs = await getFavorites();
const map = new Map(MOCK_CONTENT.map((c) => [c.id, c.text]));
const list = storedFavs
.map((fav) => ({
...fav,
text: map.get(fav.id) || ''
}))
.filter(item => item.text !== '');
const textMap = new Map<string, string>();
// 1) Mock 文案
MOCK_CONTENT.forEach((c) => textMap.set(String(c.id), t(c.textKey)));
// 2) 后端推荐缓存文案(避免收藏后 cache 覆盖就丢文案)
const cache = await getRecoFeedCache();
cache?.items?.forEach((c) => textMap.set(String(c.content_id), c.text));
// 3) 组装:优先使用收藏时写入的 text其次从 map 回填
const list = storedFavs.map((fav) => ({
...fav,
text: fav.text || textMap.get(String(fav.id)) || t('favorites.unknownText'),
}));
setFavorites(list);
}
@@ -390,7 +397,7 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () =
if (settings.status === 'denied') {
Alert.alert(
t('common.notice'),
"系统权限已被拒绝,请前往手机设置开启通知。"
t('permissions.notificationsDenied')
);
setPushEnabled(false);
return;
@@ -607,12 +614,12 @@ function WidgetHowToPage() {
}
function LanguagePage() {
const { i18n } = useTranslation();
const { t, i18n } = useTranslation();
const currentLang = i18n.language;
const languages = [
{ id: 'zh-TW', label: '繁体' },
{ id: 'en', label: 'English' },
{ id: 'zh-TW', label: t('language.zhTW') },
{ id: 'en', label: t('language.en') },
];
return (

View File

@@ -1,13 +1,14 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import { useTranslation } from 'react-i18next';
import { SerifText } from './SerifText';
import { OnboardingColors } from '@/constants/OnboardingTheme';
export const INTENTS = [
{ id: 'love', label: '爱情', icon: '❤️' },
{ id: 'life', label: '生活', icon: '⛅' },
{ id: 'travel', label: '旅游', icon: '🌴' },
{ id: 'work', label: '职场', icon: '💼' },
{ id: 'love', labelKey: 'intent.love', icon: '❤️' },
{ id: 'life', labelKey: 'intent.life', icon: '⛅' },
{ id: 'travel', labelKey: 'intent.travel', icon: '🌴' },
{ id: 'work', labelKey: 'intent.work', icon: '💼' },
];
interface IntentSelectionStepProps {
@@ -16,9 +17,10 @@ interface IntentSelectionStepProps {
}
export function IntentSelectionStep({ selectedIds, onToggle }: IntentSelectionStepProps) {
const { t } = useTranslation();
return (
<View style={styles.container}>
<SerifText style={styles.title}></SerifText>
<SerifText style={styles.title}>{t('intent.title')}</SerifText>
<View style={styles.grid}>
{INTENTS.map((intent) => {
@@ -35,7 +37,7 @@ export function IntentSelectionStep({ selectedIds, onToggle }: IntentSelectionSt
>
<SerifText style={styles.icon}>{intent.icon}</SerifText>
<SerifText style={[styles.label, isSelected && styles.labelSelected]}>
{intent.label}
{t(intent.labelKey)}
</SerifText>
</TouchableOpacity>
);

View File

@@ -49,17 +49,9 @@ export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip }
{/* 底部按钮:距离底部 12% 高度 */}
<View style={styles.footer}>
<View style={styles.footerRow}>
{onSkip && (
<TouchableOpacity onPress={onSkip} activeOpacity={0.8} style={styles.skipBtn}>
<SerifText style={styles.skipText}></SerifText>
</TouchableOpacity>
)}
<TouchableOpacity onPress={onNext} disabled={!hasSelection} activeOpacity={0.8}>
{hasSelection ? <BtnClicked width={87} height={57} /> : <BtnNotClicked width={87} height={57} />}
</TouchableOpacity>
</View>
<TouchableOpacity onPress={onNext} disabled={!hasSelection} activeOpacity={0.8}>
{hasSelection ? <BtnClicked width={87} height={57} /> : <BtnNotClicked width={87} height={57} />}
</TouchableOpacity>
</View>
</View>
);

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useMemo, useState, useRef } from 'react';
import { Modal, Pressable, StyleSheet, Text, View, PanResponder, Animated as RNAnimated, Dimensions, Image, ImageSourcePropType } from 'react-native';
import { useTranslation } from 'react-i18next';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated, {
Easing,
@@ -27,6 +28,7 @@ type Props = {
* - 高度固定:默认距离顶部固定间距,也支持传入指定高度
*/
export default function SheetModal({ visible, title, onClose, children, leftIcon, height: customHeight }: Props) {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const [mounted, setMounted] = useState(false);
const progress = useSharedValue(0); // 0: 关闭, 1: 打开
@@ -121,7 +123,7 @@ export default function SheetModal({ visible, title, onClose, children, leftIcon
</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel={leftIcon ? "返回" : "关闭"}
accessibilityLabel={leftIcon ? t('common.back') : t('common.close')}
onPress={onClose}
hitSlop={10}
style={styles.close}