feat(i18n): 增强生理周期模块的国际化支持,添加多语言格式和翻译
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { fetchMenstrualFlowSamples } from '@/utils/health';
|
||||
import { fetchMenstrualFlowSamples, healthDataEvents } from '@/utils/health';
|
||||
import {
|
||||
buildMenstrualTimeline,
|
||||
convertHealthKitSamplesToCycleRecords,
|
||||
@@ -49,7 +49,10 @@ export const MenstrualCycleCard: React.FC<Props> = ({ onPress }) => {
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
const loadMenstrualData = async () => {
|
||||
setLoading(true);
|
||||
// Avoid setting loading to true for background updates to prevent UI flicker
|
||||
if (records.length === 0) {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
const today = dayjs();
|
||||
const startDate = today.subtract(3, 'month').startOf('month').toDate();
|
||||
@@ -72,8 +75,16 @@ export const MenstrualCycleCard: React.FC<Props> = ({ onPress }) => {
|
||||
};
|
||||
|
||||
loadMenstrualData();
|
||||
|
||||
// Listen for data changes
|
||||
const handleDataChange = () => {
|
||||
loadMenstrualData();
|
||||
};
|
||||
healthDataEvents.on('menstrualDataChanged', handleDataChange);
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
healthDataEvents.off('menstrualDataChanged', handleDataChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { STATUS_COLORS } from './constants';
|
||||
import { DayCellProps } from './types';
|
||||
|
||||
export const DayCell: React.FC<DayCellProps> = ({ cell, isSelected, onPress }) => {
|
||||
const { t } = useTranslation();
|
||||
const status = cell.info?.status;
|
||||
const colors = status ? STATUS_COLORS[status] : undefined;
|
||||
|
||||
@@ -32,7 +34,7 @@ export const DayCell: React.FC<DayCellProps> = ({ cell, isSelected, onPress }) =
|
||||
{cell.label}
|
||||
</Text>
|
||||
</View>
|
||||
{cell.isToday && <Text style={styles.todayText}>今天</Text>}
|
||||
{cell.isToday && <Text style={styles.todayText}>{t('menstrual.today')}</Text>}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/en';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DimensionValue, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { InlineTipProps } from './types';
|
||||
|
||||
@@ -12,9 +15,12 @@ export const InlineTip: React.FC<InlineTipProps> = ({
|
||||
onMarkStart,
|
||||
onCancelMark,
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
// 14.28% per cell. Center is 7.14%.
|
||||
const pointerLeft = `${columnIndex * 14.2857 + 7.1428}%` as DimensionValue;
|
||||
const isFuture = selectedDate.isAfter(dayjs(), 'day');
|
||||
const localeKey = i18n.language.startsWith('en') ? 'en' : 'zh-cn';
|
||||
const dateFormat = t('menstrual.dateFormatShort', { defaultValue: 'M月D日' });
|
||||
|
||||
return (
|
||||
<View style={styles.inlineTipCard}>
|
||||
@@ -22,17 +28,19 @@ export const InlineTip: React.FC<InlineTipProps> = ({
|
||||
<View style={styles.inlineTipRow}>
|
||||
<View style={styles.inlineTipDate}>
|
||||
<Ionicons name="calendar-outline" size={16} color="#111827" />
|
||||
<Text style={styles.inlineTipDateText}>{selectedDate.format('M月D日')}</Text>
|
||||
<Text style={styles.inlineTipDateText}>
|
||||
{selectedDate.locale(localeKey).format(dateFormat)}
|
||||
</Text>
|
||||
</View>
|
||||
{!isFuture && (!selectedInfo || !selectedInfo.confirmed) && (
|
||||
<TouchableOpacity style={styles.inlinePrimaryBtn} onPress={onMarkStart}>
|
||||
<Ionicons name="add" size={14} color="#fff" />
|
||||
<Text style={styles.inlinePrimaryText}>标记经期</Text>
|
||||
<Text style={styles.inlinePrimaryText}>{t('menstrual.actions.markPeriod')}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{!isFuture && selectedInfo?.confirmed && selectedInfo.status === 'period' && (
|
||||
<TouchableOpacity style={styles.inlineSecondaryBtn} onPress={onCancelMark}>
|
||||
<Text style={styles.inlineSecondaryText}>取消标记</Text>
|
||||
<Text style={styles.inlineSecondaryText}>{t('menstrual.actions.cancelMark')}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { STATUS_COLORS } from './constants';
|
||||
import { LegendItem } from './types';
|
||||
|
||||
const LEGEND_ITEMS: LegendItem[] = [
|
||||
{ label: '经期', key: 'period' },
|
||||
{ label: '预测经期', key: 'predicted-period' },
|
||||
{ label: '排卵期', key: 'fertile' },
|
||||
{ label: '排卵日', key: 'ovulation-day' },
|
||||
];
|
||||
|
||||
export const Legend: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const legendItems: LegendItem[] = [
|
||||
{ label: t('menstrual.legend.period'), key: 'period' },
|
||||
{ label: t('menstrual.legend.predictedPeriod'), key: 'predicted-period' },
|
||||
{ label: t('menstrual.legend.fertile'), key: 'fertile' },
|
||||
{ label: t('menstrual.legend.ovulation'), key: 'ovulation-day' },
|
||||
];
|
||||
|
||||
return (
|
||||
<View style={styles.legendRow}>
|
||||
{LEGEND_ITEMS.map((item) => (
|
||||
{legendItems.map((item) => (
|
||||
<View key={item.key} style={styles.legendItem}>
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -17,6 +17,7 @@ interface MonthBlockProps {
|
||||
selectedDateKey: string;
|
||||
onSelect: (dateKey: string) => void;
|
||||
renderTip: (colIndex: number) => React.ReactNode;
|
||||
weekLabels?: string[];
|
||||
}
|
||||
|
||||
export const MonthBlock: React.FC<MonthBlockProps> = ({
|
||||
@@ -24,8 +25,10 @@ export const MonthBlock: React.FC<MonthBlockProps> = ({
|
||||
selectedDateKey,
|
||||
onSelect,
|
||||
renderTip,
|
||||
weekLabels,
|
||||
}) => {
|
||||
const weeks = useMemo(() => chunkArray(month.cells, 7), [month.cells]);
|
||||
const labels = weekLabels?.length === 7 ? weekLabels : WEEK_LABELS;
|
||||
|
||||
return (
|
||||
<View style={styles.monthCard}>
|
||||
@@ -34,7 +37,7 @@ export const MonthBlock: React.FC<MonthBlockProps> = ({
|
||||
<Text style={styles.monthSubtitle}>{month.subtitle}</Text>
|
||||
</View>
|
||||
<View style={styles.weekRow}>
|
||||
{WEEK_LABELS.map((label) => (
|
||||
{labels.map((label) => (
|
||||
<Text key={label} style={styles.weekLabel}>
|
||||
{label}
|
||||
</Text>
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Share,
|
||||
@@ -19,7 +18,7 @@ import {
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import ViewShot from 'react-native-view-shot';
|
||||
import ViewShot, { captureRef } from 'react-native-view-shot';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import {
|
||||
@@ -156,7 +155,7 @@ export function WorkoutDetailModal({
|
||||
type: 'info',
|
||||
text1: t('workoutDetail.share.generating', '正在生成分享卡片…'),
|
||||
});
|
||||
const uri = await shareContentRef.current.capture?.({
|
||||
const uri = await captureRef(shareContentRef, {
|
||||
format: 'png',
|
||||
quality: 0.95,
|
||||
snapshotContentContainer: true,
|
||||
@@ -164,6 +163,7 @@ export function WorkoutDetailModal({
|
||||
if (!uri) {
|
||||
throw new Error('share-capture-failed');
|
||||
}
|
||||
const shareUri = uri.startsWith('file://') ? uri : `file://${uri}`;
|
||||
const shareTitle = t('workoutDetail.share.title', { defaultValue: activityName || t('workoutDetail.title', '锻炼详情') });
|
||||
const caloriesLabel = metrics?.calories != null
|
||||
? `${metrics.calories} ${t('workoutDetail.metrics.caloriesUnit')}`
|
||||
@@ -179,7 +179,7 @@ export function WorkoutDetailModal({
|
||||
await Share.share({
|
||||
title: shareTitle,
|
||||
message: shareMessage,
|
||||
url: Platform.OS === 'ios' ? uri : `file://${uri}`,
|
||||
url: shareUri,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('workout-detail-share-failed', error);
|
||||
@@ -487,7 +487,6 @@ export function WorkoutDetailModal({
|
||||
<ViewShot
|
||||
ref={shareContentRef}
|
||||
style={[styles.sheetContainer, styles.shareCaptureContainer]}
|
||||
collapsable={false}
|
||||
options={{ format: 'png', quality: 0.95, snapshotContentContainer: true }}
|
||||
>
|
||||
<LinearGradient
|
||||
|
||||
Reference in New Issue
Block a user