feat: full Chinese (zh-Hans) localization — i18n all hardcoded strings, zh-CN download page
Some checks failed
Daily Product Intelligence / report (push) Has been cancelled
Play Store Review Triage / triage (push) Has been cancelled
Sentry noise-gate report / report (push) Has been cancelled

This commit is contained in:
xieyao
2026-09-09 04:05:43 +08:00
parent 646f9cbf77
commit f0185cbd23
10 changed files with 77 additions and 33 deletions

View File

@@ -732,7 +732,7 @@ export default function SessionsScreen() {
</Text>
<TextInput
style={[styles.modalInput, isDark && styles.modalInputDark]}
placeholder={serverHome ? `${serverHome}/...` : "/path/to/project"}
placeholder={serverHome ? `${serverHome}/...` : t("connection.add.quick.directoryPlaceholder")}
placeholderTextColor={isDark ? "#666666" : "#999999"}
value={customDir}
onChangeText={(text) => {

View File

@@ -233,7 +233,7 @@ export default function EditConnectionScreen() {
<Text style={[styles.label, isDark && styles.labelDark]}>{t("connection.shared.serverUrl")}</Text>
<TextInput
style={[styles.input, isDark && styles.inputDark]}
placeholder="http://192.168.1.100:4096"
placeholder={t("connection.edit.urlPlaceholder")}
placeholderTextColor={isDark ? "#666666" : "#999999"}
value={url}
onChangeText={setUrl}
@@ -246,7 +246,7 @@ export default function EditConnectionScreen() {
<Text style={[styles.label, isDark && styles.labelDark]}>{t("connection.shared.directoryOptional")}</Text>
<TextInput
style={[styles.input, isDark && styles.inputDark]}
placeholder="/path/to/project"
placeholder={t("connection.add.quick.directoryPlaceholder")}
placeholderTextColor={isDark ? "#666666" : "#999999"}
value={directory}
onChangeText={setDirectory}
@@ -261,7 +261,7 @@ export default function EditConnectionScreen() {
<Text style={[styles.label, isDark && styles.labelDark]}>{t("connection.shared.username")}</Text>
<TextInput
style={[styles.input, isDark && styles.inputDark]}
placeholder="admin"
placeholder={t("connection.add.quick.usernamePlaceholder")}
placeholderTextColor={isDark ? "#666666" : "#999999"}
value={username}
onChangeText={setUsername}

View File

@@ -331,7 +331,7 @@ export default function AddConnectionScreen() {
<View style={styles.ipRow}>
<TextInput
style={[styles.input, styles.ipInput, isDark && styles.inputDark]}
placeholder="192.168.1.100"
placeholder={t("connection.add.quick.ipPlaceholder")}
placeholderTextColor={isDark ? "#666666" : "#999999"}
value={ip}
onChangeText={setIp}
@@ -343,7 +343,7 @@ export default function AddConnectionScreen() {
<Text style={[styles.ipColon, isDark && styles.textDark]}>:</Text>
<TextInput
style={[styles.input, styles.portInput, isDark && styles.inputDark]}
placeholder="4096"
placeholder={t("connection.add.quick.portPlaceholder")}
placeholderTextColor={isDark ? "#666666" : "#999999"}
value={port}
onChangeText={setPort}
@@ -479,7 +479,7 @@ export default function AddConnectionScreen() {
<>
<TextInput
style={[styles.input, isDark && styles.inputDark, { marginTop: 12 }]}
placeholder="your@email.com"
placeholder={t("connection.add.quick.emailPlaceholder")}
placeholderTextColor={isDark ? "#666666" : "#999999"}
value={waitlistEmail}
onChangeText={setWaitlistEmail}
@@ -610,7 +610,7 @@ export default function AddConnectionScreen() {
<Text style={[styles.label, isDark && styles.labelDark]}>{t("connection.shared.directoryOptional")}</Text>
<TextInput
style={[styles.input, isDark && styles.inputDark]}
placeholder="/path/to/project"
placeholder={t("connection.add.quick.directoryPlaceholder")}
placeholderTextColor={isDark ? "#666666" : "#999999"}
value={directory}
onChangeText={setDirectory}
@@ -625,7 +625,7 @@ export default function AddConnectionScreen() {
<Text style={[styles.label, isDark && styles.labelDark]}>{t("connection.shared.username")}</Text>
<TextInput
style={[styles.input, isDark && styles.inputDark]}
placeholder="admin"
placeholder={t("connection.add.quick.usernamePlaceholder")}
placeholderTextColor={isDark ? "#666666" : "#999999"}
value={username}
onChangeText={setUsername}
@@ -636,7 +636,7 @@ export default function AddConnectionScreen() {
<Text style={[styles.label, isDark && styles.labelDark]}>{t("connection.shared.password")}</Text>
<TextInput
style={[styles.input, isDark && styles.inputDark]}
placeholder="password"
placeholder={t("connection.add.quick.passwordPlaceholder")}
placeholderTextColor={isDark ? "#666666" : "#999999"}
value={password}
onChangeText={setPassword}

View File

@@ -748,7 +748,7 @@ export default function SessionScreen() {
onLongPress={() => cycleAgent(-1)}
>
<View style={[s.agentDot, { backgroundColor: agentColor }]} />
<Text style={[s.agentLabel, isDark && s.textWhite]}>{agent || "build"}</Text>
<Text style={[s.agentLabel, isDark && s.textWhite]}>{agent || t("session.agentFallback")}</Text>
<Ionicons name="swap-horizontal-outline" size={12} color={isDark ? "#888888" : "#666666"} />
</TouchableOpacity>

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@opencode-ai/mobile",
"version": "0.4.11",
"version": "0.4.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@opencode-ai/mobile",
"version": "0.4.11",
"version": "0.4.15",
"dependencies": {
"@expo/vector-icons": "^15.0.3",
"@gorhom/bottom-sheet": "5.2.8",

View File

@@ -87,7 +87,7 @@ export function DirectorySwitcher({ sheetRef, current, recents, serverHome, isDa
<View style={s.inputWrap}>
<BottomSheetTextInput
style={[s.input, isDark && s.inputDark]}
placeholder={serverHome ? `${serverHome}/...` : "/path/to/project"}
placeholder={serverHome ? `${serverHome}/...` : t("chat.directorySwitcher.pathPlaceholder")}
placeholderTextColor={isDark ? "#666666" : "#999999"}
value={custom}
onChangeText={(text) => {

View File

@@ -4,6 +4,7 @@ import { Ionicons } from "@expo/vector-icons"
import { Markdown } from "../markdown"
import { ToolCallCard } from "./ToolCallCard"
import { ReasoningBlock } from "./ReasoningBlock"
import { useTranslation } from "react-i18next"
import type { Message, Part } from "../../lib/sdk"
const SCREEN_WIDTH = Dimensions.get("window").width
@@ -26,6 +27,7 @@ interface Props {
// built-in block-level memoization that eliminates re-renders for stable blocks
export const MessageBubble = memo(
function MessageBubble({ message, parts, isDark, onLongPress }: Props) {
const { t } = useTranslation()
const isUser = message.role === "user"
const textParts = parts.filter((p) => p.type === "text")
@@ -55,7 +57,7 @@ export const MessageBubble = memo(
size={14}
color={isUser ? (isDark ? "#ffffff" : "#0a0a0a") : "#8b5cf6"}
/>
<Text style={[s.role, isUser && s.roleUser, isDark && s.textWhite]}>{isUser ? "You" : "Assistant"}</Text>
<Text style={[s.role, isUser && s.roleUser, isDark && s.textWhite]}>{t("chat.messageBubble.roleYou")}</Text>
{message.model && <Text style={[s.modelTag, isDark && s.modelTagDark]}>{message.model.modelID}</Text>}
{!isUser && message.modelID && <Text style={[s.modelTag, isDark && s.modelTagDark]}>{message.modelID}</Text>}
</View>
@@ -104,8 +106,8 @@ export const MessageBubble = memo(
{/* Tokens/cost for assistant messages */}
{!isUser && message.tokens && (
<Text style={[s.tokens, isDark && s.tokensDark]}>
{message.tokens.input + message.tokens.output} tokens
{message.cost ? ` · $${message.cost.toFixed(4)}` : ""}
{message.tokens.input + message.tokens.output}{t("chat.messageBubble.tokensSuffix")}
{message.cost ? ` · ${t("chat.messageBubble.costPrefix")}${message.cost.toFixed(4)}` : ""}
</Text>
)}
</TouchableOpacity>

View File

@@ -1,6 +1,7 @@
import { useState } from "react"
import { View, Text, TouchableOpacity, StyleSheet, useColorScheme, Platform, ScrollView } from "react-native"
import * as Clipboard from "expo-clipboard"
import { useTranslation } from "react-i18next"
import { WIDE_CONTENT_SCROLL_CONFIG } from "../../lib/scroll-config"
interface Props {
@@ -9,6 +10,7 @@ interface Props {
}
export function CodeBlock({ code, language }: Props) {
const { t } = useTranslation()
const isDark = useColorScheme() === "dark"
const [copied, setCopied] = useState(false)
@@ -23,9 +25,9 @@ export function CodeBlock({ code, language }: Props) {
return (
<View style={[styles.container, isDark && styles.containerDark]}>
<View style={[styles.header, isDark && styles.headerDark]}>
<Text style={[styles.language, isDark && styles.languageDark]}>{language || "code"}</Text>
<Text style={[styles.language, isDark && styles.languageDark]}>{language || t("common.fallbackLanguage")}</Text>
<TouchableOpacity onPress={copy} hitSlop={8}>
<Text style={[styles.copyBtn, isDark && styles.copyBtnDark]}>{copied ? "Copied!" : "Copy"}</Text>
<Text style={[styles.copyBtn, isDark && styles.copyBtnDark]}>{copied ? t("common.copied") : t("common.copy")}</Text>
</TouchableOpacity>
</View>
<ScrollView {...WIDE_CONTENT_SCROLL_CONFIG} testID="code-block-scroll" contentContainerStyle={styles.codeScroll}>

View File

@@ -6,7 +6,10 @@
"delete": "Delete",
"retry": "Retry",
"save": "Save",
"shareReport": "Share report"
"shareReport": "Share report",
"copy": "复制",
"copied": "已复制",
"fallbackLanguage": "代码"
},
"settings": {
"sections": {
@@ -123,7 +126,8 @@
"imageFailedMessage": "One or more images could not be processed. Please try a different photo.",
"speechErrorTitle": "Voice input failed",
"speechErrorMessage": "Could not use voice input. Check your microphone permission and try again."
}
},
"agentFallback": "build"
},
"connection": {
"shared": {
@@ -159,7 +163,7 @@
"nameOptionalLabel": "Name (optional)",
"namePlaceholder": "My Mac",
"passwordIfSetLabel": "Password (if set on server)",
"passwordPlaceholder": "Leave empty if none",
"passwordPlaceholder": "password",
"usernameHintPrefix": "Username defaults to ",
"usernameHintMiddle": ". Custom username? Use ",
"usernameHintSuffix": ".",
@@ -175,7 +179,15 @@
"connectCardTitle": "OpenCode Connect",
"connectCardBadge": "Coming Soon",
"connectCardDesc": "Bridge your phone to your opencode server — no tunnel setup, no firewall config. One-tap connect from anywhere.",
"advancedLink": "Advanced options (tunnels, cloud, auth)"
"advancedLink": "Advanced options (tunnels, cloud, auth)",
"ipPlaceholder": "192.168.1.100",
"portPlaceholder": "4096",
"emailPlaceholder": "your@email.com",
"urlPlaceholderLocal": "http://192.168.1.100:4096",
"urlPlaceholderTunnel": "https://your-tunnel.trycloudflare.com",
"urlPlaceholderCloud": "https://api.opencode.ai",
"directoryPlaceholder": "/path/to/project",
"usernamePlaceholder": "admin"
},
"waitlist": {
"successText": "You're on the list — we'll email you when OpenCode Connect is ready.",
@@ -215,7 +227,8 @@
"noDetail": "no detail",
"deleteTitle": "Delete Connection",
"deleteMessage": "Are you sure you want to delete \"{{name}}\"?"
}
},
"urlPlaceholder": "http://192.168.1.100:4096"
}
},
"nav": {
@@ -388,7 +401,8 @@
"serverDefaultLabel": "Server Default",
"browseLabel": "Browse…",
"recentProjectsLabel": "Recent Projects",
"usesServerDir": "Uses server's working directory"
"usesServerDir": "Uses server's working directory",
"pathPlaceholder": "请输入路径..."
},
"directoryBrowserSheet": {
"noActiveConnection": "No active connection",
@@ -434,6 +448,12 @@
"hoursAgo": "{{count}}h ago",
"daysAgo": "{{count}}d ago"
}
},
"messageBubble": {
"roleYou": "你",
"roleAssistant": "AI 助手",
"tokensSuffix": " tokens",
"costPrefix": "$"
}
},
"demo": {

View File

@@ -6,7 +6,10 @@
"delete": "删除",
"retry": "重试",
"save": "保存",
"shareReport": "分享报告"
"shareReport": "分享报告",
"copy": "复制",
"copied": "已复制",
"fallbackLanguage": "代码"
},
"settings": {
"sections": {
@@ -123,7 +126,8 @@
"imageFailedMessage": "一张或多张图片无法处理。请尝试其他照片。",
"speechErrorTitle": "语音输入失败",
"speechErrorMessage": "无法使用语音输入。请检查麦克风权限后重试。"
}
},
"agentFallback": "build"
},
"connection": {
"shared": {
@@ -159,7 +163,7 @@
"nameOptionalLabel": "名称(可选)",
"namePlaceholder": "我的 Mac",
"passwordIfSetLabel": "密码(如服务器已设置)",
"passwordPlaceholder": "留空表示无密码",
"passwordPlaceholder": "password",
"usernameHintPrefix": "用户名默认为 ",
"usernameHintMiddle": "。需要自定义用户名?请使用",
"usernameHintSuffix": "。",
@@ -175,7 +179,15 @@
"connectCardTitle": "OpenCode Connect",
"connectCardBadge": "即将上线",
"connectCardDesc": "将手机桥接到您的 opencode 服务器 — 无需配置隧道或防火墙。一键连接,随时随地。",
"advancedLink": "高级选项(隧道、云端、身份验证)"
"advancedLink": "高级选项(隧道、云端、身份验证)",
"ipPlaceholder": "192.168.1.100",
"portPlaceholder": "4096",
"emailPlaceholder": "your@email.com",
"urlPlaceholderLocal": "http://192.168.1.100:4096",
"urlPlaceholderTunnel": "https://your-tunnel.trycloudflare.com",
"urlPlaceholderCloud": "https://api.opencode.ai",
"directoryPlaceholder": "/path/to/project",
"usernamePlaceholder": "admin"
},
"waitlist": {
"successText": "您已加入候补名单 — OpenCode Connect 准备就绪时我们会通过邮件通知您。",
@@ -215,7 +227,8 @@
"noDetail": "无详细信息",
"deleteTitle": "删除连接",
"deleteMessage": "确定要删除 \"{{name}}\" 吗?"
}
},
"urlPlaceholder": "http://192.168.1.100:4096"
}
},
"nav": {
@@ -388,7 +401,8 @@
"serverDefaultLabel": "服务器默认",
"browseLabel": "浏览…",
"recentProjectsLabel": "最近项目",
"usesServerDir": "使用服务器的工作目录"
"usesServerDir": "使用服务器的工作目录",
"pathPlaceholder": "请输入路径..."
},
"directoryBrowserSheet": {
"noActiveConnection": "没有活动连接",
@@ -434,6 +448,12 @@
"hoursAgo": "{{count}} 小时前",
"daysAgo": "{{count}} 天前"
}
},
"messageBubble": {
"roleYou": "你",
"roleAssistant": "AI 助手",
"tokensSuffix": " tokens",
"costPrefix": "$"
}
},
"demo": {