feat: full Chinese (zh-Hans) localization — i18n all hardcoded strings, zh-CN download page
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user