Skip to content

Commit 53eacd3

Browse files
Tonnodoubtclaude
andcommitted
feat(i18n): complete i18n migration for qr-scanner, server-config, and character-manager
- Migrate qr-scanner.tsx to use useTranslation hook - Migrate server-config.tsx to use useTranslation hook - Migrate character-manager.tsx to use useTranslation hook - Add translation keys for qrScanner, serverConfig, characterManager sections - Update all 6 language files (zh-CN, zh-TW, en, ja, ko, ru) - Fix app.config.ts ESM import attribute for JSON Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 677d64d commit 53eacd3

10 files changed

Lines changed: 516 additions & 128 deletions

File tree

app.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ConfigContext, ExpoConfig } from "expo/config";
22

3-
import base from "./app.json";
3+
import base from "./app.json" with { type: "json" };
44

55
type BuildProfile = "development" | "preview" | "production" | string;
66

app/character-manager.tsx

Lines changed: 60 additions & 51 deletions
Large diffs are not rendered by default.

app/qr-scanner.tsx

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import React, { useCallback, useMemo, useState } from 'react';
1+
import React, { useCallback, useState } from 'react';
22
import { Alert, Pressable, StyleSheet, Text, View } from 'react-native';
33
import { CameraView, useCameraPermissions, type BarcodeScanningResult } from 'expo-camera';
44
import { useLocalSearchParams, useRouter } from 'expo-router';
5+
import { useTranslation } from 'react-i18next';
56
import { useDevConnectionConfig } from '@/hooks/useDevConnectionConfig';
67

78
type ReturnToParam = string | undefined;
@@ -20,11 +21,12 @@ export default function QrScannerScreen() {
2021
const params = useLocalSearchParams<{ returnTo?: string }>();
2122
const returnTo: ReturnToParam = typeof params.returnTo === 'string' ? params.returnTo : undefined;
2223
const devConfig = useDevConnectionConfig();
24+
const { t } = useTranslation();
2325

2426
const [permission, requestPermission] = useCameraPermissions();
2527
const [scanned, setScanned] = useState(false);
2628

27-
const titleText = useMemo(() => (__DEV__ ? '开发扫码配置' : '扫码'), []);
29+
const titleText = __DEV__ ? t('qrScanner.titleDev') : t('qrScanner.title');
2830

2931
const handleCancel = useCallback(() => {
3032
router.back();
@@ -37,12 +39,11 @@ export default function QrScannerScreen() {
3739

3840
const raw = (result?.data ?? '').trim();
3941
if (!raw) {
40-
Alert.alert('扫码失败', '未识别到有效内容');
42+
Alert.alert(t('qrScanner.scanFailed'), t('qrScanner.noValidContent'));
4143
setScanned(false);
4244
return;
4345
}
4446

45-
// 回传到上一页:同时写入“全局连接配置”(供整个项目读取)
4647
const target: AllowedReturnTo =
4748
returnTo === '/explore' ||
4849
returnTo === '/audio-test' ||
@@ -58,19 +59,22 @@ export default function QrScannerScreen() {
5859
(async () => {
5960
const applied = await devConfig.applyQrRaw(raw);
6061
if (!applied.ok) {
61-
Alert.alert('二维码内容不可用', `${applied.error}\n\n内容:${raw.slice(0, 256)}`);
62+
Alert.alert(t('qrScanner.invalidQRContent'), `${applied.error}\n\n${t('common.content')}: ${raw.slice(0, 256)}`);
6263
setScanned(false);
6364
return;
6465
}
6566

66-
// P2P 连接成功提示 (v2 架构)
6767
if (applied.isP2p) {
6868
Alert.alert(
69-
'P2P 连接配置成功',
70-
`局域网 IP: ${applied.config.host}\n端口: ${applied.config.port}\n角色: ${applied.config.characterName}\n\n请确保手机和电脑在同一 WiFi 下。`,
69+
t('qrScanner.p2pConfigSuccess'),
70+
t('qrScanner.p2pConfigMessage', {
71+
host: applied.config.host,
72+
port: applied.config.port,
73+
character: applied.config.characterName,
74+
}),
7175
[
7276
{
73-
text: '确定',
77+
text: t('common.ok'),
7478
onPress: () => {
7579
router.replace({
7680
pathname: target,
@@ -85,21 +89,20 @@ export default function QrScannerScreen() {
8589

8690
router.replace({
8791
pathname: target,
88-
// 兼容旧逻辑:仍传 qr 参数;但主逻辑已写入全局配置
8992
params: { qr: encodeURIComponent(raw) },
9093
});
9194
})();
9295
},
93-
[devConfig, router, returnTo, scanned]
96+
[devConfig, router, returnTo, scanned, t]
9497
);
9598

9699
if (!permission) {
97100
return (
98101
<View style={styles.center}>
99102
<Text style={styles.title}>{titleText}</Text>
100-
<Text style={styles.text}>正在检查相机权限…</Text>
103+
<Text style={styles.text}>{t('qrScanner.checkingPermission')}</Text>
101104
<Pressable style={styles.button} onPress={handleCancel}>
102-
<Text style={styles.buttonText}>返回</Text>
105+
<Text style={styles.buttonText}>{t('common.back')}</Text>
103106
</Pressable>
104107
</View>
105108
);
@@ -109,14 +112,14 @@ export default function QrScannerScreen() {
109112
return (
110113
<View style={styles.center}>
111114
<Text style={styles.title}>{titleText}</Text>
112-
<Text style={styles.text}>需要相机权限才能扫描二维码。</Text>
115+
<Text style={styles.text}>{t('qrScanner.cameraPermissionRequired')}</Text>
113116
<View style={{ height: 12 }} />
114117
<Pressable style={styles.button} onPress={() => requestPermission()}>
115-
<Text style={styles.buttonText}>授权相机权限</Text>
118+
<Text style={styles.buttonText}>{t('qrScanner.grantCameraPermission')}</Text>
116119
</Pressable>
117120
<View style={{ height: 12 }} />
118121
<Pressable style={[styles.button, styles.buttonSecondary]} onPress={handleCancel}>
119-
<Text style={styles.buttonText}>返回</Text>
122+
<Text style={styles.buttonText}>{t('common.back')}</Text>
120123
</Pressable>
121124
</View>
122125
);
@@ -130,19 +133,18 @@ export default function QrScannerScreen() {
130133
onBarcodeScanned={handleBarcodeScanned}
131134
/>
132135

133-
{/* 轻量遮罩与提示 */}
134136
<View pointerEvents="none" style={styles.overlay}>
135137
<Text style={styles.overlayTitle}>{titleText}</Text>
136-
<Text style={styles.overlayText}>对准二维码自动识别</Text>
138+
<Text style={styles.overlayText}>{t('qrScanner.autoDetectHint')}</Text>
137139
</View>
138140

139141
<View style={styles.bottomBar}>
140142
<Pressable style={[styles.button, styles.buttonSecondary]} onPress={handleCancel}>
141-
<Text style={styles.buttonText}>取消</Text>
143+
<Text style={styles.buttonText}>{t('common.cancel')}</Text>
142144
</Pressable>
143145
<View style={{ width: 12 }} />
144146
<Pressable style={styles.button} onPress={() => setScanned(false)}>
145-
<Text style={styles.buttonText}>{scanned ? '继续扫描' : '重新对焦'}</Text>
147+
<Text style={styles.buttonText}>{scanned ? t('qrScanner.continueScan') : t('qrScanner.refocus')}</Text>
146148
</Pressable>
147149
</View>
148150
</View>
@@ -213,5 +215,3 @@ const styles = StyleSheet.create({
213215
fontWeight: '600',
214216
},
215217
});
216-
217-

app/server-config.tsx

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from 'react-native';
2020
import { SafeAreaView } from 'react-native-safe-area-context';
2121
import { useRouter } from 'expo-router';
22+
import { useTranslation } from 'react-i18next';
2223
import { useDevConnectionConfig } from '@/hooks/useDevConnectionConfig';
2324
import AsyncStorage from '@react-native-async-storage/async-storage';
2425

@@ -38,6 +39,7 @@ const Icons = {
3839
export default function ServerConfigScreen() {
3940
const router = useRouter();
4041
const { config, isLoaded, setConfig } = useDevConnectionConfig();
42+
const { t } = useTranslation();
4143

4244
// Form state
4345
const [host, setHost] = useState('');
@@ -61,15 +63,15 @@ export default function ServerConfigScreen() {
6163

6264
// Validation
6365
if (!trimmedHost) {
64-
Alert.alert('错误', '请输入服务器地址');
66+
Alert.alert(t('common.error'), t('serverConfig.enterHost'));
6567
return;
6668
}
6769
if (!portNum || portNum < 1 || portNum > 65535) {
68-
Alert.alert('错误', '请输入有效的端口号 (1-65535)');
70+
Alert.alert(t('common.error'), t('serverConfig.enterValidPort'));
6971
return;
7072
}
7173
if (!characterName.trim()) {
72-
Alert.alert('错误', '请输入角色名称');
74+
Alert.alert(t('common.error'), t('serverConfig.enterCharacter'));
7375
return;
7476
}
7577

@@ -85,26 +87,30 @@ export default function ServerConfigScreen() {
8587
await setConfig(newConfig);
8688

8789
Alert.alert(
88-
'保存成功',
89-
`服务器地址已设置为:\n${trimmedHost}:${portNum}\n角色: ${characterName.trim()}`,
90-
[{ text: '确定', onPress: () => router.back() }]
90+
t('serverConfig.saved'),
91+
t('serverConfig.savedMessage', {
92+
host: trimmedHost,
93+
port: portNum,
94+
character: characterName.trim(),
95+
}),
96+
[{ text: t('common.ok'), onPress: () => router.back() }]
9197
);
9298
} catch (error) {
93-
Alert.alert('保存失败', String(error));
99+
Alert.alert(t('serverConfig.saveFailed'), String(error));
94100
} finally {
95101
setSaving(false);
96102
}
97-
}, [host, port, characterName, setConfig, router]);
103+
}, [host, port, characterName, setConfig, router, t]);
98104

99105
// Reset to default
100106
const handleReset = useCallback(() => {
101107
Alert.alert(
102-
'恢复默认',
103-
'确定要恢复默认配置吗?',
108+
t('serverConfig.resetDefault'),
109+
t('serverConfig.resetConfirm'),
104110
[
105-
{ text: '取消', style: 'cancel' },
111+
{ text: t('common.cancel'), style: 'cancel' },
106112
{
107-
text: '确定',
113+
text: t('common.confirm'),
108114
style: 'destructive',
109115
onPress: async () => {
110116
await AsyncStorage.removeItem(STORAGE_KEY);
@@ -115,7 +121,7 @@ export default function ServerConfigScreen() {
115121
},
116122
]
117123
);
118-
}, []);
124+
}, [t]);
119125

120126
// Quick fill common local IP patterns
121127
const quickFillLocalhost = () => {
@@ -132,7 +138,7 @@ export default function ServerConfigScreen() {
132138
return (
133139
<SafeAreaView style={styles.container}>
134140
<View style={styles.loadingContainer}>
135-
<Text style={styles.loadingText}>加载中...</Text>
141+
<Text style={styles.loadingText}>{t('common.loading')}</Text>
136142
</View>
137143
</SafeAreaView>
138144
);
@@ -149,7 +155,7 @@ export default function ServerConfigScreen() {
149155
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
150156
<Text style={styles.backButtonText}>{Icons.back}</Text>
151157
</TouchableOpacity>
152-
<Text style={styles.headerTitle}>服务器配置</Text>
158+
<Text style={styles.headerTitle}>{t('serverConfig.title')}</Text>
153159
<TouchableOpacity onPress={handleSave} disabled={saving} style={styles.saveButton}>
154160
<Text style={styles.saveButtonText}>{Icons.save}</Text>
155161
</TouchableOpacity>
@@ -158,17 +164,17 @@ export default function ServerConfigScreen() {
158164
<ScrollView style={styles.content} keyboardShouldPersistTaps="handled">
159165
{/* Instructions */}
160166
<View style={styles.instructionCard}>
161-
<Text style={styles.instructionTitle}>如何获取服务器地址</Text>
167+
<Text style={styles.instructionTitle}>{t('serverConfig.instructions')}</Text>
162168
<Text style={styles.instructionText}>
163-
1. 在电脑上启动 Nekotong 应用{'\n'}
164-
2. 在应用界面左上角查看"连接地址"{'\n'}
165-
3. 输入下方显示的 IP 地址和端口号
169+
{t('serverConfig.instruction1')}{'\n'}
170+
{t('serverConfig.instruction2')}{'\n'}
171+
{t('serverConfig.instruction3')}
166172
</Text>
167173
</View>
168174

169175
{/* Quick Fill Buttons */}
170176
<View style={styles.quickFillContainer}>
171-
<Text style={styles.quickFillLabel}>快速填充:</Text>
177+
<Text style={styles.quickFillLabel}>{t('serverConfig.quickFill')}</Text>
172178
<View style={styles.quickFillButtons}>
173179
<TouchableOpacity style={styles.quickFillButton} onPress={quickFillLocalhost}>
174180
<Text style={styles.quickFillButtonText}>localhost</Text>
@@ -183,30 +189,32 @@ export default function ServerConfigScreen() {
183189
<View style={styles.section}>
184190
<View style={styles.sectionHeader}>
185191
<Text style={styles.sectionIcon}>{Icons.server}</Text>
186-
<Text style={styles.sectionTitle}>服务器地址</Text>
192+
<Text style={styles.sectionTitle}>{t('serverConfig.serverAddress')}</Text>
187193
</View>
188194
<View style={styles.card}>
189195
<View style={styles.field}>
190-
<Text style={styles.label}>IP 地址 / 主机名</Text>
196+
<Text style={styles.label}>{t('serverConfig.ipHostname')}</Text>
191197
<TextInput
192198
style={styles.input}
193199
value={host}
194200
onChangeText={setHost}
195-
placeholder="例如: 192.168.1.100"
201+
placeholder={t('serverConfig.hostPlaceholder')}
202+
placeholderTextColor="#666"
196203
autoCapitalize="none"
197204
autoCorrect={false}
198205
keyboardType="default"
199206
/>
200-
<Text style={styles.hint}>输入 Nekotong 显示连接地址中的 IP 部分</Text>
207+
<Text style={styles.hint}>{t('serverConfig.ipHint')}</Text>
201208
</View>
202209

203210
<View style={styles.field}>
204-
<Text style={styles.label}>端口号</Text>
211+
<Text style={styles.label}>{t('serverConfig.portNumber')}</Text>
205212
<TextInput
206213
style={styles.input}
207214
value={port}
208215
onChangeText={setPort}
209-
placeholder="例如: 48911"
216+
placeholder={t('serverConfig.portPlaceholder')}
217+
placeholderTextColor="#666"
210218
keyboardType="number-pad"
211219
maxLength={5}
212220
/>
@@ -218,20 +226,21 @@ export default function ServerConfigScreen() {
218226
<View style={styles.section}>
219227
<View style={styles.sectionHeader}>
220228
<Text style={styles.sectionIcon}>{Icons.user}</Text>
221-
<Text style={styles.sectionTitle}>角色设置</Text>
229+
<Text style={styles.sectionTitle}>{t('serverConfig.characterSettings')}</Text>
222230
</View>
223231
<View style={styles.card}>
224232
<View style={styles.field}>
225-
<Text style={styles.label}>角色名称</Text>
233+
<Text style={styles.label}>{t('serverConfig.character')}</Text>
226234
<TextInput
227235
style={styles.input}
228236
value={characterName}
229237
onChangeText={setCharacterName}
230-
placeholder="例如: test"
238+
placeholder={t('serverConfig.characterPlaceholder')}
239+
placeholderTextColor="#666"
231240
autoCapitalize="none"
232241
autoCorrect={false}
233242
/>
234-
<Text style={styles.hint}>Nekotong 中配置的角色名</Text>
243+
<Text style={styles.hint}>{t('serverConfig.characterHint')}</Text>
235244
</View>
236245
</View>
237246
</View>
@@ -240,13 +249,13 @@ export default function ServerConfigScreen() {
240249
<View style={styles.section}>
241250
<View style={styles.sectionHeader}>
242251
<Text style={styles.sectionIcon}>{Icons.refresh}</Text>
243-
<Text style={styles.sectionTitle}>当前配置</Text>
252+
<Text style={styles.sectionTitle}>{t('serverConfig.currentConfig')}</Text>
244253
</View>
245254
<View style={styles.card}>
246255
{config.p2p ? (
247256
<>
248257
<View style={styles.infoRow}>
249-
<Text style={styles.infoLabel}>P2P 连接模式</Text>
258+
<Text style={styles.infoLabel}>{t('serverConfig.p2pMode')}</Text>
250259
</View>
251260
<Text style={styles.wsUrl}>
252261
{config.host}:{config.port}
@@ -258,7 +267,7 @@ export default function ServerConfigScreen() {
258267
) : (
259268
<>
260269
<View style={styles.infoRow}>
261-
<Text style={styles.infoLabel}>WebSocket URL</Text>
270+
<Text style={styles.infoLabel}>{t('serverConfig.websocketUrl')}</Text>
262271
</View>
263272
<Text style={styles.wsUrl}>
264273
ws://{config.host}:{config.port}/ws/{config.characterName}
@@ -276,22 +285,22 @@ export default function ServerConfigScreen() {
276285
disabled={saving}
277286
>
278287
<Text style={styles.primaryButtonText}>
279-
{saving ? '保存中...' : `${Icons.check} 保存配置`}
288+
{saving ? t('serverConfig.saving') : `${Icons.check} ${t('serverConfig.save')}`}
280289
</Text>
281290
</TouchableOpacity>
282291

283292
<TouchableOpacity style={styles.secondaryButton} onPress={handleReset}>
284-
<Text style={styles.secondaryButtonText}>恢复默认</Text>
293+
<Text style={styles.secondaryButtonText}>{t('serverConfig.resetDefault')}</Text>
285294
</TouchableOpacity>
286295
</View>
287296

288297
{/* Tips */}
289298
<View style={styles.tipsContainer}>
290-
<Text style={styles.tipsTitle}>💡 提示</Text>
299+
<Text style={styles.tipsTitle}>💡 {t('serverConfig.tips')}</Text>
291300
<Text style={styles.tipsText}>
292-
确保手机和电脑在同一 WiFi 网络下{'\n'}
293-
如果连接失败,请检查防火墙设置{'\n'}
294-
也可以使用扫码配置功能自动填充
301+
{t('serverConfig.tip1')}{'\n'}
302+
{t('serverConfig.tip2')}{'\n'}
303+
{t('serverConfig.tip3')}
295304
</Text>
296305
</View>
297306
</ScrollView>

0 commit comments

Comments
 (0)