fix(mobile): harden localization catalog audit

This commit is contained in:
Brennan Benson
2026-08-01 06:46:51 -07:00
parent 02866adbd0
commit fb139ef64c
258 changed files with 17361 additions and 13850 deletions
+1 -1
View File
@@ -4,6 +4,6 @@
/mobile/locales/ @brennanb2025
/config/scripts/*localization*.mjs @brennanb2025
/config/scripts/*locale*.mjs @brennanb2025
/config/scripts/localize-mobile-strings.mjs @brennanb2025
/config/scripts/mobile-embedded-document-candidates.mjs @brennanb2025
/config/mobile-localization-coverage-allowlist.json @brennanb2025
/config/i18next.config.ts @brennanb2025
+40 -38
View File
@@ -8,6 +8,14 @@ import ts from 'typescript-api'
import { collectMobileEmbeddedDocumentCandidates } from './mobile-embedded-document-candidates.mjs'
import { classifyMobileStringNode } from './mobile-localization-candidate-rules.mjs'
import {
isAssignedToRenderedVariable,
isRenderedJsxExpression
} from './mobile-localization-rendered-variable.mjs'
import {
collectMobileTranslationBindings,
isMobileTranslationCall
} from './mobile-localization-translation-bindings.mjs'
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts'])
const SKIP_PATH_PARTS = new Set(['.git', 'dist', 'node_modules', 'out', '__snapshots__', 'assets'])
@@ -161,12 +169,15 @@ function stringParts(node) {
]
}
function isInsideLocalizationCall(node) {
function isInsideLocalizationCall(node, sourceFile, mobileBindings) {
let current = node.parent
while (current) {
if (ts.isCallExpression(current)) {
if (mobileBindings && isMobileTranslationCall(current, sourceFile, mobileBindings)) {
return true
}
const name = expressionNameText(current.expression)
if (name && LOCALIZATION_CALL_NAMES.has(name.split('.').at(-1) ?? name)) {
if (!mobileBindings && name && LOCALIZATION_CALL_NAMES.has(name.split('.').at(-1) ?? name)) {
return true
}
}
@@ -175,6 +186,24 @@ function isInsideLocalizationCall(node) {
return false
}
function isUnboundRenderedTranslationArgument(node, sourceFile, mobileBindings) {
const call = findAncestor(node, ts.isCallExpression)
if (!call || !ts.isIdentifier(call.expression)) {
return false
}
const name = call.expression.text
const resemblesTranslator =
LOCALIZATION_CALL_NAMES.has(name) ||
mobileBindings.translatorNames.has(name) ||
mobileBindings.fixedTranslatorNames.has(name) ||
mobileBindings.prefixedTranslatorNames.has(name)
return (
resemblesTranslator &&
!isMobileTranslationCall(call, sourceFile, mobileBindings) &&
(isRenderedJsxExpression(call) || isAssignedToRenderedVariable(call))
)
}
function isJsxAttributeValue(node) {
const parent = node.parent
if (!parent) {
@@ -209,40 +238,6 @@ function ancestorJsxAttributeName(node) {
return undefined
}
function isRenderedJsxExpression(node) {
let current = node.parent
while (current) {
if (ts.isJsxExpression(current)) {
return (
ts.isJsxElement(current.parent) ||
ts.isJsxFragment(current.parent) ||
ts.isJsxSelfClosingElement(current.parent)
)
}
if (
ts.isConditionalExpression(current) ||
ts.isParenthesizedExpression(current) ||
ts.isTemplateExpression(current) ||
ts.isNoSubstitutionTemplateLiteral(current)
) {
if (ts.isConditionalExpression(current) && current.condition === node) {
return false
}
current = current.parent
continue
}
if (ts.isBinaryExpression(current)) {
if (current.operatorToken.kind !== ts.SyntaxKind.PlusToken) {
return false
}
current = current.parent
continue
}
return false
}
return false
}
function nearestObjectPropertyName(node) {
let current = node.parent
while (current) {
@@ -406,10 +401,14 @@ export function collectLocalizationCandidates(filePath, sourceText, root = proce
/^mobile\/src\/(?:agent-history|browser|components|dictation|files|hooks|session|source-control|tasks)\//.test(
relativePath
)
const mobileBindings = mobileSource ? collectMobileTranslationBindings(sourceFile) : undefined
function pushReport(node, kind, text, dynamic = false) {
const value = compactText(text)
if (!hasHumanLanguageText(value) || isInsideLocalizationCall(node)) {
if (
!hasHumanLanguageText(value) ||
isInsideLocalizationCall(node, sourceFile, mobileBindings)
) {
return
}
const position = lineAndColumn(sourceFile, node)
@@ -433,7 +432,10 @@ export function collectLocalizationCandidates(filePath, sourceText, root = proce
}
const kind = mobileSource
? classifyMobileStringNode(node, userVisibleErrorSource)
? (classifyMobileStringNode(node, userVisibleErrorSource) ??
(isUnboundRenderedTranslationArgument(node, sourceFile, mobileBindings)
? 'unbound-localization-call'
: undefined))
: classifyStringNode(node)
if (kind) {
for (const part of stringParts(node)) {
@@ -4,7 +4,7 @@ import ts from 'typescript-api'
const EMBEDDED_DOCUMENT_FILE_RE = /(?:-html|webview-html)\.[cm]?[jt]sx?$/
const USER_VISIBLE_ATTRIBUTE_RE =
/\b(aria-description|aria-label|alt|data-placeholder|placeholder|title)\s*=\s*(["'])(.*?)\2/gi
const USER_VISIBLE_PROMPT_RE = /\b(?:window\.)?(alert|confirm|prompt)\(\s*(["'])(.*?)\2/g
const USER_VISIBLE_PROMPT_START_RE = /\b(?:window\.)?(alert|confirm|prompt)\(\s*/g
const INSERTED_HTML_CALL_RE = /\bexecCommand\(\s*(["'])insertHTML\1\s*,\s*false\s*,/g
function maskNonMarkupContent(documentText) {
@@ -17,33 +17,57 @@ function capturedOffset(match, capturedValue) {
return (match.index ?? 0) + match[0].indexOf(capturedValue)
}
function collectTextCandidates(value, baseOffset, kind, candidates) {
if (!value.includes('${')) {
candidates.push({
start: baseOffset,
end: baseOffset + value.length,
kind,
text: value,
dynamic: false
})
return
}
let cursor = 0
while (cursor < value.length) {
const interpolationStart = value.indexOf('${', cursor)
const segmentEnd = interpolationStart === -1 ? value.length : interpolationStart
if (segmentEnd > cursor) {
candidates.push({
start: baseOffset + cursor,
end: baseOffset + segmentEnd,
kind,
text: value.slice(cursor, segmentEnd),
dynamic: true
})
}
if (interpolationStart === -1) {
break
}
cursor = skipTemplateInterpolation(value, interpolationStart)
}
}
function collectMarkupCandidates(markup, baseOffset, candidates) {
for (const match of markup.matchAll(USER_VISIBLE_ATTRIBUTE_RE)) {
const value = match[3]
if (!value.includes('${')) {
const start = baseOffset + capturedOffset(match, value)
candidates.push({
start,
end: start + value.length,
kind: `embedded-html-attribute:${match[1].toLowerCase()}`,
text: value,
dynamic: false
})
}
collectTextCandidates(
value,
baseOffset + capturedOffset(match, value),
`embedded-html-attribute:${match[1].toLowerCase()}`,
candidates
)
}
for (const match of markup.matchAll(/>([^<]+)</g)) {
const value = match[1]
if (!value.includes('${')) {
const start = baseOffset + capturedOffset(match, value)
candidates.push({
start,
end: start + value.length,
kind: 'embedded-html-text',
text: value,
dynamic: false
})
}
collectTextCandidates(
value,
baseOffset + capturedOffset(match, value),
'embedded-html-text',
candidates
)
}
}
@@ -56,13 +80,7 @@ function collectMarkupFragmentCandidates(markup, baseOffset, candidates) {
}
const value = match[1]
const start = baseOffset + capturedOffset(match, value)
candidates.push({
start,
end: start + value.length,
kind: 'embedded-html-text',
text: value,
dynamic: false
})
collectTextCandidates(value, start, 'embedded-html-text', candidates)
}
}
@@ -100,6 +118,63 @@ function skipTemplateInterpolation(source, start) {
return cursor
}
function templateLiteralEnd(source, start) {
let cursor = start + 1
while (cursor < source.length) {
if (source[cursor] === '\\') {
cursor += 2
} else if (source.startsWith('${', cursor)) {
cursor = skipTemplateInterpolation(source, cursor)
} else if (source[cursor] === '`') {
return cursor + 1
} else {
cursor += 1
}
}
return source.length
}
function escapedTemplateLiteralEnd(source, start) {
let cursor = start + 2
while (cursor < source.length) {
if (source.startsWith('${', cursor)) {
cursor = skipTemplateInterpolation(source, cursor)
} else if (source.startsWith('\\`', cursor)) {
return cursor + 2
} else if (source[cursor] === '\\') {
cursor += 2
} else {
cursor += 1
}
}
return source.length
}
function collectPromptCandidates(documentText, baseOffset, candidates) {
for (const match of documentText.matchAll(USER_VISIBLE_PROMPT_START_RE)) {
const argumentStart = (match.index ?? 0) + match[0].length
const quote = documentText[argumentStart]
const escapedTemplate = quote === '\\' && documentText[argumentStart + 1] === '`'
if (quote !== '"' && quote !== "'" && quote !== '`' && !escapedTemplate) {
continue
}
const argumentEnd = escapedTemplate
? escapedTemplateLiteralEnd(documentText, argumentStart)
: quote === '`'
? templateLiteralEnd(documentText, argumentStart)
: skipQuotedString(documentText, argumentStart)
const valueStart = argumentStart + (escapedTemplate ? 2 : 1)
const valueEnd = argumentEnd - (escapedTemplate ? 2 : 1)
const value = documentText.slice(valueStart, valueEnd)
collectTextCandidates(
value,
baseOffset + valueStart,
`embedded-web-${match[1].toLowerCase()}`,
candidates
)
}
}
function insertedHtmlArgumentEnd(source, start) {
let depth = 1
let cursor = start
@@ -165,17 +240,7 @@ export function collectMobileEmbeddedDocumentCandidates(filePath, sourceText) {
const baseOffset = node.getStart(sourceFile) + 1
collectMarkupCandidates(maskNonMarkupContent(documentText), baseOffset, candidates)
for (const match of documentText.matchAll(USER_VISIBLE_PROMPT_RE)) {
const value = match[3]
const start = baseOffset + capturedOffset(match, value)
candidates.push({
start,
end: start + value.length,
kind: `embedded-web-${match[1].toLowerCase()}`,
text: value,
dynamic: false
})
}
collectPromptCandidates(documentText, baseOffset, candidates)
for (const match of documentText.matchAll(INSERTED_HTML_CALL_RE)) {
const argumentStart = (match.index ?? 0) + match[0].length
@@ -41,6 +41,26 @@ export const HTML = \`<!doctype html>
])
})
it('finds static copy around dynamic WebView values and prompts', () => {
const source = [
'export const HTML = `<!doctype html>',
'<button title="Copy ${name}">Retry ${name}</button>',
'<script>window.alert(\\`Could not load ${name}\\`);</script>',
'`'
].join('\n')
const candidates = collectLocalizationCandidates(
'/repo/mobile/src/components/example-webview-html.ts',
source,
'/repo'
)
expect(candidates.map(({ dynamic, text }) => ({ dynamic, text }))).toEqual([
{ dynamic: true, text: 'Copy' },
{ dynamic: true, text: 'Retry' },
{ dynamic: true, text: 'Could not load' }
])
})
it('finds literals assigned to variables that later render in JSX', () => {
const source = `
export function Example({ alternate }) {
@@ -81,4 +101,21 @@ export function toRow(item) {
expect(candidates.map((candidate) => candidate.text)).toEqual(['(no commit message)'])
})
it('recognizes aliased translators and reports calls shadowed by parameters', () => {
const source = `
import { t as translateMobile } from '@/i18n/mobile-i18n'
const localized = translateMobile('example.localized')
export function Example(translateMobile) {
return <Text>{translateMobile('Unlocalized shadowed copy')}</Text>
}
`
const candidates = collectLocalizationCandidates(
'/repo/mobile/src/components/Example.tsx',
source,
'/repo'
)
expect(candidates.map((candidate) => candidate.text)).toEqual(['Unlocalized shadowed copy'])
})
})
@@ -0,0 +1,201 @@
// TypeScript 7 is a native CLI; AST consumers still need the legacy JavaScript API.
import ts from 'typescript-api'
const MOBILE_I18N_MODULE_RE = /(?:^|\/)mobile-i18n$/
function bindingNames(name, names = []) {
if (!name) {
return names
}
if (ts.isIdentifier(name)) {
names.push(name.text)
} else {
for (const element of name.elements) {
bindingNames(element.name, names)
}
}
return names
}
function lexicalScope(node, sourceFile) {
let current = node.parent
while (current && current !== sourceFile) {
if (ts.isBlock(current) || ts.isSourceFile(current) || ts.isFunctionLike(current)) {
return current
}
current = current.parent
}
return sourceFile
}
function functionScope(node, sourceFile) {
let current = node.parent
while (current && current !== sourceFile) {
if (ts.isFunctionLike(current)) {
return current
}
current = current.parent
}
return sourceFile
}
function addDeclarations(sourceFile) {
const declarations = new Map()
function add(scope, name) {
const names = declarations.get(scope) ?? new Set()
names.add(name)
declarations.set(scope, names)
}
function visit(node) {
if (ts.isParameter(node)) {
for (const name of bindingNames(node.name)) {
add(node.parent, name)
}
} else if (ts.isVariableDeclaration(node)) {
const declarationList = node.parent
const scope =
ts.isVariableDeclarationList(declarationList) &&
(declarationList.flags & ts.NodeFlags.BlockScoped) === 0
? functionScope(node, sourceFile)
: lexicalScope(node, sourceFile)
for (const name of bindingNames(node.name)) {
add(scope, name)
}
} else if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) && node.name) {
add(lexicalScope(node, sourceFile), node.name.text)
} else if (ts.isCatchClause(node) && node.variableDeclaration) {
for (const name of bindingNames(node.variableDeclaration.name)) {
add(node, name)
}
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return declarations
}
function isShadowed(identifier, sourceFile, declarations) {
let current = identifier.parent
while (current && current !== sourceFile) {
if (declarations.get(current)?.has(identifier.text)) {
return true
}
current = current.parent
}
return false
}
function isEnglishFixedTranslator(node, instanceNames, sourceFile, declarations) {
return (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
ts.isIdentifier(node.expression.expression) &&
instanceNames.has(node.expression.expression.text) &&
!isShadowed(node.expression.expression, sourceFile, declarations) &&
node.expression.name.text === 'getFixedT' &&
node.arguments.length === 1 &&
ts.isStringLiteralLike(node.arguments[0]) &&
node.arguments[0].text === 'en'
)
}
function mobileTranslatorPrefix(node, factoryNames, sourceFile, declarations) {
if (
!ts.isCallExpression(node) ||
!ts.isIdentifier(node.expression) ||
!factoryNames.has(node.expression.text) ||
isShadowed(node.expression, sourceFile, declarations) ||
node.arguments.length !== 1 ||
!ts.isStringLiteralLike(node.arguments[0])
) {
return undefined
}
return node.arguments[0].text
}
export function collectMobileTranslationBindings(sourceFile) {
const translatorNames = new Set()
const translatorFactoryNames = new Set()
const instanceNames = new Set()
const fixedTranslatorNames = new Set()
const prefixedTranslatorNames = new Map()
const declarations = addDeclarations(sourceFile)
for (const statement of sourceFile.statements) {
if (
!ts.isImportDeclaration(statement) ||
!ts.isStringLiteralLike(statement.moduleSpecifier) ||
!MOBILE_I18N_MODULE_RE.test(statement.moduleSpecifier.text)
) {
continue
}
for (const element of statement.importClause?.namedBindings?.elements ?? []) {
const importedName = element.propertyName?.text ?? element.name.text
if (importedName === 't') {
translatorNames.add(element.name.text)
} else if (importedName === 'createMobileTranslator') {
translatorFactoryNames.add(element.name.text)
} else if (importedName === 'mobileI18n') {
instanceNames.add(element.name.text)
}
}
}
function collectFixed(node) {
if (
ts.isVariableDeclaration(node) &&
ts.isIdentifier(node.name) &&
node.initializer &&
isEnglishFixedTranslator(node.initializer, instanceNames, sourceFile, declarations)
) {
fixedTranslatorNames.add(node.name.text)
}
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
const prefix = mobileTranslatorPrefix(
node.initializer,
translatorFactoryNames,
sourceFile,
declarations
)
if (prefix !== undefined) {
prefixedTranslatorNames.set(node.name.text, prefix)
}
}
ts.forEachChild(node, collectFixed)
}
collectFixed(sourceFile)
return {
declarations,
fixedTranslatorNames,
instanceNames,
prefixedTranslatorNames,
translatorNames
}
}
export function mobileTranslationCallPrefix(call, sourceFile, bindings) {
const callee = call.expression
if (ts.isIdentifier(callee)) {
if (isShadowed(callee, sourceFile, bindings.declarations)) {
return undefined
}
if (
bindings.translatorNames.has(callee.text) ||
bindings.fixedTranslatorNames.has(callee.text)
) {
return ''
}
return bindings.prefixedTranslatorNames.get(callee.text)
}
return isEnglishFixedTranslator(callee, bindings.instanceNames, sourceFile, bindings.declarations)
? ''
: undefined
}
export function isMobileTranslationCall(call, sourceFile, bindings) {
return mobileTranslationCallPrefix(call, sourceFile, bindings) !== undefined
}
@@ -6,6 +6,12 @@ import process from 'node:process'
// TypeScript 7 is a native CLI; AST consumers still need the legacy JavaScript API.
import ts from 'typescript-api'
import {
collectMobileTranslationBindings,
isMobileTranslationCall,
mobileTranslationCallPrefix
} from './mobile-localization-translation-bindings.mjs'
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])
const SOURCE_RELATIVE_ROOTS = [path.join('mobile', 'app'), path.join('mobile', 'src')]
const LOCALES_RELATIVE_DIR = path.join('mobile', 'src', 'i18n', 'locales')
@@ -46,10 +52,69 @@ const PROTECTED_LITERALS = [
'WSL',
'SSH',
'HEAD',
'MR !'
'MR !',
'HOOKS'
]
const PLACEHOLDER_RE = /\{\{\s*([^,}\s]+)(?:,[^}]*)?\}\}/g
const ENCODED_HTML_ENTITY_RE = /&(?:amp|apos|gt|lt|quot);/i
const INTENT_MESSAGE_ID_RE = /^[a-z][A-Za-z0-9]*(?:\.[a-z][A-Za-z0-9]*)+$/
const POSITIONAL_PLACEHOLDER_RE = /^value\d+$/
const LANGUAGE_NEUTRAL_VALUES = new Set([
'@orca_build',
'[x]',
'Aider',
'Amp',
'Ante',
'Antigravity',
'Auggie',
'Autohand Code',
'Charm',
'Claude',
'Claude Agent Teams',
'Cline',
'Codebuff',
'Codex',
'Command Code',
'Continue',
'Cursor',
'Devin',
'Droid',
'Gemini',
'GitHub',
'GitHub Copilot',
'GitHub Releases',
'GitLab',
'Goose',
'Grok',
'HEAD',
'Hermes',
'Kilocode',
'Kimi',
'Kiro',
'Linear',
'Markdown',
'MiMo Code',
'Mistral Vibe',
'MR',
'MR !',
'OMP',
'onOrca.dev',
'OpenAI API',
'OpenClaude',
'OpenClaw',
'OpenCode',
'Orca',
'Orca Relay',
'orca.yaml',
'ORCA.YAML',
'Pi',
'PR',
'Qwen Code',
'Rovo Dev',
'stablyai/orca',
'Trae',
'YYYY-MM-DD'
])
function normalizePath(root, filePath) {
return path.relative(root, filePath).split(path.sep).join('/')
@@ -193,6 +258,39 @@ function verifyProtectedLiterals(englishValue, localeValue, locale, key) {
return issues
}
function isLanguageNeutralValue(value) {
const withoutPlaceholders = value.replace(PLACEHOLDER_RE, '').trim()
return (
LANGUAGE_NEUTRAL_VALUES.has(value) ||
/^(?:Alt|Ctrl|Shift)(?:\+(?:[A-Z]|Tab))?$/.test(value) ||
/^(?:Del|Enter|Esc|Ins|PgDn|PgUp|Tab)$/.test(value) ||
/^(?:F|H)\d{1,2}$/.test(value) ||
/^(?:https?:\/\/|orca:\/\/|lin_api_)/.test(value) ||
/^(?:npm run dev|Linux: sudo ufw allow 6768|src\/renderer packages\/ui)$/.test(value) ||
withoutPlaceholders === '' ||
withoutPlaceholders === '..HEAD' ||
withoutPlaceholders === '×'
)
}
function verifyTerminology(englishValue, localeValue, locale, key) {
const issues = []
if (
locale === 'es' &&
/\bbranch(?:es)?\b/i.test(englishValue) &&
/\bsucursal(?:es)?\b/i.test(localeValue)
) {
issues.push(`${locale}.json Git terminology mismatch: ${key} translates branch as sucursal`)
}
if (locale === 'zh' && /\bhost\b/i.test(englishValue) && localeValue.includes('主人')) {
issues.push(`${locale}.json host terminology mismatch: ${key} uses 主人`)
}
if (locale === 'zh' && /\bagent/i.test(englishValue) && localeValue.includes('检测剂')) {
issues.push(`${locale}.json agent terminology mismatch: ${key} uses 检测剂`)
}
return issues
}
export function collectMobileTranslationCalls(filePath, sourceText, root = process.cwd()) {
const sourceKind =
filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
@@ -204,47 +302,17 @@ export function collectMobileTranslationCalls(filePath, sourceText, root = proce
sourceKind
)
const calls = []
const fixedTranslatorNames = new Set()
function isEnglishFixedTranslator(node) {
const expression = unwrapExpression(node)
return (
ts.isCallExpression(expression) &&
ts.isPropertyAccessExpression(expression.expression) &&
ts.isIdentifier(expression.expression.expression) &&
expression.expression.expression.text === 'mobileI18n' &&
expression.expression.name.text === 'getFixedT' &&
expression.arguments.length === 1 &&
ts.isStringLiteralLike(expression.arguments[0]) &&
expression.arguments[0].text === 'en'
)
}
function collectFixedTranslators(node) {
if (
ts.isVariableDeclaration(node) &&
ts.isIdentifier(node.name) &&
node.initializer &&
isEnglishFixedTranslator(node.initializer)
) {
fixedTranslatorNames.add(node.name.text)
}
ts.forEachChild(node, collectFixedTranslators)
}
collectFixedTranslators(sourceFile)
const bindings = collectMobileTranslationBindings(sourceFile)
function visit(node) {
if (
ts.isCallExpression(node) &&
((ts.isIdentifier(node.expression) &&
(node.expression.text === 't' || fixedTranslatorNames.has(node.expression.text))) ||
isEnglishFixedTranslator(node.expression))
) {
if (ts.isCallExpression(node) && isMobileTranslationCall(node, sourceFile, bindings)) {
const keyExpression = node.arguments[0]
if (keyExpression) {
const prefix = mobileTranslationCallPrefix(node, sourceFile, bindings)
const branches = collectKeyBranches(keyExpression)
calls.push({
...collectKeyBranches(keyExpression),
...branches,
keys: branches.keys.map((key) => (prefix ? `${prefix}.${key}` : key)),
location: locationFor(root, filePath, sourceFile, keyExpression),
options: collectOptionNames(node.arguments[1]),
source: keyExpression.getText(sourceFile)
@@ -290,6 +358,25 @@ function verifyCalls(calls, englishEntries) {
return issues
}
function verifyEnglishEntries(calls, englishEntries) {
const issues = []
const referencedKeys = new Set(calls.flatMap((call) => (call.inspected ? call.keys : [])))
for (const [key, value] of englishEntries) {
if (!INTENT_MESSAGE_ID_RE.test(key) || key.startsWith('m.')) {
issues.push(`en.json message ID must be intent-named: ${key}`)
}
for (const placeholder of collectPlaceholderNames(value)) {
if (POSITIONAL_PLACEHOLDER_RE.test(placeholder)) {
issues.push(`en.json placeholder must be intent-named: ${key} uses ${placeholder}`)
}
}
if (!referencedKeys.has(key)) {
issues.push(`en.json has orphaned key: ${key}`)
}
}
return issues
}
function verifyLocaleEntries(englishEntries, locale, localeEntries) {
const issues = []
const localeKeys = [...localeEntries.keys()].sort()
@@ -307,6 +394,10 @@ function verifyLocaleEntries(englishEntries, locale, localeEntries) {
issues.push(`${locale}.json placeholder mismatch: ${key}`)
}
issues.push(...verifyProtectedLiterals(englishValue, localeValue, locale, key))
issues.push(...verifyTerminology(englishValue, localeValue, locale, key))
if (localeValue === englishValue && !isLanguageNeutralValue(englishValue)) {
issues.push(`${locale}.json copies English instead of using fallback: ${key}`)
}
}
return issues
@@ -476,6 +567,7 @@ export async function main(root = process.cwd()) {
}
issues.push(...verifyCalls(calls, englishEntries))
issues.push(...verifyEnglishEntries(calls, englishEntries))
for (const locale of MOBILE_LOCALES.slice(1)) {
const localeEntries = catalogs.get(locale)
if (localeEntries) {
@@ -4,7 +4,10 @@ import path from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { main as verifyMobileCatalog } from './verify-mobile-localization-catalog.mjs'
import {
collectMobileTranslationCalls,
main as verifyMobileCatalog
} from './verify-mobile-localization-catalog.mjs'
const LOCALES = ['en', 'es', 'ja', 'ko', 'zh']
const NATIVE_LOCALES = ['en', 'es', 'ja', 'ko', 'zh-Hans']
@@ -62,7 +65,10 @@ function makeProject({ sourceText, catalogs, nativeCatalogs, appConfig }) {
writeJson(path.join(root, 'mobile', 'app.json'), appConfig ?? defaultAppConfig())
for (const locale of LOCALES) {
writeJson(path.join(localeDirectory, `${locale}.json`), catalogs?.[locale] ?? catalogs.en)
writeJson(
path.join(localeDirectory, `${locale}.json`),
catalogs?.[locale] ?? (locale === 'en' ? catalogs.en : {})
)
}
for (const locale of NATIVE_LOCALES) {
writeJson(
@@ -85,10 +91,10 @@ async function runFailedVerification(root) {
describe('verify-mobile-localization-catalog', () => {
it('verifies literal and conditional keys with matching options', async () => {
const catalog = { m: { greeting: 'Hello {{name}}', farewell: 'Bye {{name}}' } }
const catalog = { example: { greeting: 'Hello {{name}}', farewell: 'Bye {{name}}' } }
const root = makeProject({
sourceText:
"import { t } from '@/i18n/mobile-i18n'\nconst name = 'Orca'\nexport const label = t(name ? 'm.greeting' : 'm.farewell', { name })\n",
"import { t } from '@/i18n/mobile-i18n'\nconst name = 'Orca'\nexport const label = t(name ? 'example.greeting' : 'example.farewell', { name })\n",
catalogs: { en: catalog }
})
@@ -115,6 +121,43 @@ describe('verify-mobile-localization-catalog', () => {
expect(await runFailedVerification(root)).toContain('missing English key: m.missing')
})
it('tracks aliased imports without treating shadowed names as translators', async () => {
const sourceText = `
import { t as translateMobile } from '@/i18n/mobile-i18n'
export const label = translateMobile('example.known')
export function local(translateMobile) {
return translateMobile('not-a-translation-key')
}
`
const calls = collectMobileTranslationCalls('/repo/mobile/app/Example.tsx', sourceText, '/repo')
expect(calls.map((call) => call.keys)).toEqual([['example.known']])
})
it('expands statically prefixed translators without accepting shadowed calls', () => {
const sourceText = `
import { createMobileTranslator as makeTranslator } from '@/i18n/mobile-i18n'
const translate = makeTranslator('example')
export const label = translate('known')
export function local(translate) {
return translate('not-a-translation-key')
}
`
const calls = collectMobileTranslationCalls('/repo/mobile/app/Example.tsx', sourceText, '/repo')
expect(calls.map((call) => call.keys)).toEqual([['example.known']])
})
it('ignores unrelated local t functions', () => {
const calls = collectMobileTranslationCalls(
'/repo/mobile/app/Example.tsx',
"function t(value) { return value }\nexport const label = t('not-a-translation-key')\n",
'/repo'
)
expect(calls).toEqual([])
})
it('rejects translation keys that cannot be statically inspected', async () => {
const root = makeProject({
sourceText: "import { t } from '@/i18n/mobile-i18n'\nexport const label = t(runtimeKey)\n",
@@ -137,19 +180,64 @@ describe('verify-mobile-localization-catalog', () => {
})
it('allows missing translations in sparse locale catalogs', async () => {
const en = { m: { greeting: 'Hello {{name}}', farewell: 'Bye' } }
const en = { example: { greeting: 'Hello {{name}}' } }
const root = makeProject({
sourceText:
"import { t } from '@/i18n/mobile-i18n'\nexport const label = t('m.greeting', { name: 'Orca' })\n",
"import { t } from '@/i18n/mobile-i18n'\nexport const label = t('example.greeting', { name: 'Orca' })\n",
catalogs: {
en,
es: { m: { greeting: 'Hola {{name}}' } }
es: { example: { greeting: 'Hola {{name}}' } }
}
})
await expect(verifyMobileCatalog(root)).resolves.toBe(0)
})
it('rejects opaque IDs, positional placeholders, and orphaned English keys', async () => {
const root = makeProject({
sourceText:
"import { t } from '@/i18n/mobile-i18n'\nexport const label = t('m.opaque', { value0: 'Orca' })\n",
catalogs: { en: { m: { opaque: 'Hello {{value0}}', orphan: 'Unused' } } }
})
const report = await runFailedVerification(root)
expect(report).toContain('message ID must be intent-named: m.opaque')
expect(report).toContain('placeholder must be intent-named: m.opaque uses value0')
expect(report).toContain('en.json has orphaned key: m.orphan')
})
it('rejects copied English target entries unless they are language-neutral', async () => {
const root = makeProject({
sourceText:
"import { t } from '@/i18n/mobile-i18n'\nexport const label = t('example.downloadFailed')\n",
catalogs: {
en: { example: { downloadFailed: 'Download failed' } },
ja: { example: { downloadFailed: 'Download failed' } }
}
})
expect(await runFailedVerification(root)).toContain(
'ja.json copies English instead of using fallback: example.downloadFailed'
)
})
it('enforces Git and agent terminology', async () => {
const root = makeProject({
sourceText:
"import { t } from '@/i18n/mobile-i18n'\nexport const branch = t('example.branch')\nexport const host = t('example.host')\n",
catalogs: {
en: { example: { branch: 'Switch branch', host: 'Detecting agents on host' } },
es: { example: { branch: 'Cambiar sucursal' } },
zh: { example: { host: '主人正在检测剂' } }
}
})
const report = await runFailedVerification(root)
expect(report).toContain('translates branch as sucursal')
expect(report).toContain('uses 主人')
expect(report).toContain('uses 检测剂')
})
it('validates placeholders and extra keys in present translations', async () => {
const en = { m: { greeting: 'Hello {{name}}' } }
const root = makeProject({
+6 -6
View File
@@ -47,13 +47,13 @@ export default function AboutScreen() {
<Pressable style={styles.backButton} onPress={() => router.back()}>
<ChevronLeft size={22} color={colors.textSecondary} />
</Pressable>
<Text style={styles.heading}>{t('m.qBqOdtg')}</Text>
<Text style={styles.heading}>{t('about.about')}</Text>
</View>
<View style={styles.brand}>
<OrcaLogo size={28} />
<Text style={styles.brandName}>{t('m.o1zkACA')}</Text>
<Text style={styles.brandSub}>{t('m.wRf8z4A')}</Text>
<Text style={styles.brandName}>{t('about.orca')}</Text>
<Text style={styles.brandSub}>{t('about.open')}</Text>
</View>
<View style={styles.section}>
@@ -62,7 +62,7 @@ export default function AboutScreen() {
onPress={() => void Linking.openURL('https://onOrca.dev')}
>
<Globe size={16} color={colors.textSecondary} />
<Text style={styles.rowValue}>{t('m.1sjQKuE')}</Text>
<Text style={styles.rowValue}>{t('about.orcaDev')}</Text>
</Pressable>
<View style={styles.separator} />
<Pressable
@@ -70,7 +70,7 @@ export default function AboutScreen() {
onPress={() => void Linking.openURL('https://github.com/stablyai/orca')}
>
<GithubIcon />
<Text style={styles.rowValue}>{t('m.qfIlnqY')}</Text>
<Text style={styles.rowValue}>{t('about.stablyai')}</Text>
</Pressable>
<View style={styles.separator} />
<Pressable
@@ -78,7 +78,7 @@ export default function AboutScreen() {
onPress={() => void Linking.openURL('https://x.com/orca_build')}
>
<XIcon />
<Text style={styles.rowValue}>{t('m.pBOdX2k')}</Text>
<Text style={styles.rowValue}>{t('about.orcaBuild')}</Text>
</Pressable>
</View>
+9 -9
View File
@@ -20,13 +20,13 @@ export default function BrowserSettingsScreen(): React.JSX.Element {
const linkModeOptions: PickerOption<MobileTerminalLinkOpenMode>[] = [
{
value: 'orca-browser',
label: t('m.OcnHGtE'),
subtitle: t('m.Z49nZqQ')
label: t('browserSettings.orca'),
subtitle: t('browserSettings.openStreamed')
},
{
value: 'phone-browser',
label: t('m.hIo6XgE'),
subtitle: t('m.Wj6QRnE')
label: t('browserSettings.phone'),
subtitle: t('browserSettings.openSafari')
}
]
const linkModeLabel =
@@ -47,12 +47,12 @@ export default function BrowserSettingsScreen(): React.JSX.Element {
<Pressable style={styles.backButton} onPress={() => router.back()}>
<ChevronLeft size={22} color={colors.textSecondary} />
</Pressable>
<Text style={styles.heading}>{t('m.GJwMNak')}</Text>
<Text style={styles.heading}>{t('browserSettings.browser')}</Text>
</View>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<Text style={styles.groupHeading}>{t('m.gh7VaTc')}</Text>
<Text style={styles.groupDescription}>{t('m.K6VU-6c')}</Text>
<Text style={styles.groupHeading}>{t('browserSettings.links')}</Text>
<Text style={styles.groupDescription}>{t('browserSettings.choose')}</Text>
<View style={[styles.section, styles.sectionTopGap]}>
<Pressable
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
@@ -60,7 +60,7 @@ export default function BrowserSettingsScreen(): React.JSX.Element {
>
<Globe size={16} color={colors.textSecondary} />
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>{t('m.wdyLCXU')}</Text>
<Text style={styles.rowLabel}>{t('browserSettings.openTerminal')}</Text>
<Text style={styles.rowSublabel}>{linkModeLabel}</Text>
</View>
<ChevronRight size={16} color={colors.textMuted} />
@@ -70,7 +70,7 @@ export default function BrowserSettingsScreen(): React.JSX.Element {
<PickerModal<MobileTerminalLinkOpenMode>
visible={pickerOpen}
title={t('m.wdyLCXU')}
title={t('browserSettings.openTerminal')}
options={linkModeOptions}
selected={linkMode}
onSelect={selectLinkMode}
+11 -5
View File
@@ -87,7 +87,7 @@ export default function ConnectionLogScreen() {
<Pressable style={styles.backButton} onPress={() => router.back()}>
<ChevronLeft size={22} color={colors.textSecondary} />
</Pressable>
<Text style={styles.heading}>{t('m.x43FzPs')}</Text>
<Text style={styles.heading}>{t('connectionLog.connection')}</Text>
</View>
{hosts.length > 1 && (
@@ -114,7 +114,11 @@ export default function ConnectionLogScreen() {
<View style={styles.statusRow}>
<Text style={styles.statusText}>
{state}
{reconnectAttempts > 0 ? t('m.mu5iayg', { value0: reconnectAttempts }) : ''}
{reconnectAttempts > 0
? t('connectionLog.attempt', {
reconnectAttempts: reconnectAttempts
})
: ''}
</Text>
<Pressable style={styles.copyButton} onPress={() => void copyDiagnostics()}>
{copied ? (
@@ -122,17 +126,19 @@ export default function ConnectionLogScreen() {
) : (
<Copy size={14} color={colors.textSecondary} />
)}
<Text style={styles.copyButtonText}>{copied ? t('m.DYy9eRs') : t('m.GiZXYb8')}</Text>
<Text style={styles.copyButtonText}>
{copied ? t('connectionLog.copied') : t('connectionLog.copy')}
</Text>
</Pressable>
</View>
{entries.length > 0 ? (
<ConnectionLog entries={[...entries]} title={selected.name} />
) : (
<Text style={styles.emptyText}>{t('m.FX9lUwo')}</Text>
<Text style={styles.emptyText}>{t('connectionLog.noConnection')}</Text>
)}
</>
) : (
<Text style={styles.emptyText}>{t('m.POUjxlM')}</Text>
<Text style={styles.emptyText}>{t('connectionLog.noPaired')}</Text>
)}
</View>
)
+16 -14
View File
@@ -59,7 +59,7 @@ export default function AccountsScreen() {
// Why: a stale snapshot can expose a finite reset action for the wrong
// account; fail closed if a host sends a shape this mobile cannot prove.
setSnapshot(null)
setError(t('m.Cop_m2o'))
setError(t('accounts.invalid'))
}, [])
const {
supported: codexResetSupported,
@@ -96,7 +96,7 @@ export default function AccountsScreen() {
}
const host = hosts.find((h) => h.id === hostId)
if (!host) {
setError(t('m.nnr0WpI'))
setError(t('accounts.hostNot'))
return
}
setHostName(host.name)
@@ -176,7 +176,7 @@ export default function AccountsScreen() {
codexTarget?.runtime === 'wsl' ? { accountId, target: codexTarget } : { accountId }
const res = await client.sendRequest(method, params)
if (!res.ok) {
Alert.alert(t('m.5TVCgEg'), res.error.message)
Alert.alert(t('accounts.could'), res.error.message)
} else {
// Why: optimistic refresh — the streaming subscription will also
// emit, but a one-shot keeps the UI responsive even if the stream
@@ -184,7 +184,7 @@ export default function AccountsScreen() {
await refresh()
}
} catch (e) {
Alert.alert(t('m.5TVCgEg'), e instanceof Error ? e.message : String(e))
Alert.alert(t('accounts.could'), e instanceof Error ? e.message : String(e))
} finally {
setBusyAccountId(null)
}
@@ -220,22 +220,22 @@ export default function AccountsScreen() {
disabled={busyAccountId !== null || resettingCodex || connState !== 'connected'}
>
<View style={styles.rowMain}>
<Text style={styles.rowTitle}>{t('m.uuo9M60')}</Text>
<Text style={styles.rowSubtitle}>{t('m.rjJXJpg')}</Text>
<Text style={styles.rowTitle}>{t('accounts.system')}</Text>
<Text style={styles.rowSubtitle}>{t('accounts.use')}</Text>
{/* Why: when system default is the active selection, activeUsage
holds the system-default login's rate limits — surface them
here so non-managed users still see their usage. */}
{activeAccountId === null && hasActiveProviderUsage(activeUsage) ? (
<View style={styles.usageRow}>
<UsageBar
label={t('m.YmnN8fw')}
label={t('accounts.five')}
usedPercent={activeSessionBar.usedPercent}
unavailable={activeSessionBar.unavailable}
loading={activeSessionBar.loading}
resetText={getWindowResetLabel(activeUsage, 'session', now)}
/>
<UsageBar
label={t('m.08ocQGs')}
label={t('accounts.seven')}
usedPercent={activeWeeklyBar.usedPercent}
unavailable={activeWeeklyBar.unavailable}
loading={activeWeeklyBar.loading}
@@ -283,14 +283,14 @@ export default function AccountsScreen() {
</Text>
<View style={styles.usageRow}>
<UsageBar
label={t('m.YmnN8fw')}
label={t('accounts.five')}
usedPercent={sessionBar.usedPercent}
unavailable={sessionBar.unavailable}
loading={sessionBar.loading}
resetText={getWindowResetLabel(usage, 'session', now)}
/>
<UsageBar
label={t('m.08ocQGs')}
label={t('accounts.seven')}
usedPercent={weeklyBar.usedPercent}
unavailable={weeklyBar.unavailable}
loading={weeklyBar.loading}
@@ -335,7 +335,7 @@ export default function AccountsScreen() {
<ChevronLeft size={22} color={colors.textPrimary} />
</Pressable>
<View style={styles.titleWrap}>
<Text style={styles.heading}>{t('m.XheJ_p8')}</Text>
<Text style={styles.heading}>{t('accounts.accounts')}</Text>
{hostName ? (
<Text style={styles.subheading} numberOfLines={1}>
{hostName}
@@ -369,7 +369,9 @@ export default function AccountsScreen() {
<View style={styles.placeholder}>
<ActivityIndicator color={colors.textSecondary} />
<Text style={styles.placeholderText}>
{t('m.sqHaM-Q', { value0: hostName || t('m.esqB5TM') })}
{t('accounts.connecting', {
hostName: hostName || t('accounts.host')
})}
</Text>
</View>
) : error && !snapshot ? (
@@ -379,7 +381,7 @@ export default function AccountsScreen() {
) : !snapshot ? (
<View style={styles.placeholder}>
<ActivityIndicator color={colors.textSecondary} />
<Text style={styles.placeholderText}>{t('m.z0N4v1Y')}</Text>
<Text style={styles.placeholderText}>{t('accounts.loading')}</Text>
</View>
) : (
<>
@@ -387,7 +389,7 @@ export default function AccountsScreen() {
{renderProviderSection('codex', 'Codex')}
<View style={styles.footerHint}>
<User size={14} color={colors.textMuted} />
<Text style={styles.footerHintText}>{t('m.lXRU_hQ')}</Text>
<Text style={styles.footerHintText}>{t('accounts.add')}</Text>
</View>
</>
)}
+18 -18
View File
@@ -40,14 +40,14 @@ export default function EditHostScreen() {
const load = useCallback(async () => {
if (!hostId) {
setLoadError(t('m.yLNjPPk'))
setLoadError(t('edit.missing'))
return
}
try {
const hosts = await loadHosts()
const found = hosts.find((h) => h.id === hostId) ?? null
if (!found) {
setLoadError(t('m.T_ULLfI'))
setLoadError(t('edit.hostRemoved'))
setHost(null)
return
}
@@ -56,7 +56,7 @@ export default function EditHostScreen() {
setAddress(displayHostEndpoint(found.endpoint))
setLoadError(null)
} catch (err) {
setLoadError(err instanceof Error ? err.message : t('m.koyS6Oc'))
setLoadError(err instanceof Error ? err.message : t('edit.failedLoad'))
setHost(null)
}
}, [hostId])
@@ -87,7 +87,7 @@ export default function EditHostScreen() {
}
const nextName = name.trim()
if (!nextName) {
setSaveError(t('m.7UECNUc'))
setSaveError(t('edit.enter'))
return
}
if (endpointEdit.kind === 'invalid') {
@@ -114,7 +114,7 @@ export default function EditHostScreen() {
...(nextEndpoint !== undefined ? { endpoint: nextEndpoint } : {})
})
} catch (err) {
setSaveError(err instanceof Error ? err.message : t('m.sI26LXo'))
setSaveError(err instanceof Error ? err.message : t('edit.failedSave'))
savingRef.current = false
setSaving(false)
return
@@ -149,11 +149,11 @@ export default function EditHostScreen() {
style={styles.backButton}
onPress={() => router.back()}
accessibilityRole="button"
accessibilityLabel={t('m.K8w9o8U')}
accessibilityLabel={t('edit.back')}
>
<ChevronLeft size={22} color={colors.textSecondary} />
</Pressable>
<Text style={styles.heading}>{t('m.NKISHlY')}</Text>
<Text style={styles.heading}>{t('edit.edit')}</Text>
<Pressable
style={({ pressed }) => [
styles.saveButton,
@@ -162,12 +162,12 @@ export default function EditHostScreen() {
onPress={() => void handleSave()}
disabled={!canSave}
accessibilityRole="button"
accessibilityLabel={t('m.ugYhCr4')}
accessibilityLabel={t('edit.saveHost')}
>
{saving ? (
<ActivityIndicator size="small" color={colors.bgBase} />
) : (
<Text style={styles.saveButtonText}>{t('m.OOhGw28')}</Text>
<Text style={styles.saveButtonText}>{t('edit.save')}</Text>
)}
</Pressable>
</View>
@@ -176,7 +176,7 @@ export default function EditHostScreen() {
<View style={styles.errorState}>
<Text style={styles.errorText}>{loadError}</Text>
<Pressable style={styles.secondaryButton} onPress={() => router.back()}>
<Text style={styles.secondaryButtonText}>{t('m.LS3NecQ')}</Text>
<Text style={styles.secondaryButtonText}>{t('edit.go')}</Text>
</Pressable>
</View>
) : !host ? (
@@ -192,28 +192,28 @@ export default function EditHostScreen() {
contentContainerStyle={[styles.form, { paddingBottom: insets.bottom + spacing.xl }]}
keyboardShouldPersistTaps="handled"
>
<Text style={styles.help}>{t('m.KytWtss')}</Text>
<Text style={styles.help}>{t('edit.connectionAddressDescription')}</Text>
<Text style={styles.label}>{t('m.wLjwLQg')}</Text>
<Text style={styles.label}>{t('edit.name')}</Text>
<TextInput
style={styles.input}
accessibilityLabel={t('m.wLjwLQg')}
accessibilityLabel={t('edit.name')}
value={name}
onChangeText={(value) => {
setName(value)
setSaveError(null)
}}
placeholder={t('m.jHccG1s')}
placeholder={t('edit.hostName')}
placeholderTextColor={colors.textMuted}
autoCapitalize="words"
autoCorrect={false}
returnKeyType="next"
/>
<Text style={styles.label}>{t('m.kMmtf5E')}</Text>
<Text style={styles.label}>{t('edit.address')}</Text>
<TextInput
style={styles.input}
accessibilityLabel={t('m.kMmtf5E')}
accessibilityLabel={t('edit.address')}
value={address}
onChangeText={(value) => {
setAddress(value)
@@ -232,11 +232,11 @@ export default function EditHostScreen() {
}
}}
/>
<Text style={styles.hint}>{t('m.dYplni8')}</Text>
<Text style={styles.hint}>{t('edit.accepts')}</Text>
{endpointEdit == null ? null : endpointEdit.kind !== 'invalid' ? (
<Text style={styles.preview} numberOfLines={2}>
{t('m.eB5hKE4', { value0: endpointEdit.endpoint })}
{t('edit.connects', { endpoint: endpointEdit.endpoint })}
</Text>
) : address.trim().length > 0 ? (
<Text style={styles.previewError}>{endpointEdit.error}</Text>
+66 -69
View File
@@ -45,7 +45,7 @@ import {
} from '../../../src/transport/client-context-connection-metrics'
import {
classifyConnection,
type ConnectionVerdict
isConnectionErrorVerdict
} from '../../../src/transport/connection-health'
import type { RpcSuccess } from '../../../src/transport/types'
import { StatusDot } from '../../../src/components/StatusDot'
@@ -101,31 +101,22 @@ import {
getWorkspaceSortOptions
} from '../../../src/worktree/workspace-list-picker-options'
import type { RepoSummary } from '../../../src/worktree/host-worktree-rpc-types'
import type { MobileHostScreenProps } from '../../../src/worktree/mobile-host-screen-props'
import type { WorkspaceStatusDefinition } from '../../../../src/shared/types'
import { getDefaultMobileWorkspaceStatuses } from '../../../src/worktree/mobile-workspace-statuses'
import { t } from '@/i18n/mobile-i18n'
import { createMobileTranslator } from '@/i18n/mobile-i18n'
function isErrorVerdict(v: ConnectionVerdict): boolean {
return v.kind === 'warning' || v.kind === 'unreachable' || v.kind === 'auth-failed'
}
const tr = createMobileTranslator('host')
const filterLabel = (activeFilterCount: string) => tr('filterActive', { activeFilterCount })
const REPO_METADATA_REFRESH_MS = 60_000
type HostScreenProps = {
// When true, rendered as the persistent tablet sidebar by the host layout, not as its own routed screen.
embedded?: boolean
// Route params aren't in scope when rendered from the layout, so the caller passes these explicitly.
hostId?: string
action?: string
onHideSidebar?: () => void
}
export function HostScreen({
embedded = false,
hostId: hostIdProp,
action: actionProp,
onHideSidebar
}: HostScreenProps = {}) {
}: MobileHostScreenProps = {}) {
const params = useLocalSearchParams<{ hostId: string; action?: string }>()
const hostId = hostIdProp ?? params.hostId
const action = actionProp ?? params.action
@@ -346,7 +337,7 @@ export function HostScreen({
}
const host = hosts.find((h) => h.id === hostId)
if (!host) {
setError(t('m.2FRJWFs'))
setError(tr('hostNot'))
return
}
setHostName(host.name)
@@ -619,7 +610,7 @@ export function HostScreen({
} catch {
// Why: removal can fail while still paired; re-open confirm (ConfirmModal closes on confirm).
setConfirmRemoveHost(true)
Alert.alert(t('m.P_OTj14'), t('m.QHBQ8yI'))
Alert.alert(tr('could'), tr('please'))
}
}, [hostId, leaveHost, closeHostClient])
@@ -709,7 +700,7 @@ export function HostScreen({
return count
}, [filters])
const selectedSortLabel =
getWorkspaceSortOptions().find((option) => option.value === sortMode)?.label ?? t('m.qVCBvFg')
getWorkspaceSortOptions().find((option) => option.value === sortMode)?.label ?? tr('recent')
const handleGroupChange = useCallback(
(value: MobileGroupMode) => {
@@ -787,7 +778,7 @@ export function HostScreen({
style={styles.backButton}
onPress={leaveHost}
accessibilityRole="button"
accessibilityLabel={t('m.IDkp2AI')}
accessibilityLabel={tr('back')}
hitSlop={8}
>
<ChevronLeft size={22} color={colors.textPrimary} />
@@ -803,14 +794,14 @@ export function HostScreen({
<View style={styles.hostIdentity}>
<StatusDot state={connState} verdict={headerVerdict} />
<Text style={styles.hostNameText} numberOfLines={1}>
{hostName || t('m.3GdsXVQ')}
{hostName || tr('host')}
</Text>
</View>
{connState !== 'connected' &&
(() => {
// Why: auth-failed has its own banner, so suppress the Reconnect button for that verdict.
const verdict = headerVerdict
const isError = isErrorVerdict(verdict)
const isError = isConnectionErrorVerdict(verdict)
const showReconnectButton = isError && hostId && verdict.kind !== 'auth-failed'
if (!showReconnectButton) {
return null
@@ -821,7 +812,7 @@ export function HostScreen({
onPress={() => void forceReconnectHost(hostId!)}
hitSlop={8}
>
<Text style={styles.reconnectButtonText}>{t('m.2FFC6Og')}</Text>
<Text style={styles.reconnectButtonText}>{tr('reconnect')}</Text>
</Pressable>
)
})()}
@@ -837,7 +828,7 @@ export function HostScreen({
onPress={openFloatingWorkspace}
disabled={connState !== 'connected'}
accessibilityRole="button"
accessibilityLabel={t('m.qs3IfgE')}
accessibilityLabel={tr('floating')}
hitSlop={8}
>
<SquareTerminal
@@ -851,7 +842,7 @@ export function HostScreen({
style={styles.sidebarCollapseButton}
onPress={onHideSidebar}
accessibilityRole="button"
accessibilityLabel={t('m.Iq0v42M')}
accessibilityLabel={tr('hideSidebar')}
hitSlop={8}
>
<PanelLeftClose size={14} color={colors.textSecondary} />
@@ -873,10 +864,12 @@ export function HostScreen({
accessibilityRole="button"
accessibilityLabel={
activeFilterCount === 0
? t('m.kz_Kdkw')
? tr('filterWorkspaces')
: activeFilterCount === 1
? t('m.6oNXq-k')
: t('m.DxBfvEc', { value0: activeFilterCount })
? tr('filterWorkspaces1')
: tr('filterWorkspacesActive', {
activeFilterCount: activeFilterCount
})
}
>
<Filter
@@ -890,7 +883,7 @@ export function HostScreen({
]}
numberOfLines={1}
>
{t('m.2hirWvg', { value0: activeFilterCount ? ` (${activeFilterCount})` : '' })}
{filterLabel(activeFilterCount ? ` (${activeFilterCount})` : '')}
</Text>
</Pressable>
@@ -898,7 +891,9 @@ export function HostScreen({
style={[styles.modeButton, styles.embeddedModeButton]}
onPress={() => setShowSortPicker(true)}
accessibilityRole="button"
accessibilityLabel={t('m.jZR1hFw', { value0: selectedSortLabel })}
accessibilityLabel={tr('sortSelected', {
selectedSortLabel: selectedSortLabel
})}
>
<SlidersHorizontal size={14} color={colors.textSecondary} />
<Text style={styles.sortLabel} numberOfLines={1}>
@@ -910,17 +905,17 @@ export function HostScreen({
style={[styles.modeButton, styles.embeddedModeButton]}
onPress={() => setShowGroupPicker(true)}
accessibilityRole="button"
accessibilityLabel={t('m.KYYIQO4')}
accessibilityLabel={tr('groupWorkspaces')}
>
<Layers size={14} color={colors.textSecondary} />
<Text style={styles.sortLabel} numberOfLines={1}>
{groupMode === 'none'
? t('m.mZ5NlSU')
? tr('group')
: groupMode === 'workspaceStatus'
? t('m.GAUYfWk')
? tr('status')
: groupMode === 'repo'
? t('m.CzGUIP8')
: t('m.sUx-id0')}
? tr('repo')
: tr('pr')}
</Text>
</Pressable>
</View>
@@ -934,7 +929,7 @@ export function HostScreen({
onPress={() => navigateFromHostList(`/h/${hostId}/accounts`)}
disabled={connState !== 'connected'}
accessibilityRole="button"
accessibilityLabel={t('m.CVlu0tE')}
accessibilityLabel={tr('accounts')}
>
<UserCircle
size={16}
@@ -950,7 +945,7 @@ export function HostScreen({
onPress={() => navigateFromHostList(`/h/${hostId}/tasks`)}
disabled={connState !== 'connected'}
accessibilityRole="button"
accessibilityLabel={t('m.V5DOCOQ')}
accessibilityLabel={tr('tasks')}
>
<List
size={16}
@@ -967,7 +962,7 @@ export function HostScreen({
onPress={openFloatingWorkspace}
disabled={connState !== 'connected'}
accessibilityRole="button"
accessibilityLabel={t('m.qs3IfgE')}
accessibilityLabel={tr('floating')}
>
<SquareTerminal
size={18}
@@ -984,7 +979,7 @@ export function HostScreen({
onPress={openNewWorktreeModal}
disabled={connState !== 'connected'}
accessibilityRole="button"
accessibilityLabel={t('m.LIEbwVI')}
accessibilityLabel={tr('new')}
>
<Plus
size={16}
@@ -996,7 +991,7 @@ export function HostScreen({
style={styles.embeddedToolbarIconButton}
onPress={() => setShowSearch((s) => !s)}
accessibilityRole="button"
accessibilityLabel={showSearch ? t('m.AYkrJs8') : t('m.LgVSSVY')}
accessibilityLabel={showSearch ? tr('close') : tr('searchWorkspaces')}
>
{showSearch ? (
<X size={16} color={colors.textSecondary} />
@@ -1022,7 +1017,7 @@ export function HostScreen({
activeFilterCount > 0 && styles.filterChipTextActive
]}
>
{t('m.2hirWvg', { value0: activeFilterCount > 0 ? ` (${activeFilterCount})` : '' })}
{filterLabel(activeFilterCount > 0 ? ` (${activeFilterCount})` : '')}
</Text>
</Pressable>
@@ -1037,12 +1032,12 @@ export function HostScreen({
<Layers size={14} color={colors.textSecondary} />
<Text style={styles.sortLabel} numberOfLines={1}>
{groupMode === 'none'
? t('m.mZ5NlSU')
? tr('group')
: groupMode === 'workspaceStatus'
? t('m.GAUYfWk')
? tr('status')
: groupMode === 'repo'
? t('m.CzGUIP8')
: t('m.sUx-id0')}
? tr('repo')
: tr('pr')}
</Text>
</Pressable>
@@ -1097,11 +1092,11 @@ export function HostScreen({
<MobileSearchField
value={search}
onChangeText={setSearch}
placeholder={t('m.ThGUKR8')}
placeholder={tr('searchWorktrees')}
autoFocus
// Why: new key per open remounts the focus effect across rapid toggles so the keyboard reappears.
focusKey={showSearch}
accessibilityLabel={t('m.Y4yzly8')}
accessibilityLabel={tr('searchWorktreesAccessibility')}
/>
</View>
)}
@@ -1119,7 +1114,11 @@ export function HostScreen({
{connState === 'connected' && worktreesLoaded && sections.length === 0 && (
<View style={styles.centered}>
<Text style={styles.emptyText}>
{search ? t('m.d-FojRM') : activeFilterCount > 0 ? t('m.oRXGJpo') : t('m.4HGax2g')}
{search
? tr('noMatching')
: activeFilterCount > 0
? tr('noWorktreesMatch')
: tr('noWorktrees')}
</Text>
</View>
)}
@@ -1210,7 +1209,7 @@ export function HostScreen({
<PickerModal
visible={showSortPicker}
title={t('m.hFsVshM')}
title={tr('sort')}
options={getWorkspaceSortOptions()}
selected={sortMode}
onSelect={handleSortChange}
@@ -1219,7 +1218,7 @@ export function HostScreen({
<PickerModal
visible={showGroupPicker}
title={t('m.fVWdjBk')}
title={tr('groupTitle')}
options={getWorkspaceGroupOptions()}
selected={groupMode}
onSelect={handleGroupChange}
@@ -1228,30 +1227,30 @@ export function HostScreen({
<BottomDrawer visible={showFilterModal} onClose={() => setShowFilterModal(false)}>
<View style={styles.filterModalHeader}>
<Text style={styles.filterModalTitle}>{t('m.S8jJ9v4')}</Text>
<Text style={styles.filterModalTitle}>{tr('filter')}</Text>
{activeFilterCount > 0 && (
<Pressable onPress={clearFilters}>
<Text style={styles.clearFiltersText}>{t('m.5IYckMg')}</Text>
<Text style={styles.clearFiltersText}>{tr('clear')}</Text>
</Pressable>
)}
</View>
<Text style={styles.filterSectionLabel}>{t('m.bd8B2ME')}</Text>
<Text style={styles.filterSectionLabel}>{tr('workspaces')}</Text>
<View style={styles.filterGroup}>
<Pressable style={styles.filterRow} onPress={toggleHideSleeping}>
<Text style={styles.filterRowText}>{t('m.1w-t3vQ')}</Text>
<Text style={styles.filterRowText}>{tr('hideSleeping')}</Text>
{filters.hideSleeping && <Check size={14} color={colors.textPrimary} />}
</Pressable>
<View style={styles.filterSeparator} />
<Pressable style={styles.filterRow} onPress={toggleHideDefaultBranch}>
<Text style={styles.filterRowText}>{t('m.MFWDh4g')}</Text>
<Text style={styles.filterRowText}>{tr('hideDefault')}</Text>
{filters.hideDefaultBranch && <Check size={14} color={colors.textPrimary} />}
</Pressable>
</View>
{uniqueRepos.length > 1 && (
<>
<Text style={styles.filterSectionLabel}>{t('m.aCNuYPE')}</Text>
<Text style={styles.filterSectionLabel}>{tr('repositories')}</Text>
<View style={styles.filterGroup}>
{uniqueRepos.map((repo, i) => (
<View key={repo.id}>
@@ -1283,11 +1282,11 @@ export function HostScreen({
{confirmDelete ? (
<View>
<View style={styles.confirmContent}>
<Text style={styles.confirmTitle}>{t('m.sCHpXNI')}</Text>
<Text style={styles.confirmTitle}>{tr('deleteWorktree')}</Text>
<Text style={styles.confirmMessage}>
{t('m.asTmnrM', {
value0: confirmDelete.displayName || confirmDelete.repo,
value1: confirmDelete.branch
{tr('deleteDisplay', {
displayName: confirmDelete.displayName || confirmDelete.repo,
branch: confirmDelete.branch
})}
</Text>
</View>
@@ -1300,7 +1299,7 @@ export function HostScreen({
]}
onPress={() => setConfirmDelete(null)}
>
<Text style={styles.confirmBtnCancelText}>{t('m.jQgIadE')}</Text>
<Text style={styles.confirmBtnCancelText}>{tr('cancel')}</Text>
</Pressable>
<Pressable
style={({ pressed }) => [
@@ -1316,7 +1315,7 @@ export function HostScreen({
setActionTarget(null)
}}
>
<Text style={styles.confirmBtnDestructiveText}>{t('m.PCaw7X8')}</Text>
<Text style={styles.confirmBtnDestructiveText}>{tr('delete')}</Text>
</Pressable>
</View>
</View>
@@ -1336,7 +1335,7 @@ export function HostScreen({
onDone: () => setActionTarget(null)
}),
{
label: t('m.7b05QJE'),
label: tr('sleep'),
icon: Moon,
onPress: () => {
if (client) {
@@ -1351,16 +1350,14 @@ export function HostScreen({
}
},
{
label: isWorktreePinned(actionTarget, pinnedIds)
? t('m.dgZsiv8')
: t('m.JwcgRbU'),
label: isWorktreePinned(actionTarget, pinnedIds) ? tr('unpin') : tr('pin'),
onPress: () => {
togglePin(actionTarget.worktreeId)
setActionTarget(null)
}
},
{
label: t('m.PCaw7X8'),
label: tr('delete'),
destructive: true,
onPress: () => setConfirmDelete(actionTarget)
}
@@ -1374,9 +1371,9 @@ export function HostScreen({
{/* Host remove confirmation */}
<ConfirmModal
visible={confirmRemoveHost}
title={t('m.tndsmJE')}
message={t('m.HbMqJ5k', { value0: hostName })}
confirmLabel={t('m.EaX2_X8')}
title={tr('removeHost')}
message={tr('removeHostName', { hostName: hostName })}
confirmLabel={tr('remove')}
destructive
onConfirm={() => void handleRemoveHost()}
onCancel={() => setConfirmRemoveHost(false)}
@@ -22,7 +22,7 @@ export function QuickCommandsTabButton({ disabled, onPress }: Props) {
]}
disabled={disabled}
onPress={onPress}
accessibilityLabel={t('m.Y1upIE8')}
accessibilityLabel={t('quickCommandsTabButton.quick')}
>
<SquareChevronRight size={16} color={colors.textSecondary} strokeWidth={2.2} />
</Pressable>
File diff suppressed because it is too large Load Diff
@@ -153,3 +153,37 @@ export type TerminalGestureInputQueue = {
timer: ReturnType<typeof setTimeout> | null
lastUpdatedMs: number
}
export type MarkdownReaderProps = {
documentId: string
doc: MarkdownDocState | undefined
onRefresh: () => void
onChange: (content: string) => void
onSave: () => void
onCopy: () => void
onDiscard: () => void
keyboardLift: number
}
export type DiffLineRowProps = {
line: RenderableDiffLine
title: string
index: number
comments: DiffComment[]
activeCommentLine: number | null
commentDraft: string
commentsBusy: boolean
onStartComment: (lineNumber: number) => void
onCancelComment: () => void
onDraftChange: (value: string) => void
onSubmitComment: (lineNumber: number) => void
onDeleteComment: (commentId: string) => void
}
export type FileReaderProps = {
doc: FileDocState | undefined
title: string
relativePath: string
language?: string
diffCommentActions?: DiffCommentActions
}
File diff suppressed because it is too large Load Diff
+12 -9
View File
@@ -40,21 +40,24 @@ function HostStack({ animation }: { animation: 'none' | 'default' }) {
animation
}}
>
<Stack.Screen name="[hostId]/index" options={{ title: t('m.esqB5TM') }} />
<Stack.Screen name="[hostId]/edit" options={{ title: t('m.0zcPPW8') }} />
<Stack.Screen name="[hostId]/accounts" options={{ title: t('m.KVY3pfk') }} />
<Stack.Screen name="[hostId]/tasks" options={{ title: t('m.YTfV9-o') }} />
<Stack.Screen name="[hostId]/session/[worktreeId]" options={{ title: t('m.wtxV_i4') }} />
<Stack.Screen name="[hostId]/index" options={{ title: t('accounts.host') }} />
<Stack.Screen name="[hostId]/edit" options={{ title: t('layout.edit') }} />
<Stack.Screen name="[hostId]/accounts" options={{ title: t('layout.accounts') }} />
<Stack.Screen name="[hostId]/tasks" options={{ title: t('layout.tasks') }} />
<Stack.Screen
name="[hostId]/session/[worktreeId]"
options={{ title: t('layout.terminal') }}
/>
<Stack.Screen
name="[hostId]/source-control/[worktreeId]"
options={{ title: t('m.MZqxT8s') }}
options={{ title: t('layout.source') }}
/>
<Stack.Screen
name="[hostId]/agent-history/[worktreeId]"
options={{ title: t('m.atubgYk') }}
options={{ title: t('layout.agent') }}
/>
<Stack.Screen name="[hostId]/review/[worktreeId]" options={{ title: t('m.AcEIxcI') }} />
<Stack.Screen name="[hostId]/pr/[worktreeId]" options={{ title: t('m.fO2nz88') }} />
<Stack.Screen name="[hostId]/review/[worktreeId]" options={{ title: t('layout.changes') }} />
<Stack.Screen name="[hostId]/pr/[worktreeId]" options={{ title: t('task.pullRequest') }} />
</Stack>
)
}
+51 -38
View File
@@ -105,9 +105,9 @@ type HomeLinearStatus = {
function taskProviderLabel(provider: TaskProvider): string {
return {
github: t('m.TH4OaOM'),
gitlab: t('m.fvIr9H8'),
linear: t('m.kZGbeC4')
github: t('home.gitHub'),
gitlab: t('home.gitLab'),
linear: t('home.linear')
}[provider]
}
@@ -626,11 +626,11 @@ export default function HomeScreen() {
<ListTodo size={18} color={colors.textSecondary} />
</View>
<View style={styles.taskHomeMain}>
<Text style={styles.taskHomeTitle}>{t('m.y7w0hIs')}</Text>
<Text style={styles.taskHomeTitle}>{t('home.tasks')}</Text>
<Text style={styles.taskHomeSubtitle} numberOfLines={1}>
{primaryTaskProviders.length > 0
? primaryTaskProviders.map(taskProviderLabel).join(' · ')
: t('m.1_sz9cs')}
: t('home.no')}
</Text>
</View>
<View style={styles.taskHomeTrailing}>
@@ -642,7 +642,9 @@ export default function HomeScreen() {
<Pressable
key={provider}
accessibilityRole="button"
accessibilityLabel={t('m.tNj4XnM', { value0: taskProviderLabel(provider) })}
accessibilityLabel={t('home.openTask', {
taskProviderLabel: taskProviderLabel(provider)
})}
hitSlop={8}
style={({ pressed }) => [
styles.taskHomeProviderButton,
@@ -674,7 +676,7 @@ export default function HomeScreen() {
} catch {
// Why: ConfirmModal closes on confirm; re-open for retry so the failure isn't silent.
setConfirmRemove(hostToRemove)
Alert.alert(t('m.U7WlIpo'), t('m.GLgoz1s'))
Alert.alert(t('home.could'), t('home.please'))
}
}
@@ -686,7 +688,7 @@ export default function HomeScreen() {
<View style={styles.logoMark}>
<OrcaLogo size={18} />
</View>
<Text style={styles.brandName}>{t('m.VjaDNq0')}</Text>
<Text style={styles.brandName}>{t('home.orca')}</Text>
</View>
<Pressable
style={({ pressed }) => [styles.iconButton, pressed && styles.iconButtonPressed]}
@@ -706,16 +708,16 @@ export default function HomeScreen() {
]}
>
<View style={styles.emptyHero}>
<Text style={styles.emptyTitle}>{t('m.8vieqXE')}</Text>
<Text style={styles.emptyBody}>{t('m.QT_H7rk')}</Text>
<Text style={styles.emptyTitle}>{t('home.connectYour')}</Text>
<Text style={styles.emptyBody}>{t('home.pairingDescription')}</Text>
<Pressable style={styles.primaryButton} onPress={() => router.push('/pair-scan')}>
<QrCode size={17} color={colors.bgBase} />
<Text style={styles.primaryButtonText}>{t('m.lWqrsbM')}</Text>
<Text style={styles.primaryButtonText}>{t('home.pair')}</Text>
</Pressable>
</View>
<View style={styles.stepsSection}>
<Text style={styles.sectionHeading}>{t('m.9HldFAc')}</Text>
<Text style={styles.sectionHeading}>{t('home.how')}</Text>
{getOnboardingSteps().map((step, i) => (
<View key={step.title} style={[styles.stepRow, i > 0 && styles.stepRowBorder]}>
<View style={styles.stepNum}>
@@ -743,7 +745,7 @@ export default function HomeScreen() {
ListHeaderComponent={
<View>
<View style={styles.hero}>
<Text style={styles.heroTitle}>{t('m.9i5ZhFo')}</Text>
<Text style={styles.heroTitle}>{t('home.welcome')}</Text>
</View>
{stats && (
@@ -752,20 +754,20 @@ export default function HomeScreen() {
<Text style={styles.statValue}>
{stats.totalAgentsSpawned.toLocaleString()}
</Text>
<Text style={styles.statLabel}>{t('m.qFFbXcQ')}</Text>
<Text style={styles.statLabel}>{t('home.agents')}</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>{formatDuration(stats.totalAgentTimeMs)}</Text>
<Text style={styles.statLabel}>{t('m.4uJ9yUY')}</Text>
<Text style={styles.statLabel}>{t('home.agent')}</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>{stats.totalPRsCreated.toLocaleString()}</Text>
<Text style={styles.statLabel}>{t('m.TPknwrs')}</Text>
<Text style={styles.statLabel}>{t('home.prs')}</Text>
</View>
</View>
)}
<Text style={styles.sectionHeading}>{t('m.81Qrbno')}</Text>
<Text style={styles.sectionHeading}>{t('home.desktops')}</Text>
</View>
}
ItemSeparatorComponent={CardGap}
@@ -803,7 +805,7 @@ export default function HomeScreen() {
{resumeWorktree ? (
<>
<Text style={[styles.sectionHeading, styles.sectionHeadingTightTop]}>
{t('m.ROZQW2U')}
{t('home.resume')}
</Text>
<Pressable
style={({ pressed }) => [styles.resumeCard, pressed && styles.hostCardPressed]}
@@ -841,14 +843,14 @@ export default function HomeScreen() {
<ChevronRight size={16} color={colors.textMuted} />
</Pressable>
<Text style={[styles.sectionHeading, styles.sectionHeadingTightTop]}>
{t('m.y7w0hIs')}
{t('home.tasks')}
</Text>
{renderTaskHomeCard()}
</>
) : (
<>
<Text style={[styles.sectionHeading, styles.sectionHeadingTightTop]}>
{t('m.y7w0hIs')}
{t('home.tasks')}
</Text>
{renderTaskHomeCard()}
</>
@@ -856,7 +858,7 @@ export default function HomeScreen() {
{/* ─── Quick actions ─── */}
<Text style={[styles.sectionHeading, { marginTop: spacing.xl }]}>
{t('m.1St5QBU')}
{t('home.quick')}
</Text>
<View style={styles.quickActions}>
<Pressable
@@ -867,7 +869,7 @@ export default function HomeScreen() {
<QrCode size={16} color={colors.textSecondary} />
</View>
<Text style={styles.quickActionLabel} numberOfLines={2}>
{t('m.lWqrsbM')}
{t('home.pair')}
</Text>
</Pressable>
<Pressable
@@ -887,7 +889,7 @@ export default function HomeScreen() {
<Plus size={16} color={colors.textSecondary} />
</View>
<Text style={styles.quickActionLabel} numberOfLines={2}>
{t('m.8CNCPA8')}
{t('home.new')}
</Text>
</Pressable>
</View>
@@ -896,7 +898,7 @@ export default function HomeScreen() {
{accountsHosts.length > 0 ? (
<>
<Text style={[styles.sectionHeading, { marginTop: spacing.xl }]}>
{t('m.VXpHvL4')}
{t('home.account')}
</Text>
{accountsHosts.map(({ host, snapshot }) => {
const claudeActiveId = snapshot.claude.activeAccountId
@@ -944,17 +946,17 @@ export default function HomeScreen() {
</View>
<View style={styles.accountsInfo}>
<Text style={styles.accountsEmail} numberOfLines={1}>
{active?.email ?? t('m.IOLibYw')}
{active?.email ?? t('home.system')}
</Text>
<View style={styles.accountsBars}>
<UsageBar
label={t('m._KqkyAU')}
label={t('home.five')}
usedPercent={sessionBar.usedPercent}
unavailable={sessionBar.unavailable}
loading={sessionBar.loading}
/>
<UsageBar
label={t('m.JnIDr0Y')}
label={t('home.seven')}
usedPercent={weeklyBar.usedPercent}
unavailable={weeklyBar.unavailable}
loading={weeklyBar.loading}
@@ -986,11 +988,11 @@ export default function HomeScreen() {
? (hostLastConnected[actionTarget.id] ?? null) != null
: false,
labels: {
connect: t('m.X0HdEQ0'),
reconnect: t('m.Ho177Ac'),
disconnect: t('m.RwT781U'),
editHost: t('m.dqjXxV8'),
remove: t('m.t1m0jcA')
connect: t('home.connect'),
reconnect: t('home.reconnect'),
disconnect: t('home.disconnect'),
editHost: t('home.edit'),
remove: t('home.remove')
},
onDismiss: () => setActionTarget(null),
onReconnect: (hostId) => void forceReconnectHost(hostId),
@@ -1003,9 +1005,11 @@ export default function HomeScreen() {
<ConfirmModal
visible={confirmRemove != null}
title={t('m.yOJT3HY')}
message={t('m.o8-uNXo', { value0: confirmRemove?.name })}
confirmLabel={t('m.t1m0jcA')}
title={t('home.removeHost')}
message={t('home.removeConfirm', {
confirmRemoveName: confirmRemove?.name
})}
confirmLabel={t('home.remove')}
destructive
onConfirm={() => void handleRemove()}
onCancel={() => setConfirmRemove(null)}
@@ -1020,9 +1024,18 @@ function CardGap() {
function getOnboardingSteps() {
return [
{ title: t('m.N1Ra8Og'), desc: t('m.aCJTiqw') },
{ title: t('m.bKwbnYs'), desc: t('m.-SigWBo') },
{ title: t('m.rqAbU9s'), desc: t('m.td40qOY') }
{
title: t('home.openOrca'),
desc: t('home.go')
},
{
title: t('home.scan'),
desc: t('home.tap')
},
{
title: t('home.youre'),
desc: t('home.your')
}
]
}
+4 -4
View File
@@ -110,7 +110,7 @@ function MobileOnboardingFlow({
await saveDefaultSessionView(view)
advanceOrContinue()
} catch {
setError(t('m.Dl9ttFI'))
setError(t('mobileOnboarding.your'))
setBusyChoice(null)
choiceInFlightRef.current = false
}
@@ -131,7 +131,7 @@ function MobileOnboardingFlow({
await savePushNotificationsEnabled(enabled)
advanceOrContinue()
} catch {
setError(t('m.6vGSxlM'))
setError(t('mobileOnboarding.notification'))
setBusyChoice(null)
choiceInFlightRef.current = false
}
@@ -145,12 +145,12 @@ function MobileOnboardingFlow({
<SafeAreaView style={styles.container}>
<View style={styles.brandRow}>
<OrcaLogo size={22} />
<Text style={styles.brandName}>{t('m.mB8kw4M')}</Text>
<Text style={styles.brandName}>{t('mobileOnboarding.orca')}</Text>
{steps.length > 1 ? (
<View
accessible
accessibilityRole="progressbar"
accessibilityLabel={t('m.aOm3VN4')}
accessibilityLabel={t('mobileOnboarding.onboarding')}
accessibilityValue={{
text: t('onboarding.progress', {
currentStep: activeIndex + 1,
+7 -7
View File
@@ -18,31 +18,31 @@ export default function NativeChatSettingsScreen() {
<View style={styles.topRow}>
<Pressable
accessibilityRole="button"
accessibilityLabel={t('m.yTJLwao')}
accessibilityLabel={t('nativeChatSettings.back')}
style={styles.backButton}
onPress={() => router.back()}
>
<ChevronLeft size={22} color={colors.textSecondary} />
</Pressable>
<Text style={styles.heading}>{t('m.ao09T48')}</Text>
<Text style={styles.heading}>{t('nativeChatSettings.chat')}</Text>
</View>
<ScrollView
contentContainerStyle={{ paddingBottom: insets.bottom + spacing.lg }}
showsVerticalScrollIndicator={false}
>
<Text style={styles.groupHeading}>{t('m.R7mksio')}</Text>
<Text style={styles.groupDescription}>{t('m.sW_6PG4')}</Text>
<Text style={styles.groupHeading}>{t('nativeChatSettings.default')}</Text>
<Text style={styles.groupDescription}>{t('nativeChatSettings.choose')}</Text>
<View style={[styles.section, styles.sectionTopGap]}>
<View style={styles.row}>
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>{t('m.LutlQWg')}</Text>
<Text style={styles.rowLabel}>{t('nativeChatSettings.open')}</Text>
<Text style={styles.rowSublabel}>
{chatDefault ? t('m.OXcAS68') : t('m.p0ydSMw')}
{chatDefault ? t('nativeChatSettings.on') : t('nativeChatSettings.off')}
</Text>
</View>
<Switch
accessibilityLabel={t('m.LutlQWg')}
accessibilityLabel={t('nativeChatSettings.open')}
value={chatDefault}
onValueChange={(next) => setDefaultView(next ? 'chat' : 'terminal')}
trackColor={{ false: colors.bgRaised, true: colors.textSecondary }}
+6 -4
View File
@@ -69,7 +69,9 @@ export default function NotificationsScreen() {
const switchEnabled = pushEnabled && permissionState.granted
const notificationsBlocked = permissionState.status === 'denied'
const hint = notificationsBlocked ? t('m.uzWRFSg') : t('m.dVAzjOs')
const hint = notificationsBlocked
? t('notifications.notificationsDisabled')
: t('notifications.get')
return (
<View style={[styles.container, { paddingTop: insets.top + spacing.sm }]}>
@@ -77,12 +79,12 @@ export default function NotificationsScreen() {
<Pressable style={styles.backButton} onPress={() => router.back()}>
<ChevronLeft size={22} color={colors.textSecondary} />
</Pressable>
<Text style={styles.heading}>{t('m.GIjQvDw')}</Text>
<Text style={styles.heading}>{t('notifications.notifications')}</Text>
</View>
<View style={styles.section}>
<View style={styles.row}>
<Text style={styles.rowLabel}>{t('m.aXWuT8I')}</Text>
<Text style={styles.rowLabel}>{t('notifications.agent')}</Text>
<Switch
value={switchEnabled}
disabled={notificationsBlocked}
@@ -100,7 +102,7 @@ export default function NotificationsScreen() {
]}
onPress={() => void Linking.openSettings()}
>
<Text style={styles.settingsButtonText}>{t('m.x7lqDGA')}</Text>
<Text style={styles.settingsButtonText}>{t('notifications.open')}</Text>
</Pressable>
)}
</View>
+14 -10
View File
@@ -136,8 +136,12 @@ export default function PairConfirmScreen() {
setStatus('error')
setErrorMessage(
timedOut
? t('m.uZiEDJ8', { value0: PAIRING_OVERALL_TIMEOUT_MS / 1000 })
: t('m.AHFOidY', { value0: err instanceof Error ? err.message : String(err) })
? t('pairConfirm.could', {
pairingOverallTimeoutSeconds: PAIRING_OVERALL_TIMEOUT_MS / 1000
})
: t('pairConfirm.pairingFailed', {
errorMessage: err instanceof Error ? err.message : String(err)
})
)
}
}
@@ -153,14 +157,14 @@ export default function PairConfirmScreen() {
<View style={styles.content}>
{offer && resolvedStatus === 'awaiting-confirm' && (
<>
<Text style={styles.title}>{t('m.ymuJ-Uc')}</Text>
<Text style={styles.subtitle}>{t('m.3LfZ8xU')}</Text>
<Text style={styles.title}>{t('pairConfirm.pairDesktop')}</Text>
<Text style={styles.subtitle}>{t('pairConfirm.confirmDesktopPairingLink')}</Text>
<View style={styles.actionStack}>
<Pressable style={styles.primaryButton} onPress={() => void confirm()}>
<Text style={styles.primaryButtonText}>{t('m.V-NORJA')}</Text>
<Text style={styles.primaryButtonText}>{t('pairConfirm.pair')}</Text>
</Pressable>
<Pressable style={styles.secondaryButton} onPress={cancel}>
<Text style={styles.secondaryButtonText}>{t('m.gZ1SfD8')}</Text>
<Text style={styles.secondaryButtonText}>{t('pairConfirm.cancel')}</Text>
</Pressable>
</View>
</>
@@ -169,9 +173,9 @@ export default function PairConfirmScreen() {
{resolvedStatus === 'connecting' && (
<>
<ActivityIndicator size="large" color={colors.textSecondary} />
<Text style={styles.connectingText}>{t('m.zDZEMlw')}</Text>
<Text style={styles.connectingText}>{t('pairConfirm.connecting')}</Text>
<View style={styles.logSlot}>
<ConnectionLog entries={logs} title={t('m.2r4Wqzo')} />
<ConnectionLog entries={logs} title={t('pairConfirm.pairingLog')} />
</View>
</>
)}
@@ -181,12 +185,12 @@ export default function PairConfirmScreen() {
<Text style={styles.errorText}>{resolvedErrorMessage}</Text>
{logs.length > 0 && (
<View style={styles.logSlot}>
<ConnectionLog entries={logs} title={t('m.2r4Wqzo')} />
<ConnectionLog entries={logs} title={t('pairConfirm.pairingLog')} />
</View>
)}
<View style={styles.actionStack}>
<Pressable style={styles.primaryButton} onPress={cancel}>
<Text style={styles.primaryButtonText}>{t('m.sKiKFEI')}</Text>
<Text style={styles.primaryButtonText}>{t('pairConfirm.back')}</Text>
</Pressable>
</View>
</>
+31 -23
View File
@@ -83,7 +83,7 @@ export default function PairScanScreen() {
const offer = decodePairingUrl(data)
if (!offer) {
setStatus('error')
setErrorMessage(t('m.iHaWj50'))
setErrorMessage(t('pairScan.notValidOrca'))
processingRef.current = false
return
}
@@ -103,7 +103,7 @@ export default function PairScanScreen() {
const offer = parsePairingCode(input)
if (!offer) {
setStatus('error')
setErrorMessage(t('m.feaDGg4'))
setErrorMessage(t('pairScan.notValidPairing'))
processingRef.current = false
return
}
@@ -180,8 +180,12 @@ export default function PairScanScreen() {
setStatus('error')
setErrorMessage(
timedOut
? t('m.zyTFKp4', { value0: PAIRING_OVERALL_TIMEOUT_MS / 1000 })
: t('m.HVl7cpc', { value0: err instanceof Error ? err.message : String(err) })
? t('pairScan.could', {
pairingOverallTimeoutSeconds: PAIRING_OVERALL_TIMEOUT_MS / 1000
})
: t('pairScan.pairingFailed', {
errorMessage: err instanceof Error ? err.message : String(err)
})
)
processingRef.current = false
}
@@ -225,15 +229,19 @@ export default function PairScanScreen() {
<ChevronLeft size={22} color={colors.textSecondary} />
</Pressable>
<View style={styles.centered}>
<Text style={styles.title}>{canAskAgain ? t('m.0VOtRkg') : t('m.sBkGoYA')}</Text>
<Text style={styles.subtitle}>{canAskAgain ? t('m.InfekMg') : t('m.b6n4c_8')}</Text>
<Text style={styles.title}>
{canAskAgain ? t('pairScan.pair') : t('pairScan.camera')}
</Text>
<Text style={styles.subtitle}>
{canAskAgain ? t('pairScan.scanOrPastePairingCode') : t('pairScan.enable')}
</Text>
<Pressable
style={styles.primaryButton}
onPress={canAskAgain ? requestPermission : () => void Linking.openSettings()}
>
{canAskAgain && <QrCode size={16} color={colors.bgBase} />}
<Text style={styles.primaryButtonText}>
{canAskAgain ? t('m.Scz67W0') : t('m.wvDgXHk')}
{canAskAgain ? t('pairScan.continue') : t('pairScan.openSettings')}
</Text>
</Pressable>
<Pressable
@@ -241,14 +249,14 @@ export default function PairScanScreen() {
onPress={() => setPasteVisible(true)}
>
<ClipboardIcon size={16} color={colors.textSecondary} />
<Text style={styles.pasteButtonText}>{t('m.Q_qrUNw')}</Text>
<Text style={styles.pasteButtonText}>{t('pairScan.pasteCode')}</Text>
</Pressable>
</View>
<TextInputModal
visible={pasteVisible}
title={t('m.klFU7hQ')}
message={t('m.G-MJBZU')}
placeholder={t('m.-jAAi0E')}
title={t('pairScan.pastePairingCode')}
message={t('pairScan.copy')}
placeholder={t('pairScan.orca')}
onSubmit={handlePasteSubmit}
onCancel={() => setPasteVisible(false)}
/>
@@ -263,9 +271,9 @@ export default function PairScanScreen() {
</Pressable>
<View style={styles.steps}>
<Step number={1} text={t('m.dFxLHEw')} />
<Step number={2} text={t('m.mPdpsdo')} />
<Step number={3} text={t('m.dTFolus')} />
<Step number={1} text={t('pairScan.openOrca')} />
<Step number={2} text={t('pairScan.go')} />
<Step number={3} text={t('pairScan.scan')} />
</View>
{status === 'scanning' && (
@@ -299,7 +307,7 @@ export default function PairScanScreen() {
onPress={() => setPasteVisible(true)}
>
<ClipboardIcon size={16} color={colors.textSecondary} />
<Text style={styles.pasteButtonText}>{t('m.U_fkYz4')}</Text>
<Text style={styles.pasteButtonText}>{t('pairScan.pastePairingCodeMessage')}</Text>
</Pressable>
</>
)}
@@ -307,9 +315,9 @@ export default function PairScanScreen() {
{status === 'connecting' && (
<View style={styles.centered}>
<ActivityIndicator size="large" color={colors.textSecondary} />
<Text style={styles.connectingText}>{t('m.T3HZLEU')}</Text>
<Text style={styles.connectingText}>{t('pairScan.connecting')}</Text>
<View style={styles.logSlot}>
<ConnectionLog entries={logs} title={t('m.G_n7Zxg')} />
<ConnectionLog entries={logs} title={t('pairScan.pairingLog')} />
</View>
</View>
)}
@@ -319,12 +327,12 @@ export default function PairScanScreen() {
<Text style={styles.errorText}>{errorMessage}</Text>
{logs.length > 0 && (
<View style={styles.logSlot}>
<ConnectionLog entries={logs} title={t('m.G_n7Zxg')} />
<ConnectionLog entries={logs} title={t('pairScan.pairingLog')} />
</View>
)}
<View style={styles.errorActions}>
<Pressable style={styles.primaryButton} onPress={retry}>
<Text style={styles.primaryButtonText}>{t('m._C6l3j4')}</Text>
<Text style={styles.primaryButtonText}>{t('pairScan.try')}</Text>
</Pressable>
<Pressable
style={({ pressed }) => [
@@ -336,7 +344,7 @@ export default function PairScanScreen() {
setPasteVisible(true)
}}
>
<Text style={styles.secondaryButtonText}>{t('m.Q_qrUNw')}</Text>
<Text style={styles.secondaryButtonText}>{t('pairScan.pasteCode')}</Text>
</Pressable>
</View>
</View>
@@ -344,9 +352,9 @@ export default function PairScanScreen() {
<TextInputModal
visible={pasteVisible}
title={t('m.klFU7hQ')}
message={t('m.G-MJBZU')}
placeholder={t('m.-jAAi0E')}
title={t('pairScan.pastePairingCode')}
message={t('pairScan.copy')}
placeholder={t('pairScan.orca')}
onSubmit={handlePasteSubmit}
onCancel={() => setPasteVisible(false)}
/>
+2 -2
View File
@@ -46,9 +46,9 @@ export default function PairRedirectScreen() {
<View style={styles.container}>
{missingCode ? (
<>
<Text style={styles.errorText}>{t('m.eQE_ziw')}</Text>
<Text style={styles.errorText}>{t('pair.missing')}</Text>
<Pressable style={styles.primaryButton} onPress={goHome}>
<Text style={styles.primaryButtonText}>{t('m.AIhB-fM')}</Text>
<Text style={styles.primaryButtonText}>{t('pair.back')}</Text>
</Pressable>
</>
) : (
+23 -18
View File
@@ -99,7 +99,7 @@ export default function SettingsScreen() {
<Pressable style={styles.backButton} onPress={() => router.back()}>
<ChevronLeft size={22} color={colors.textSecondary} />
</Pressable>
<Text style={styles.heading}>{t('m.0cOgjik')}</Text>
<Text style={styles.heading}>{t('settings.settings')}</Text>
</View>
<ScrollView
@@ -112,7 +112,7 @@ export default function SettingsScreen() {
onPress={() => router.push('/terminal-settings')}
>
<TerminalIcon size={16} color={colors.textSecondary} />
<Text style={styles.rowLabel}>{t('m.QlyWJyY')}</Text>
<Text style={styles.rowLabel}>{t('settings.terminal')}</Text>
<ChevronRight size={16} color={colors.textMuted} />
</Pressable>
<View style={styles.separator} />
@@ -121,7 +121,7 @@ export default function SettingsScreen() {
onPress={() => router.push('/native-chat-settings')}
>
<MessageSquare size={16} color={colors.textSecondary} />
<Text style={styles.rowLabel}>{t('m.-aB74WM')}</Text>
<Text style={styles.rowLabel}>{t('settings.chat')}</Text>
<ChevronRight size={16} color={colors.textMuted} />
</Pressable>
<View style={styles.separator} />
@@ -130,7 +130,7 @@ export default function SettingsScreen() {
onPress={() => router.push('/browser-settings')}
>
<Globe size={16} color={colors.textSecondary} />
<Text style={styles.rowLabel}>{t('m.ttAM7Ds')}</Text>
<Text style={styles.rowLabel}>{t('settings.browser')}</Text>
<ChevronRight size={16} color={colors.textMuted} />
</Pressable>
<View style={styles.separator} />
@@ -139,7 +139,7 @@ export default function SettingsScreen() {
onPress={() => router.push('/voice-settings')}
>
<Mic size={16} color={colors.textSecondary} />
<Text style={styles.rowLabel}>{t('m.rm30Zkg')}</Text>
<Text style={styles.rowLabel}>{t('settings.voice')}</Text>
<ChevronRight size={16} color={colors.textMuted} />
</Pressable>
<View style={styles.separator} />
@@ -148,7 +148,7 @@ export default function SettingsScreen() {
onPress={() => router.push('/notifications')}
>
<Bell size={16} color={colors.textSecondary} />
<Text style={styles.rowLabel}>{t('m.i8QR0SY')}</Text>
<Text style={styles.rowLabel}>{t('settings.notifications')}</Text>
<ChevronRight size={16} color={colors.textMuted} />
</Pressable>
<View style={styles.separator} />
@@ -157,7 +157,7 @@ export default function SettingsScreen() {
onPress={() => router.push('/troubleshoot')}
>
<Wrench size={16} color={colors.textSecondary} />
<Text style={styles.rowLabel}>{t('m.0aipMZQ')}</Text>
<Text style={styles.rowLabel}>{t('settings.troubleshooting')}</Text>
<ChevronRight size={16} color={colors.textMuted} />
</Pressable>
<View style={styles.separator} />
@@ -166,7 +166,7 @@ export default function SettingsScreen() {
onPress={() => router.push('/about')}
>
<Info size={16} color={colors.textSecondary} />
<Text style={styles.rowLabel}>{t('m.EELdtVU')}</Text>
<Text style={styles.rowLabel}>{t('settings.about')}</Text>
<ChevronRight size={16} color={colors.textMuted} />
</Pressable>
</View>
@@ -176,20 +176,25 @@ export default function SettingsScreen() {
<View style={styles.credentialCleanupRow}>
<KeyRound size={16} color={colors.statusAmber} />
<View style={styles.credentialCleanupCopy}>
<Text style={styles.credentialCleanupTitle}>{t('m.BFrWnfA')}</Text>
<Text style={styles.credentialCleanupTitle}>{t('settings.pairing')}</Text>
<Text accessibilityLiveRegion="polite" style={styles.rowHint}>
{credentialRetryFailed
? t('m.K-uhruk')
? t('settings.cleanup')
: pendingCredentialCount > 0
? t(pendingCredentialCount === 1 ? 'm.DCkRqC8' : 'm.XStfhOk', {
value0: pendingCredentialCount
})
: t('m.kiQMppk')}
? t(
pendingCredentialCount === 1
? 'settings.couldNotConfirmCleanupPendingCredentialCountCredential'
: 'settings.couldNotConfirmCleanupPendingCredentialCountCredentials',
{
pendingCredentialCount: pendingCredentialCount
}
)
: t('settings.couldNotCheck')}
</Text>
</View>
<Pressable
accessibilityRole="button"
accessibilityLabel={t('m.ks7el1o')}
accessibilityLabel={t('settings.retryClearing')}
accessibilityState={{
busy: retryingCredentialCleanup,
disabled: retryingCredentialCleanup
@@ -205,7 +210,7 @@ export default function SettingsScreen() {
{retryingCredentialCleanup ? (
<ActivityIndicator size="small" color={colors.textSecondary} />
) : (
<Text style={styles.retryButtonText}>{t('m.EJmGYjM')}</Text>
<Text style={styles.retryButtonText}>{t('settings.retry')}</Text>
)}
</Pressable>
</View>
@@ -218,7 +223,7 @@ export default function SettingsScreen() {
onPress={() => void Linking.openURL('https://www.onorca.dev/privacy')}
>
<Shield size={16} color={colors.textSecondary} />
<Text style={styles.rowLabel}>{t('m.7jufUfc')}</Text>
<Text style={styles.rowLabel}>{t('settings.privacy')}</Text>
</Pressable>
<View style={styles.separator} />
<Pressable
@@ -226,7 +231,7 @@ export default function SettingsScreen() {
onPress={() => void Linking.openURL('https://github.com/stablyai/orca/issues')}
>
<LifeBuoy size={16} color={colors.textSecondary} />
<Text style={styles.rowLabel}>{t('m.cFPanOU')}</Text>
<Text style={styles.rowLabel}>{t('settings.support')}</Text>
</Pressable>
</View>
</ScrollView>
+38 -24
View File
@@ -32,12 +32,12 @@ type TextSizeValue = 'smallest' | 'smaller' | 'default' | 'large' | 'larger' | '
// scale = baseline zoom the terminal WebView applies on top of fit-to-width.
// Keep in sync with TERMINAL_TEXT_SCALES; pinch-to-zoom snaps to these values.
const TEXT_SIZE_OPTIONS: (PickerOption<TextSizeValue> & { scale: number })[] = [
{ value: 'smallest', label: t('m.hiouM1M'), scale: 0.5 },
{ value: 'smaller', label: t('m.CAs8oUM'), scale: 0.75 },
{ value: 'default', label: t('m.0OKOsKY'), scale: 1 },
{ value: 'large', label: t('m.HqP6IBw'), scale: 1.25 },
{ value: 'larger', label: t('m.tAn81tY'), scale: 1.5 },
{ value: 'largest', label: t('m.75Kr_24'), scale: 2 }
{ value: 'smallest', label: t('terminalSettings.smallest'), scale: 0.5 },
{ value: 'smaller', label: t('terminalSettings.smaller'), scale: 0.75 },
{ value: 'default', label: t('terminalSettings.default'), scale: 1 },
{ value: 'large', label: t('terminalSettings.large'), scale: 1.25 },
{ value: 'larger', label: t('terminalSettings.larger'), scale: 1.5 },
{ value: 'largest', label: t('terminalSettings.largest'), scale: 2 }
]
function textSizeValueFromScale(scale: number): TextSizeValue {
@@ -49,10 +49,10 @@ function textSizeSummary(scale: number): string {
}
const AUTO_RESTORE_FIT_OPTIONS: (PickerOption<RestoreValue> & { ms: number | null })[] = [
{ value: 'indefinite', label: t('m.q6VQcZE'), ms: null },
{ value: '60s', label: t('m.gBGOsCI'), ms: 60_000 },
{ value: '5m', label: t('m.p2EWmRg'), ms: 5 * 60_000 },
{ value: '30m', label: t('m.rU9qdt0'), ms: 30 * 60_000 }
{ value: 'indefinite', label: t('terminalSettings.keep'), ms: null },
{ value: '60s', label: t('terminalSettings.after1'), ms: 60_000 },
{ value: '5m', label: t('terminalSettings.after5'), ms: 5 * 60_000 },
{ value: '30m', label: t('terminalSettings.after30'), ms: 30 * 60_000 }
]
function valueFromMs(ms: number | null | undefined): RestoreValue {
@@ -90,7 +90,9 @@ function autoRestoreSummary(ms: number | null | undefined): string {
return AUTO_RESTORE_FIT_OPTIONS[0]!.label
}
const exact = AUTO_RESTORE_FIT_OPTIONS.find((o) => o.ms === ms)
return exact ? exact.label : t('m.WVNYnak', { value0: Math.round(ms / 1000) })
return exact
? exact.label
: t('terminalSettings.afterDuration', { durationSeconds: Math.round(ms / 1000) })
}
function HostFitRow({
@@ -260,7 +262,7 @@ export default function TerminalSettingsScreen() {
<Pressable style={styles.backButton} onPress={() => router.back()}>
<ChevronLeft size={22} color={colors.textSecondary} />
</Pressable>
<Text style={styles.heading}>{t('m.kJj5Z2g')}</Text>
<Text style={styles.heading}>{t('terminalSettings.terminal')}</Text>
</View>
<Animated.ScrollView
@@ -273,12 +275,14 @@ export default function TerminalSettingsScreen() {
scrollContentHeight.value = height
}}
>
<Text style={styles.groupHeading}>{t('m.f9vgUHc')}</Text>
<Text style={styles.groupDescription}>{t('m.-fV3Ce8')}</Text>
<Text style={styles.groupHeading}>{t('terminalSettings.when')}</Text>
<Text style={styles.groupDescription}>
{t('terminalSettings.resizeBehaviorDescription')}
</Text>
{hosts.length === 0 ? (
<View style={[styles.section, styles.sectionTopGap]}>
<Text style={styles.emptyText}>{t('m.U2mtyZg')}</Text>
<Text style={styles.emptyText}>{t('terminalSettings.no')}</Text>
</View>
) : (
<View style={[styles.section, styles.sectionTopGap]}>
@@ -299,8 +303,10 @@ export default function TerminalSettingsScreen() {
</View>
)}
<Text style={[styles.groupHeading, styles.inputGroupGap]}>{t('m.OS-rNTI')}</Text>
<Text style={styles.groupDescription}>{t('m.nzfG92c')}</Text>
<Text style={[styles.groupHeading, styles.inputGroupGap]}>
{t('terminalSettings.textSize')}
</Text>
<Text style={styles.groupDescription}>{t('terminalSettings.textScaleDescription')}</Text>
<View style={[styles.section, styles.sectionTopGap]}>
<Pressable
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
@@ -308,21 +314,23 @@ export default function TerminalSettingsScreen() {
>
<Type size={16} color={colors.textSecondary} />
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>{t('m.QcL6LT4')}</Text>
<Text style={styles.rowLabel}>{t('terminalSettings.textSizeMessage')}</Text>
<Text style={styles.rowSublabel}>{textSizeSummary(textScale)}</Text>
</View>
<ChevronRight size={16} color={colors.textMuted} />
</Pressable>
</View>
<Text style={[styles.groupHeading, styles.inputGroupGap]}>{t('m.18J3yr0')}</Text>
<Text style={styles.groupDescription}>{t('m.mK_kCfI')}</Text>
<Text style={[styles.groupHeading, styles.inputGroupGap]}>
{t('terminalSettings.keyboard')}
</Text>
<Text style={styles.groupDescription}>{t('terminalSettings.autocompleteDescription')}</Text>
<View style={[styles.section, styles.sectionTopGap]}>
<View style={styles.row}>
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>{t('m.AsTLd6A')}</Text>
<Text style={styles.rowLabel}>{t('terminalSettings.autocomplete')}</Text>
<Text style={styles.rowSublabel}>
{autocompleteEnabled ? t('m.qR5mpHo') : t('m.rE4IiF8')}
{autocompleteEnabled ? t('terminalSettings.on') : t('terminalSettings.off')}
</Text>
</View>
<Switch
@@ -344,7 +352,13 @@ export default function TerminalSettingsScreen() {
<PickerModal<RestoreValue>
visible={pickerHost != null}
title={pickerHost ? t('m.nN25Xmo', { value0: pickerHost.name }) : ''}
title={
pickerHost
? t('terminalSettings.restore', {
pickerHostName: pickerHost.name
})
: ''
}
options={AUTO_RESTORE_FIT_OPTIONS}
selected={valueFromMs(pickerHost ? hostMs[pickerHost.id] : null)}
onSelect={(v) => {
@@ -357,7 +371,7 @@ export default function TerminalSettingsScreen() {
<PickerModal<TextSizeValue>
visible={textSizePickerOpen}
title={t('m.lfAeAOI')}
title={t('terminalSettings.terminalText')}
options={TEXT_SIZE_OPTIONS}
selected={textSizeValueFromScale(textScale)}
onSelect={selectTextSize}
+42 -16
View File
@@ -96,14 +96,22 @@ export default function TroubleshootScreen() {
results.push(
hosts.length > 0
? {
label: t('m._IfUZ2s'),
label: t('troubleshoot.paired'),
status: 'pass',
detail: t('m.OCWSDG0', { value0: hosts.length })
detail: t('troubleshoot.host', { hostCount: hosts.length })
}
: {
label: t('troubleshoot.paired'),
status: 'fail',
detail: t('troubleshoot.none')
}
: { label: t('m._IfUZ2s'), status: 'fail', detail: t('m.AtwKjZ4') }
)
} catch {
results.push({ label: t('m._IfUZ2s'), status: 'warn', detail: t('m.uJRC49k') })
results.push({
label: t('troubleshoot.paired'),
status: 'warn',
detail: t('troubleshoot.couldNotRead')
})
}
if (!isCurrentRun()) {
@@ -122,14 +130,26 @@ export default function TroubleshootScreen() {
}
results.push(
resp.ok
? { label: t('m.3gA9qeo'), status: 'pass', detail: t('m.O1SHffI') }
: { label: t('m.3gA9qeo'), status: 'warn', detail: t('m.DxL3QsM') }
? {
label: t('troubleshoot.internet'),
status: 'pass',
detail: t('troubleshoot.connected')
}
: {
label: t('troubleshoot.internet'),
status: 'warn',
detail: t('troubleshoot.unexpected')
}
)
} catch {
if (!isCurrentRun()) {
return
}
results.push({ label: t('m.3gA9qeo'), status: 'fail', detail: t('m.gdHptko') })
results.push({
label: t('troubleshoot.internet'),
status: 'fail',
detail: t('troubleshoot.no')
})
} finally {
internetCheck.dispose()
if (activeInternetCheckRef.current === internetCheck) {
@@ -156,13 +176,19 @@ export default function TroubleshootScreen() {
label: host.name,
status: reachable ? 'pass' : 'fail',
detail: reachable
? t('m.ImyX3J0', { value0: formatEndpoint(host.endpoint) })
? t('troubleshoot.reachable', {
endpoint: formatEndpoint(host.endpoint)
})
: unreachableHostDetail(host.endpoint)
})
setChecks([...results])
}
} catch {
results.push({ label: t('m.Ie918PM'), status: 'warn', detail: t('m._wFCKE8') })
results.push({
label: t('troubleshoot.hosts'),
status: 'warn',
detail: t('troubleshoot.couldNotTest')
})
}
if (!isCurrentRun()) {
@@ -170,7 +196,7 @@ export default function TroubleshootScreen() {
}
results.push({
label: t('m.8sOFcoA'),
label: t('troubleshoot.platform'),
status: 'pass',
detail: `${Platform.OS} ${Platform.Version ?? ''}`
})
@@ -188,7 +214,7 @@ export default function TroubleshootScreen() {
<Pressable style={styles.backButton} onPress={() => router.back()}>
<ChevronLeft size={22} color={colors.textSecondary} />
</Pressable>
<Text style={styles.heading}>{t('m.DIEmGuM')}</Text>
<Text style={styles.heading}>{t('troubleshoot.troubleshooting')}</Text>
</View>
<ScrollView
@@ -212,10 +238,10 @@ export default function TroubleshootScreen() {
)}
<Text style={styles.diagnosticButtonLabel}>
{diagnosticStatus === 'running'
? t('m.dQs8-jc')
? t('troubleshoot.running')
: diagnosticStatus === 'done'
? t('m.B5C2aso')
: t('m.5xQU6vw')}
? t('troubleshoot.runAgain')
: t('troubleshoot.runDiagnostics')}
</Text>
</Pressable>
@@ -227,7 +253,7 @@ export default function TroubleshootScreen() {
onPress={() => router.push('/connection-log')}
>
<ScrollText size={16} color={colors.textPrimary} />
<Text style={styles.diagnosticButtonLabel}>{t('m.bwJLtK4')}</Text>
<Text style={styles.diagnosticButtonLabel}>{t('troubleshoot.view')}</Text>
</Pressable>
{checks.length > 0 && (
@@ -249,7 +275,7 @@ export default function TroubleshootScreen() {
</View>
)}
<Text style={styles.sectionHeading}>{t('m.2J8vm70')}</Text>
<Text style={styles.sectionHeading}>{t('troubleshoot.common')}</Text>
<View style={styles.section}>
{getTroubleshootCommonIssues().map((section, i) => (
+24 -20
View File
@@ -33,8 +33,8 @@ import { t } from '@/i18n/mobile-i18n'
const POLL_INTERVAL_MS = 1500
const DICTATION_MODES = [
{ value: 'toggle', label: t('m.yxHp0Uc') },
{ value: 'hold', label: t('m.hYUl_w0') }
{ value: 'toggle', label: t('voiceSettings.toggle') },
{ value: 'hold', label: t('voiceSettings.hold') }
] as const
type ModelBusyAction = { modelId: string; type: 'download' | 'select' | 'delete' }
@@ -79,7 +79,7 @@ export default function VoiceSettingsScreen(): React.JSX.Element {
setError(null)
return next.models.some(isModelInFlight)
} catch (err) {
setError(err instanceof Error ? err.message : t('m.wFKD2Es'))
setError(err instanceof Error ? err.message : t('voiceSettings.failedLoadVoiceSettings'))
return undefined
} finally {
setLoading(false)
@@ -111,7 +111,7 @@ export default function VoiceSettingsScreen(): React.JSX.Element {
try {
setSetup(await setDictationConfig(client, { enabled }))
} catch (err) {
setError(err instanceof Error ? err.message : t('m.PiXz3AQ'))
setError(err instanceof Error ? err.message : t('voiceSettings.couldNotUpdate'))
void refreshSetup()
}
},
@@ -128,7 +128,7 @@ export default function VoiceSettingsScreen(): React.JSX.Element {
try {
setSetup(await setDictationConfig(client, { dictationMode }))
} catch (err) {
setError(err instanceof Error ? err.message : t('m.PiXz3AQ'))
setError(err instanceof Error ? err.message : t('voiceSettings.couldNotUpdate'))
void refreshSetup()
}
},
@@ -146,7 +146,7 @@ export default function VoiceSettingsScreen(): React.JSX.Element {
setSetup(await setDictationConfig(client, { enabled: true, modelId: model.id }))
setModelDrawerOpen(false)
} catch (err) {
setError(err instanceof Error ? err.message : t('m.cpL32fY'))
setError(err instanceof Error ? err.message : t('voiceSettings.couldNotSelect'))
} finally {
setBusyAction(null)
}
@@ -165,7 +165,7 @@ export default function VoiceSettingsScreen(): React.JSX.Element {
await downloadDictationModel(client, model.id)
await refreshSetup()
} catch (err) {
setError(err instanceof Error ? err.message : t('m.O-GK4L4'))
setError(err instanceof Error ? err.message : t('voiceSettings.download'))
} finally {
setBusyAction(null)
}
@@ -187,7 +187,7 @@ export default function VoiceSettingsScreen(): React.JSX.Element {
setModelDrawerOpen(false)
}
} catch (err) {
setError(err instanceof Error ? err.message : t('m.Wgx1aTY'))
setError(err instanceof Error ? err.message : t('voiceSettings.delete'))
} finally {
setBusyAction(null)
}
@@ -197,7 +197,7 @@ export default function VoiceSettingsScreen(): React.JSX.Element {
const enabled = setup?.enabled ?? false
const selectedModel = setup?.models.find((m) => m.id === setup.selectedModelId)
const selectedModelLabel = selectedModel?.label ?? t('m.J055vls')
const selectedModelLabel = selectedModel?.label ?? t('voiceSettings.none')
return (
<View style={[styles.container, { paddingTop: insets.top + spacing.sm }]}>
@@ -205,12 +205,12 @@ export default function VoiceSettingsScreen(): React.JSX.Element {
<Pressable style={styles.backButton} onPress={() => router.back()}>
<ChevronLeft size={22} color={colors.textSecondary} />
</Pressable>
<Text style={styles.heading}>{t('m.pSmQbNo')}</Text>
<Text style={styles.heading}>{t('voiceSettings.voice')}</Text>
</View>
{!client ? (
<View style={[styles.section, styles.sectionTopGap]}>
<Text style={styles.emptyText}>{t('m.pJrwjt0')}</Text>
<Text style={styles.emptyText}>{t('voiceSettings.connect')}</Text>
</View>
) : loading && setup === null ? (
<View style={styles.loading}>
@@ -218,19 +218,21 @@ export default function VoiceSettingsScreen(): React.JSX.Element {
</View>
) : setup === null ? (
<View style={[styles.section, styles.sectionTopGap]}>
<Text style={styles.errorText}>{error ?? t('m.Fbnf310')}</Text>
<Text style={styles.errorText}>
{error ?? t('voiceSettings.failedLoadVoiceSettingsMessage')}
</Text>
</View>
) : (
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<Text style={styles.groupHeading}>{t('m.L4c5Sxw')}</Text>
<Text style={styles.groupHeading}>{t('voiceSettings.dictation')}</Text>
<View style={[styles.section, styles.sectionTopGap]}>
<View style={styles.row}>
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>{t('m.23v3WVE')}</Text>
<Text style={styles.rowSublabel}>{t('m.4g1gMYA')}</Text>
<Text style={styles.rowLabel}>{t('voiceSettings.enable')}</Text>
<Text style={styles.rowSublabel}>{t('voiceSettings.dictate')}</Text>
</View>
<Switch
value={enabled}
@@ -247,8 +249,8 @@ export default function VoiceSettingsScreen(): React.JSX.Element {
pointerEvents={enabled ? 'auto' : 'none'}
>
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>{t('m.Jh4zuEU')}</Text>
<Text style={styles.rowSublabel}>{t('m.uwVWBGE')}</Text>
<Text style={styles.rowLabel}>{t('voiceSettings.dictationMode')}</Text>
<Text style={styles.rowSublabel}>{t('voiceSettings.togglePress')}</Text>
</View>
<View style={styles.segmented}>
{DICTATION_MODES.map((mode) => {
@@ -269,7 +271,9 @@ export default function VoiceSettingsScreen(): React.JSX.Element {
</View>
</View>
<Text style={[styles.groupHeading, styles.inputGroupGap]}>{t('m.n-BJK-c')}</Text>
<Text style={[styles.groupHeading, styles.inputGroupGap]}>
{t('voiceSettings.speechModel')}
</Text>
<View style={[styles.section, styles.sectionTopGap]}>
<Pressable
style={({ pressed }) => [
@@ -281,7 +285,7 @@ export default function VoiceSettingsScreen(): React.JSX.Element {
onPress={() => setModelDrawerOpen(true)}
>
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>{t('m.C-t7pm8')}</Text>
<Text style={styles.rowLabel}>{t('voiceSettings.speechModelMessage')}</Text>
<Text style={styles.rowSublabel} numberOfLines={1}>
{selectedModelLabel}
</Text>
@@ -295,7 +299,7 @@ export default function VoiceSettingsScreen(): React.JSX.Element {
)}
<BottomDrawer visible={modelDrawerOpen} onClose={() => setModelDrawerOpen(false)}>
<Text style={styles.drawerTitle}>{t('m.C-t7pm8')}</Text>
<Text style={styles.drawerTitle}>{t('voiceSettings.speechModelMessage')}</Text>
{setup ? (
<VoiceModelList
setup={setup}
@@ -130,11 +130,16 @@ function AgentHistoryCardRow({
<View style={styles.cardMetaRow}>
<Text style={styles.cardMetaText}>{card.agentLabel}</Text>
<Text style={styles.cardMetaText}>
{card.messageCount} {card.messageCount === 1 ? t('m.fZXkGhQ') : t('m.tL7Cy3c')}
{card.messageCount}{' '}
{card.messageCount === 1
? t('mobileAgentSessionHistoryList.message')
: t('mobileAgentSessionHistoryList.messages')}
</Text>
{showCurrentWorktreeBadge && card.isCurrentWorktree ? (
<View style={styles.currentBadge}>
<Text style={styles.currentBadgeText}>{t('m.f1-v-HM')}</Text>
<Text style={styles.currentBadgeText}>
{t('mobileAgentSessionHistoryList.current')}
</Text>
</View>
) : null}
{session && onResume ? (
@@ -153,7 +158,7 @@ function AgentHistoryCardRow({
disabled={resumeActionState?.disabled}
hitSlop={8}
accessibilityRole="button"
accessibilityLabel={t('m.p7fz5tw')}
accessibilityLabel={t('mobileAgentSessionHistoryList.resume')}
>
{resumeActionState?.loading ? (
<ActivityIndicator size="small" color={colors.textPrimary} />
@@ -28,15 +28,13 @@ import { useMobileAgentHistoryState } from './use-mobile-agent-history-state'
import { buildMobileAgentHistorySections } from './agent-history-sections'
import { shouldShowMobileCurrentWorktreeBadge } from './agent-history-current-worktree-badge'
import { MobileAgentSessionHistoryList } from './MobileAgentSessionHistoryList'
import {
resolveMobileAiVaultSessionResumeTarget,
type MobileAiVaultResumeFolderWorkspace,
type MobileAiVaultResumeProjectGroup,
type MobileAiVaultResumeRepo
} from './agent-history-resume-target'
import { resolveMobileAiVaultSessionResumeTarget } from './agent-history-resume-target'
import type * as ResumeTarget from './agent-history-resume-target'
import { buildMobileAgentHistoryResumeActionState } from './agent-history-session-card'
import { styles } from './agent-history-styles'
import { t } from '@/i18n/mobile-i18n'
import { createMobileTranslator } from '@/i18n/mobile-i18n'
const tr = createMobileTranslator('agentHistory')
export type MobileAgentSessionHistoryPanelProps = {
hostId: string
@@ -45,9 +43,9 @@ export type MobileAgentSessionHistoryPanelProps = {
}
const SCOPE_TABS: { scope: AiVaultScope; label: string }[] = [
{ scope: 'workspace', label: t('m.0k6Zchk') },
{ scope: 'project', label: t('m.dhgeP_o') },
{ scope: 'all', label: t('m.rcPfxtE') }
{ scope: 'workspace', label: tr('workspace') },
{ scope: 'project', label: tr('project') },
{ scope: 'all', label: tr('all') }
]
export function MobileAgentSessionHistoryPanel({
@@ -151,12 +149,12 @@ export function MobileAgentSessionHistoryPanel({
return
}
if (!client || connState !== 'connected') {
setResumeMessage(t('m.BLr07Ao'))
setResumeMessage(tr('waiting'))
triggerError()
return
}
if (!session.sessionId) {
setResumeMessage(t('m.8fvhHmw'))
setResumeMessage(tr('session'))
triggerError()
return
}
@@ -196,7 +194,7 @@ export function MobileAgentSessionHistoryPanel({
target.terminalPlatform
)
if (!platform) {
setResumeMessage(t('m.ADvyKkM'))
setResumeMessage(tr('unableDetermine'))
triggerError()
return
}
@@ -214,7 +212,7 @@ export function MobileAgentSessionHistoryPanel({
})
resumeMutationRegistryRef.current.releaseOnSuccess(session.id)
triggerSuccess()
setResumeMessage(t('m.sibM7yE'))
setResumeMessage(tr('agentSessionQueued'))
router.push(
`/h/${encodeURIComponent(hostId)}/session/${encodeURIComponent(target.worktreeId)}` as Parameters<
typeof router.push
@@ -222,7 +220,7 @@ export function MobileAgentSessionHistoryPanel({
)
} catch (err) {
triggerError()
setResumeMessage(err instanceof Error ? err.message : t('m.HsI5rsc'))
setResumeMessage(err instanceof Error ? err.message : tr('failed'))
} finally {
resumeLaunchInFlightRef.current = false
setResumingSessionId(null)
@@ -248,13 +246,13 @@ export function MobileAgentSessionHistoryPanel({
style={({ pressed }) => [styles.backButton, pressed && styles.backButtonPressed]}
onPress={() => router.back()}
hitSlop={8}
accessibilityLabel={t('m.bGNaHw8')}
accessibilityLabel={tr('back')}
>
<ChevronLeft size={22} color={colors.textSecondary} strokeWidth={2.2} />
</Pressable>
<View style={styles.titleBlock}>
<Text style={styles.title} numberOfLines={1}>
{t('m.z2KOXZE')}
{tr('agentSessionHistory')}
</Text>
<Text style={styles.meta} numberOfLines={1}>
{worktreeLabel}
@@ -264,7 +262,7 @@ export function MobileAgentSessionHistoryPanel({
style={({ pressed }) => [styles.refreshButton, pressed && styles.refreshButtonPressed]}
onPress={() => void onRefresh()}
hitSlop={8}
accessibilityLabel={t('m.Qvd5WD0')}
accessibilityLabel={tr('refresh')}
>
<RefreshCw size={18} color={colors.textSecondary} strokeWidth={2.1} />
</Pressable>
@@ -277,15 +275,15 @@ export function MobileAgentSessionHistoryPanel({
</View>
) : screenState.kind === 'unsupported' ? (
<View style={styles.state}>
<Text style={styles.stateTitle}>{t('m.yRlsHYk')}</Text>
<Text style={styles.stateText}>{t('m.D1lHtt4')}</Text>
<Text style={styles.stateTitle}>{tr('agentSessionHistoryUnavailable')}</Text>
<Text style={styles.stateText}>{tr('update')}</Text>
</View>
) : screenState.kind === 'error' ? (
<View style={styles.state}>
<Text style={styles.stateTitle}>{t('m.IFh38kk')}</Text>
<Text style={styles.stateTitle}>{tr('unableLoad')}</Text>
<Text style={styles.stateText}>{screenState.message}</Text>
<Pressable style={styles.retryButton} onPress={retry}>
<Text style={styles.retryText}>{t('m.7OL03t8')}</Text>
<Text style={styles.retryText}>{tr('retry')}</Text>
</Pressable>
</View>
) : (
@@ -311,7 +309,7 @@ export function MobileAgentSessionHistoryPanel({
style={styles.searchInput}
value={query}
onChangeText={setQuery}
placeholder={t('m.Uc72mxE')}
placeholder={tr('search')}
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
@@ -320,8 +318,8 @@ export function MobileAgentSessionHistoryPanel({
{issues.length > 0 ? (
<View style={styles.noticeBanner}>
<Text style={styles.noticeText}>
{t(issues.length === 1 ? 'm.xnvhKbA' : 'm.UXXYuws', {
value0: issues.length
{tr(issues.length === 1 ? 'issueCountTranscript' : 'issueCountTranscripts', {
issueCount: issues.length
})}
</Text>
</View>
@@ -333,8 +331,8 @@ export function MobileAgentSessionHistoryPanel({
) : null}
{sections.length === 0 ? (
<View style={styles.state}>
<Text style={styles.stateTitle}>{t('m.KkgSO8k')}</Text>
<Text style={styles.stateText}>{query ? t('m.X8748Hs') : t('m.L_8qprI')}</Text>
<Text style={styles.stateTitle}>{tr('noAgent')}</Text>
<Text style={styles.stateText}>{query ? tr('noSessions') : tr('noPast')}</Text>
</View>
) : (
<MobileAgentSessionHistoryList
@@ -357,9 +355,9 @@ const EMPTY_SESSIONS: AiVaultSession[] = []
const EMPTY_ISSUES: { agent: AiVaultSession['agent']; path: string; message: string }[] = []
async function loadMobileResumeMetadata(client: Pick<RpcClient, 'sendRequest'>): Promise<{
repos: MobileAiVaultResumeRepo[]
folderWorkspaces: MobileAiVaultResumeFolderWorkspace[]
projectGroups: MobileAiVaultResumeProjectGroup[]
repos: ResumeTarget.MobileAiVaultResumeRepo[]
folderWorkspaces: ResumeTarget.MobileAiVaultResumeFolderWorkspace[]
projectGroups: ResumeTarget.MobileAiVaultResumeProjectGroup[]
settings: MobileAiVaultResumeSettings | null
worktrees: Worktree[] | null
}> {
@@ -389,18 +387,18 @@ async function loadMobileResumeMetadata(client: Pick<RpcClient, 'sendRequest'>):
.catch(() => null)
])
if (!repoResponse.ok) {
throw new Error(repoResponse.error?.message || t('m.hRiZhQg'))
throw new Error(repoResponse.error?.message || tr('unableLoadWorkspace'))
}
const repoResult = repoResponse.result as { repos?: MobileAiVaultResumeRepo[] }
const repoResult = repoResponse.result as { repos?: ResumeTarget.MobileAiVaultResumeRepo[] }
const folderWorkspaceResult =
folderWorkspaceResponse?.ok === true
? (folderWorkspaceResponse.result as {
folderWorkspaces?: MobileAiVaultResumeFolderWorkspace[]
folderWorkspaces?: ResumeTarget.MobileAiVaultResumeFolderWorkspace[]
})
: null
const projectGroupResult =
projectGroupResponse?.ok === true
? (projectGroupResponse.result as { groups?: MobileAiVaultResumeProjectGroup[] })
? (projectGroupResponse.result as { groups?: ResumeTarget.MobileAiVaultResumeProjectGroup[] })
: null
const settingsResult =
settingsResponse?.ok === true
@@ -107,12 +107,12 @@ export function mobileAiVaultResumeTargetBlockMessage(
status: MobileAiVaultResumeTargetStatus
): string {
if (status === 'runtime') {
return t('m.GLFjyPk')
return t('agentHistoryResumeTarget.resume')
}
if (status === 'ssh') {
return t('m.eTFpmzo')
return t('agentHistoryResumeTarget.session')
}
return t('m.mWMZ-dk')
return t('agentHistoryResumeTarget.open')
}
export function resolveMobileAiVaultSessionResumeTarget(args: {
@@ -45,7 +45,7 @@ export function buildMobileAgentHistoryCard(
id: session.id,
agent: session.agent,
agentLabel: aiVaultAgentLabel(session.agent),
title: session.title || t('m.VgO3m0E'),
title: session.title || t('agentHistorySessionCard.untitled'),
lastMessage: latestTurn?.text.trim() ?? '',
messageCount: session.messageCount,
timeAgo: Number.isFinite(updatedAtMs) ? formatTimeAgo(updatedAtMs, now) : '',
@@ -79,7 +79,12 @@ export function useMobileAgentHistoryState(params: MobileAgentHistoryStateParams
// (connState flips re-run the load effect) instead of tearing it
// down to a full-screen error, matching the host list screen.
setScreenState((prev) =>
prev.kind === 'ready' ? prev : { kind: 'error', message: t('m.nSh69OQ') }
prev.kind === 'ready'
? prev
: {
kind: 'error',
message: t('useMobileAgentHistoryState.waiting')
}
)
}
return
@@ -94,7 +99,9 @@ export function useMobileAgentHistoryState(params: MobileAgentHistoryStateParams
return
}
if (!statusResponse.ok) {
throw new Error(statusResponse.error?.message || t('m.THbZzp4'))
throw new Error(
statusResponse.error?.message || t('useMobileAgentHistoryState.unableReach')
)
}
const status = (statusResponse as RpcSuccess).result as StatusWithCapabilities
setHostStatusResult(status)
@@ -124,7 +131,7 @@ export function useMobileAgentHistoryState(params: MobileAgentHistoryStateParams
return
}
if (!response.ok) {
throw new Error(response.error?.message || t('m.ukxwfJ8'))
throw new Error(response.error?.message || t('useMobileAgentHistoryState.unableLoad'))
}
const result = (response as RpcSuccess).result as AiVaultListResult
setScreenState({ kind: 'ready', sessions: result.sessions, issues: result.issues })
@@ -132,7 +139,8 @@ export function useMobileAgentHistoryState(params: MobileAgentHistoryStateParams
if (!isCurrent()) {
return
}
const message = err instanceof Error ? err.message : t('m.ukxwfJ8')
const message =
err instanceof Error ? err.message : t('useMobileAgentHistoryState.unableLoad')
setHostStatusResult(null)
setScreenState({ kind: 'error', message })
}
@@ -22,7 +22,7 @@ type Args = {
export function buildWorktreeNavigationActions(args: Args): ActionSheetAction[] {
const actions: ActionSheetAction[] = [
{
label: t('m.XjyxBLA'),
label: t('worktreeNavigationActions.source'),
icon: GitBranch,
onPress: () => {
const params = new URLSearchParams({ name: args.worktreeName, origin: 'host' })
@@ -35,7 +35,7 @@ export function buildWorktreeNavigationActions(args: Args): ActionSheetAction[]
]
if (args.hostCapabilities.includes(MOBILE_AI_VAULT_CAPABILITY)) {
actions.push({
label: t('m.Au06U2c'),
label: t('worktreeNavigationActions.agent'),
renderIcon: () =>
MobileAgentSessionHistoryIcon({ size: 16, color: colors.textSecondary, strokeWidth: 2 }),
onPress: () => {
+1 -1
View File
@@ -24,7 +24,7 @@ export function MobileBrowserKeyRow({ disabled, onKeypress }: Props): React.JSX.
onPress={() => onKeypress(key)}
>
<Text style={[styles.keyButtonText, disabled && styles.disabledText]}>
{key === 'Backspace' ? '⌫' : key === 'Escape' ? t('m.u8NH0Xk') : key}
{key === 'Backspace' ? '⌫' : key === 'Escape' ? t('mobileBrowserKeyRow.esc') : key}
</Text>
</Pressable>
))}
+25 -24
View File
@@ -431,11 +431,11 @@ export function MobileBrowserPane({
busyRef.current = false
setBusy(false)
if (screencastSupported === false) {
setError(t('m.X2WJPN4'))
setError(t('mobileBrowserPane.update'))
} else if (screencastSupported === null) {
setError(t('m.lBu6FGw'))
setError(t('mobileBrowserPane.checking'))
} else if (!tab.browserPageId) {
setError(t('m.YmwDAcU'))
setError(t('mobileBrowserPane.browserPage'))
}
return
}
@@ -447,7 +447,7 @@ export function MobileBrowserPane({
}
busyRef.current = false
setBusy(false)
setError(t('m.ZoS2fOc'))
setError(t('mobileBrowserPane.browserStreamTimed'))
}, 15_000)
const clearStartupTimer = (): void => {
if (startupTimer) {
@@ -503,7 +503,7 @@ export function MobileBrowserPane({
} else if (event.type === 'dialog') {
setDialog({
dialogType: event.dialogType ?? 'alert',
message: event.message ?? t('m.a6UgDc8')
message: event.message ?? t('mobileBrowserPane.browserDialog')
})
} else if (event.type === 'dialogClosed') {
setDialog(null)
@@ -513,7 +513,8 @@ export function MobileBrowserPane({
busyRef.current = false
setBusy(false)
}
const message = event.message ?? event.error?.message ?? t('m.21rrqn0')
const message =
event.message ?? event.error?.message ?? t('mobileBrowserPane.browserStreamFailed')
if (shouldSurfaceBrowserError(message)) {
if (readyRef.current) {
readyRef.current = false
@@ -579,7 +580,7 @@ export function MobileBrowserPane({
setError(null)
return (response as RpcSuccess).result
} catch (err) {
const message = browserErrorMessage(err, t('m.9E99YI8'))
const message = browserErrorMessage(err, t('mobileBrowserPane.browserCommand'))
if (!opts.suppressError && shouldSurfaceBrowserError(message)) {
setError(message)
}
@@ -597,7 +598,7 @@ export function MobileBrowserPane({
const navigateToAddress = useCallback(async () => {
const url = normalizeBrowserUrl(addressValue)
if (!url) {
setError(t('m.5dQ6Hbc'))
setError(t('mobileBrowserPane.enter'))
return
}
const result = (await sendBrowserRequest(
@@ -630,7 +631,7 @@ export function MobileBrowserPane({
x: pending.point.x,
y: pending.point.y
}),
t('m.A-4DgN0')
t('mobileBrowserPane.browserPointerMove')
)
assertRpcOk(
await client.sendRequest('browser.mouseWheel', {
@@ -638,7 +639,7 @@ export function MobileBrowserPane({
dx: pending.dx,
dy: pending.dy
}),
t('m.yrjs9M0')
t('mobileBrowserPane.browserScroll')
)
setError(null)
} catch {
@@ -683,15 +684,15 @@ export function MobileBrowserPane({
try {
assertRpcOk(
await client.sendRequest('browser.mouseMove', { ...base, x: point.x, y: point.y }),
t('m.A-4DgN0')
t('mobileBrowserPane.browserPointerMove')
)
assertRpcOk(
await client.sendRequest('browser.mouseDown', { ...base, button }),
t('m.oaarOD4')
t('mobileBrowserPane.browserPointerDown')
)
assertRpcOk(
await client.sendRequest('browser.mouseUp', { ...base, button }),
t('m._ax72Jw')
t('mobileBrowserPane.browserPointerUp')
)
setError(null)
} catch {
@@ -794,7 +795,7 @@ export function MobileBrowserPane({
}
rightClickSentRef.current = true
void sendPointerClick(point, 'right')
onToast(t('m.X1FYz1o'))
onToast(t('mobileBrowserPane.right'))
}, LONG_PRESS_MS)
},
[clearLongPressTimer, frameGeometry, mapTouchPoint, onToast, sendPointerClick]
@@ -939,7 +940,7 @@ export function MobileBrowserPane({
{ suppressError: true }
)
if (result !== null) {
onToast(t('m.aJGSaVI'))
onToast(t('mobileBrowserPane.sent'))
} else {
setKeyboardValue(text)
}
@@ -1086,21 +1087,21 @@ export function MobileBrowserPane({
<View style={styles.toolbar}>
<MobileBrowserToolbarIconButton
disabled={controlsDisabled || !tab.canGoBack}
label={t('m.a_PEZW8')}
label={t('mobileBrowserPane.back')}
onPress={goBack}
>
<ChevronLeft size={15} color={buttonColor(!controlsDisabled && tab.canGoBack)} />
</MobileBrowserToolbarIconButton>
<MobileBrowserToolbarIconButton
disabled={controlsDisabled || !tab.canGoForward}
label={t('m.-YMBg9U')}
label={t('mobileBrowserPane.forward')}
onPress={goForward}
>
<ChevronRight size={15} color={buttonColor(!controlsDisabled && tab.canGoForward)} />
</MobileBrowserToolbarIconButton>
<MobileBrowserToolbarIconButton
disabled={controlsDisabled}
label={t('m.GVCAd00')}
label={t('mobileBrowserPane.reload')}
onPress={reloadPage}
>
<RefreshCw size={15} color={buttonColor(!controlsDisabled)} />
@@ -1119,7 +1120,7 @@ export function MobileBrowserPane({
keyboardType={Platform.OS === 'ios' ? 'url' : 'default'}
numberOfLines={1}
returnKeyType="go"
placeholder={t('m.-00MlTU')}
placeholder={t('mobileBrowserPane.url')}
placeholderTextColor={colors.textMuted}
editable={!controlsDisabled}
/>
@@ -1229,7 +1230,7 @@ export function MobileBrowserPane({
{dialog ? (
<View style={styles.dialogOverlay}>
<View style={styles.dialogCard}>
<Text style={styles.dialogTitle}>{t('m.z1nkBLw')}</Text>
<Text style={styles.dialogTitle}>{t('mobileBrowserPane.browserDialogMessage')}</Text>
<Text style={styles.dialogMessage}>{dialog.message}</Text>
<View style={styles.dialogActions}>
{dialog.dialogType !== 'alert' ? (
@@ -1240,7 +1241,7 @@ export function MobileBrowserPane({
]}
onPress={() => void sendDialogCommand('browser.dialogDismiss')}
>
<Text style={styles.dialogButtonText}>{t('m.RXEfHVA')}</Text>
<Text style={styles.dialogButtonText}>{t('mobileBrowserPane.cancel')}</Text>
</Pressable>
) : null}
<Pressable
@@ -1252,7 +1253,7 @@ export function MobileBrowserPane({
onPress={() => void sendDialogCommand('browser.dialogAccept')}
>
<Text style={[styles.dialogButtonText, styles.dialogButtonPrimaryText]}>
{t('m.48esWoc')}
{t('mobileBrowserPane.ok')}
</Text>
</Pressable>
</View>
@@ -1281,7 +1282,7 @@ export function MobileBrowserPane({
style={styles.keyboardInput}
value={keyboardValue}
onChangeText={setKeyboardValue}
placeholder={t('m.g3GvsOs')}
placeholder={t('mobileBrowserPane.type')}
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
@@ -1292,7 +1293,7 @@ export function MobileBrowserPane({
style={[styles.sendButton, (controlsDisabled || !keyboardValue) && styles.disabled]}
disabled={controlsDisabled || !keyboardValue}
onPress={() => void sendKeyboardText()}
accessibilityLabel={t('m.rfYWlSA')}
accessibilityLabel={t('mobileBrowserPane.send')}
>
<ArrowUp size={18} color={buttonColor(!controlsDisabled && !!keyboardValue)} />
</Pressable>
@@ -5,10 +5,10 @@ import { t } from '@/i18n/mobile-i18n'
export type BrowserPointerModifier = 'cmd' | 'ctrl' | 'alt' | 'shift'
const BROWSER_POINTER_MODIFIERS: { id: BrowserPointerModifier; label: string }[] = [
{ id: 'cmd', label: t('m.IW8xp4k') },
{ id: 'ctrl', label: t('m.EqW8StQ') },
{ id: 'alt', label: t('m.fP87QIY') },
{ id: 'shift', label: t('m.GTbLYFM') }
{ id: 'cmd', label: t('mobileBrowserPointerModifiers.cmd') },
{ id: 'ctrl', label: t('mobileBrowserPointerModifiers.ctrl') },
{ id: 'alt', label: t('mobileBrowserPointerModifiers.alt') },
{ id: 'shift', label: t('mobileBrowserPointerModifiers.shift') }
]
type Props = {
@@ -39,7 +39,9 @@ export function MobileBrowserPointerModifiers({
onPress={() => onToggle(modifier.id)}
accessibilityRole="button"
accessibilityState={{ selected, disabled }}
accessibilityLabel={t('m.gZUBadw', { value0: modifier.label })}
accessibilityLabel={t('mobileBrowserPointerModifiers.modifier', {
modifierLabel: modifier.label
})}
>
<Text
style={[
@@ -15,8 +15,8 @@ export function MobileBrowserViewModeSwitch({
onChange
}: Props): React.JSX.Element {
const viewModes: { id: MobileBrowserViewMode; label: string }[] = [
{ id: 'web', label: t('m.3TuUR-s') },
{ id: 'mobile', label: t('m.eJJUzYE') }
{ id: 'web', label: t('mobileBrowserViewModeSwitch.web') },
{ id: 'mobile', label: t('mobileBrowserViewModeSwitch.mobile') }
]
return (
<View style={styles.switch}>
@@ -56,7 +56,9 @@ function ViewModeButton({
onPress={onPress}
accessibilityRole="button"
accessibilityState={{ selected, disabled }}
accessibilityLabel={t('m.lV607bI', { value0: label.toLowerCase() })}
accessibilityLabel={t('mobileBrowserViewModeSwitch.show', {
browserLabel: label.toLowerCase()
})}
>
<Text style={[styles.buttonText, selected && styles.buttonTextSelected]}>{label}</Text>
</Pressable>
+6 -4
View File
@@ -19,18 +19,20 @@ export function AuthFailedBanner({
}) {
return (
<View style={styles.banner}>
<Text style={styles.text}>{t('m.IrfKQh8')}</Text>
<Text style={styles.text}>{t('authFailedBanner.authentication')}</Text>
<View style={styles.actions}>
{canRetry && (
<Pressable style={styles.action} onPress={onRetry}>
<Text style={styles.actionText}>{t('m.ujiecek')}</Text>
<Text style={styles.actionText}>{t('authFailedBanner.retry')}</Text>
</Pressable>
)}
<Pressable style={styles.action} onPress={onRepair}>
<Text style={styles.actionText}>{t('m.KuqR8FY')}</Text>
<Text style={styles.actionText}>{t('authFailedBanner.re')}</Text>
</Pressable>
<Pressable style={styles.action} onPress={onRemove}>
<Text style={[styles.actionText, { color: colors.statusRed }]}>{t('m.q-MNnd4')}</Text>
<Text style={[styles.actionText, { color: colors.statusRed }]}>
{t('authFailedBanner.remove')}
</Text>
</Pressable>
</View>
</View>
@@ -24,7 +24,8 @@ export function CodexResetCreditAction({
<View style={styles.copy}>
<Text style={styles.title}>{summary.availabilityLabel}</Text>
<Text style={styles.subtitle}>
{[summary.expiryLabel, scopeLabel].filter(Boolean).join(' · ') || t('m.wA_0d2E')}
{[summary.expiryLabel, scopeLabel].filter(Boolean).join(' · ') ||
t('codexResetCreditAction.earned')}
</Text>
</View>
<Pressable
@@ -36,8 +37,16 @@ export function CodexResetCreditAction({
onPress={onPress}
disabled={disabled}
accessibilityRole="button"
accessibilityLabel={busy ? t('m.fNZPjTQ') : t('m.7uEIrNQ')}
accessibilityHint={scopeLabel ? t('m.Y2o7mgc', { value0: scopeLabel }) : t('m.8CLIWuE')}
accessibilityLabel={
busy ? t('codexResetCreditAction.resettingCodex') : t('codexResetCreditAction.useCodex')
}
accessibilityHint={
scopeLabel
? t('codexResetCreditAction.usesOneEarnedResetScope', {
scopeLabel: scopeLabel
})
: t('codexResetCreditAction.usesOneEarnedResetActive')
}
accessibilityState={{ busy, disabled }}
hitSlop={8}
>
@@ -46,7 +55,9 @@ export function CodexResetCreditAction({
) : (
<RotateCcw size={14} color={colors.textPrimary} />
)}
<Text style={styles.buttonText}>{busy ? t('m.LN50bQ0') : t('m.2LrOLw8')}</Text>
<Text style={styles.buttonText}>
{busy ? t('codexResetCreditAction.resetting') : t('codexResetCreditAction.useReset')}
</Text>
</Pressable>
</View>
</>
+2 -2
View File
@@ -18,8 +18,8 @@ export function ConfirmModal({
visible,
title,
message,
confirmLabel = t('m.OxB9eF0'),
cancelLabel = t('m.QVUePYc'),
confirmLabel = t('confirmModal.confirm'),
cancelLabel = t('confirmModal.cancel'),
destructive = false,
onConfirm,
onCancel
+24 -24
View File
@@ -67,17 +67,17 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
] satisfies readonly { id: TerminalShortcutModifier; label: string; glyph?: string }[]
const specialKeyGroups: { title: string; ids: string[]; columns: number }[] = [
{
title: t('m.1RunS2Y'),
title: t('customKeyModal.editing'),
ids: ['escape', 'tab', 'enter', 'backspace', 'delete', 'insert', 'space'],
columns: 4
},
{
title: t('m.ClgDZUM'),
title: t('customKeyModal.navigation'),
ids: ['arrowUp', 'arrowDown', 'arrowLeft', 'arrowRight', 'home', 'end', 'pageUp', 'pageDown'],
columns: 4
},
{
title: t('m.WwKbTEk'),
title: t('customKeyModal.function'),
ids: ['f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12'],
columns: 6
}
@@ -186,7 +186,7 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
<Pressable
style={({ pressed }) => [styles.backButton, pressed && styles.backButtonPressed]}
onPress={onBack}
accessibilityLabel={t('m.v1Nn-IY')}
accessibilityLabel={t('customKeyModal.back')}
>
<ChevronLeft size={18} color={colors.textSecondary} />
</Pressable>
@@ -194,10 +194,10 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
<View style={styles.backSpacer} />
)}
<Text style={styles.title}>
{step === 'choose-type' && t('m._g9qkig')}
{step === 'shortcut-combo' && t('m.Lr_GVFQ')}
{step === 'special-keys' && t('m.rMV0BMU')}
{step === 'text-macro' && t('m.fNBP1Us')}
{step === 'choose-type' && t('customKeyModal.addShortcut')}
{step === 'shortcut-combo' && t('customKeyModal.shortcut')}
{step === 'special-keys' && t('customKeyModal.pick')}
{step === 'text-macro' && t('customKeyModal.text')}
</Text>
<View style={styles.backSpacer} />
</View>
@@ -208,16 +208,16 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
onPress={() => setStep('shortcut-combo')}
>
<Text style={styles.rowLabel}>{t('m.Lr_GVFQ')}</Text>
<Text style={styles.rowHint}>{t('m.Y7zDQOM')}</Text>
<Text style={styles.rowLabel}>{t('customKeyModal.shortcut')}</Text>
<Text style={styles.rowHint}>{t('customKeyModal.build')}</Text>
</Pressable>
<View style={styles.separator} />
<Pressable
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
onPress={() => setStep('text-macro')}
>
<Text style={styles.rowLabel}>{t('m.fNBP1Us')}</Text>
<Text style={styles.rowHint}>{t('m.I-Rlhvc')}</Text>
<Text style={styles.rowLabel}>{t('customKeyModal.text')}</Text>
<Text style={styles.rowHint}>{t('customKeyModal.send')}</Text>
</Pressable>
{onManageShortcuts ? (
<>
@@ -226,8 +226,8 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
onPress={onManageShortcuts}
>
<Text style={styles.rowLabel}>{t('m.BFsLjpc')}</Text>
<Text style={styles.rowHint}>{t('m.w7YJ0Kw')}</Text>
<Text style={styles.rowLabel}>{t('customKeyModal.manage')}</Text>
<Text style={styles.rowHint}>{t('customKeyModal.show')}</Text>
</Pressable>
</>
) : null}
@@ -254,7 +254,7 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
</View>
<View style={styles.section}>
<Text style={styles.sectionLabel}>{t('m.9jQVv-8')}</Text>
<Text style={styles.sectionLabel}>{t('customKeyModal.modifiers')}</Text>
<View style={styles.mods}>
{shortcutModifierOptions.map((modifier) => {
const selected = shortcutModifiers.includes(modifier.id)
@@ -285,7 +285,7 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
</View>
<View style={styles.section}>
<Text style={styles.sectionLabel}>{t('m.QlB11DI')}</Text>
<Text style={styles.sectionLabel}>{t('customKeyModal.key')}</Text>
<TextInput
style={styles.keyInput}
value={shortcutKey.length === 1 ? shortcutKey.toUpperCase() : ''}
@@ -300,7 +300,7 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
style={({ pressed }) => [styles.moreLink, pressed && styles.moreLinkPressed]}
onPress={() => setStep('special-keys')}
>
<Text style={styles.moreLinkText}>{t('m.GSrBito')}</Text>
<Text style={styles.moreLinkText}>{t('customKeyModal.more')}</Text>
</Pressable>
</View>
@@ -312,7 +312,7 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
<Text
style={[styles.saveButtonText, !shortcutPreview && styles.saveButtonTextDisabled]}
>
{t('m.ndVygIU')}
{t('customKeyModal.add')}
</Text>
</Pressable>
</View>
@@ -359,28 +359,28 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
{step === 'text-macro' && (
<View style={styles.group}>
<View style={styles.macroForm}>
<Text style={styles.fieldLabel}>{t('m.zkM-vYk')}</Text>
<Text style={styles.fieldLabel}>{t('customKeyModal.label')}</Text>
<TextInput
style={styles.fieldInput}
value={macroLabel}
onChangeText={setMacroLabel}
placeholder={t('m.d0JpJz4')}
placeholder={t('customKeyModal.eGBuild')}
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
/>
<Text style={styles.fieldLabel}>{t('m.YZWk8R4')}</Text>
<Text style={styles.fieldLabel}>{t('customKeyModal.command')}</Text>
<TextInput
style={styles.fieldInput}
value={macroText}
onChangeText={setMacroText}
placeholder={t('m.CiFc9DQ')}
placeholder={t('customKeyModal.eGPnpm')}
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
/>
<View style={styles.switchRow}>
<Text style={styles.switchLabel}>{t('m.N7UodPY')}</Text>
<Text style={styles.switchLabel}>{t('customKeyModal.press')}</Text>
<Switch
value={macroEnter}
onValueChange={setMacroEnter}
@@ -396,7 +396,7 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
<Text
style={[styles.saveButtonText, !macroText.trim() && styles.saveButtonTextDisabled]}
>
{t('m._g9qkig')}
{t('customKeyModal.addShortcut')}
</Text>
</Pressable>
</View>
+4 -4
View File
@@ -297,11 +297,11 @@ function DragReorderRow({
style={styles.handle}
accessible
accessibilityRole="button"
accessibilityLabel={t('m.-lwE4Q0')}
accessibilityHint={t('m.T-H1CUQ')}
accessibilityLabel={t('dragReorderList.drag')}
accessibilityHint={t('dragReorderList.use')}
accessibilityActions={[
{ name: 'moveUp', label: t('m.ee4D14o') },
{ name: 'moveDown', label: t('m.uGHhnC0') }
{ name: 'moveUp', label: t('dragReorderList.moveUp') },
{ name: 'moveDown', label: t('dragReorderList.moveDown') }
]}
onAccessibilityAction={(event) => {
if (event.nativeEvent.actionName === 'moveUp') {
+5 -2
View File
@@ -16,7 +16,7 @@ const HostStatusGatesContext = createContext<HostStatusGates | null>(null)
export function useHostProtocolGates(): HostStatusGates {
const gates = useContext(HostStatusGatesContext)
if (!gates) {
const invariantViolation = new Error(t('m.3cWf_os'))
const invariantViolation = new Error(t('hostProtocolGate.use'))
throw invariantViolation
}
return gates
@@ -37,7 +37,10 @@ export function HostProtocolGate({ hostId, children }: Props) {
// Why: child routes may call newer RPCs on mount, so wait until compatibility is known.
return (
<View style={styles.pending}>
<ActivityIndicator color={colors.textSecondary} accessibilityLabel={t('m.qvnQ47I')} />
<ActivityIndicator
color={colors.textSecondary}
accessibilityLabel={t('hostProtocolGate.checking')}
/>
</View>
)
}
@@ -49,7 +49,7 @@ export function MobileDictationSetupSheet({ visible, client, onClose, onReady }:
setError(null)
return next.models.some(isModelInFlight)
} catch (err) {
setError(err instanceof Error ? err.message : t('m.lUsptPY'))
setError(err instanceof Error ? err.message : t('mobileDictationSetupSheet.failed'))
return undefined
}
}, [client])
@@ -80,7 +80,7 @@ export function MobileDictationSetupSheet({ visible, client, onClose, onReady }:
await refreshSetup()
} catch (err) {
triggerError()
setError(err instanceof Error ? err.message : t('m.0rsS1Dw'))
setError(err instanceof Error ? err.message : t('mobileDictationSetupSheet.downloadFailed'))
} finally {
setBusy(null)
}
@@ -102,7 +102,7 @@ export function MobileDictationSetupSheet({ visible, client, onClose, onReady }:
onReady?.()
} catch (err) {
triggerError()
setError(err instanceof Error ? err.message : t('m.jX7Yn3E'))
setError(err instanceof Error ? err.message : t('mobileDictationSetupSheet.couldNotSelect'))
} finally {
setBusy(null)
}
@@ -119,7 +119,7 @@ export function MobileDictationSetupSheet({ visible, client, onClose, onReady }:
try {
setSetup(await setDictationConfig(client, { enabled }))
} catch (err) {
setError(err instanceof Error ? err.message : t('m.kocNlo8'))
setError(err instanceof Error ? err.message : t('mobileDictationSetupSheet.couldNotUpdate'))
}
},
[client]
@@ -130,8 +130,8 @@ export function MobileDictationSetupSheet({ visible, client, onClose, onReady }:
{/* Why: BottomDrawer already scrolls its children in a keyboard-aware container;
a nested capped ScrollView cut off the lower controls. */}
<View>
<Text style={styles.heading}>{t('m.13hFHO4')}</Text>
<Text style={styles.subtitle}>{t('m.D5YFHLU')}</Text>
<Text style={styles.heading}>{t('mobileDictationSetupSheet.setUpVoice')}</Text>
<Text style={styles.subtitle}>{t('mobileDictationSetupSheet.setupInstructions')}</Text>
{setup === null ? (
<View style={styles.loading}>
@@ -140,7 +140,7 @@ export function MobileDictationSetupSheet({ visible, client, onClose, onReady }:
) : (
<>
<View style={styles.enableRow}>
<Text style={styles.enableLabel}>{t('m.6WwF0_8')}</Text>
<Text style={styles.enableLabel}>{t('mobileDictationSetupSheet.dictation')}</Text>
<Switch value={setup.enabled} onValueChange={(v) => void handleToggleEnabled(v)} />
</View>
@@ -154,27 +154,35 @@ export function MobileDictationSetupSheet({ visible, client, onClose, onReady }:
<View style={styles.modelTitleRow}>
<Text style={styles.modelLabel}>{model.label}</Text>
{model.recommended ? (
<Text style={styles.recommended}>{t('m.TJd8_nE')}</Text>
<Text style={styles.recommended}>
{t('mobileDictationSetupSheet.recommended')}
</Text>
) : null}
</View>
<Text style={styles.modelMeta}>
{model.provider === 'openai' ? t('m.bh_QQps') : formatSize(model.sizeBytes)}
{model.provider === 'openai'
? t('mobileDictationSetupSheet.open')
: formatSize(model.sizeBytes)}
{inFlight && model.progress != null
? ` · ${Math.round(model.progress * 100)}%`
: model.status === 'extracting'
? t('m.PG_Pfbo')
? t('mobileDictationSetupSheet.extracting')
: ''}
</Text>
</View>
{model.provider === 'openai' ? (
<Text style={styles.modelStateText}>
{model.status === 'ready' ? t('m.D90AZhs') : t('m.qf7IWvA')}
{model.status === 'ready'
? t('mobileDictationSetupSheet.api')
: t('mobileDictationSetupSheet.setUpDesktop')}
</Text>
) : model.status === 'ready' ? (
isSelected ? (
<View style={styles.selectedTag}>
<Check size={14} color={colors.statusGreen} strokeWidth={2.4} />
<Text style={styles.selectedText}>{t('m.ri8swSc')}</Text>
<Text style={styles.selectedText}>
{t('mobileDictationSetupSheet.use')}
</Text>
</View>
) : (
<Pressable
@@ -185,7 +193,9 @@ export function MobileDictationSetupSheet({ visible, client, onClose, onReady }:
disabled={rowBusy}
onPress={() => void handleUseModel(model)}
>
<Text style={styles.actionText}>{t('m.N18umz0')}</Text>
<Text style={styles.actionText}>
{t('mobileDictationSetupSheet.useMessage')}
</Text>
</Pressable>
)
) : inFlight ? (
@@ -204,7 +214,9 @@ export function MobileDictationSetupSheet({ visible, client, onClose, onReady }:
) : (
<>
<Download size={13} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.actionText}>{t('m.oJ3sUM4')}</Text>
<Text style={styles.actionText}>
{t('mobileDictationSetupSheet.download')}
</Text>
</>
)}
</Pressable>
+23 -15
View File
@@ -41,22 +41,28 @@ export function MobileDiffReviewBody({
onRetry
}: Props) {
if (screenState.kind === 'loading') {
return <CenteredState text={t('m.On0H_JY')} busy />
return <CenteredState text={t('mobileDiffReviewBody.loadingReview')} busy />
}
if (screenState.kind === 'error' || screenState.kind === 'unavailable') {
return (
<CenteredState
title={screenState.kind === 'unavailable' ? t('m.r-dK9lI') : t('m.e5sIbV0')}
title={
screenState.kind === 'unavailable'
? t('mobileDiffReviewBody.review')
: t('mobileDiffReviewBody.unable')
}
text={screenState.message}
onRetry={onRetry}
/>
)
}
if (filteredCount === 0) {
return <CenteredState title={t('m.3-RMeIo')} text={t('m.Ku2wK-g')} />
return (
<CenteredState title={t('mobileDiffReviewBody.no')} text={t('mobileDiffReviewBody.try')} />
)
}
if (diffState.kind === 'loading') {
return <CenteredState text={t('m.e6R_IPM')} busy muted />
return <CenteredState text={t('mobileDiffReviewBody.loadingDiff')} busy muted />
}
if (diffState.kind !== 'ready') {
return <DiffUnavailableState diffState={diffState} onRetry={onRetry} />
@@ -91,7 +97,9 @@ export function MobileDiffReviewBody({
})
}}
ListFooterComponent={
diffState.truncated ? <Text style={styles.truncatedText}>{t('m.W6MLNo0')}</Text> : null
diffState.truncated ? (
<Text style={styles.truncatedText}>{t('mobileDiffReviewBody.diffTruncated')}</Text>
) : null
}
/>
)
@@ -106,22 +114,22 @@ function DiffUnavailableState({
}) {
const title =
diffState.kind === 'binary'
? t('m.Cku_aRM')
? t('mobileDiffReviewBody.binary')
: diffState.kind === 'too-large'
? t('m.BrE_aCg')
? t('mobileDiffReviewBody.diffTooLarge')
: diffState.kind === 'deleted'
? t('m.9Sv7Jb4')
: t('m.Rajm1co')
? t('mobileDiffReviewBody.deleted')
: t('mobileDiffReviewBody.diffUnavailable')
const text =
diffState.kind === 'binary'
? t('m.FdOHIhk')
? t('mobileDiffReviewBody.fileCannot')
: diffState.kind === 'too-large'
? t('m.YeQabYs')
? t('mobileDiffReviewBody.diffTooLargeMobile')
: diffState.kind === 'deleted'
? t('m.Em5nLwQ')
? t('mobileDiffReviewBody.fileDeleted')
: diffState.kind === 'error'
? diffState.message
: t('m.PpZpLWw')
: t('mobileDiffReviewBody.select')
return <CenteredState title={title} text={text} onRetry={onRetry} />
}
@@ -150,10 +158,10 @@ function CenteredState({
style={({ pressed }) => [styles.retryButton, pressed && styles.buttonPressed]}
onPress={onRetry}
accessibilityRole="button"
accessibilityLabel={t('m.RF7UWwo')}
accessibilityLabel={t('mobileDiffReviewBody.retryLoading')}
>
<RefreshCw size={14} color={colors.textPrimary} strokeWidth={2.2} />
<Text style={styles.retryText}>{t('m.GWwDGTE')}</Text>
<Text style={styles.retryText}>{t('mobileDiffReviewBody.retry')}</Text>
</Pressable>
) : null}
</View>
@@ -22,10 +22,12 @@ export function MobileDiffReviewDrawers({ controller }: Props) {
<>
<ActionSheetModal
visible={controller.showOverflow}
title={t('m.oCVOA0w')}
title={t('mobileDiffReviewDrawers.reviewActions')}
message={
controller.reviewedUnstagedCount > 0
? t('m._TCuNKQ', { value0: controller.reviewedUnstagedCount })
? t('mobileDiffReviewDrawers.reviewedFileCountReviewed', {
reviewedFileCount: controller.reviewedUnstagedCount
})
: undefined
}
actions={overflowActions}
@@ -33,20 +35,22 @@ export function MobileDiffReviewDrawers({ controller }: Props) {
/>
<ActionSheetModal
visible={controller.sendSheet !== null}
title={t('m.voLBMwU')}
title={t('mobileDiffReviewDrawers.sendNotes')}
message={sendSheetMessage(controller)}
actions={sendActions}
onClose={() => controller.setSendSheet(null)}
/>
<ConfirmModal
visible={controller.discardTarget !== null}
title={t('m.1V8B5pc')}
title={t('mobileDiffReviewDrawers.discardFile')}
message={
controller.discardTarget
? t('m.ajcWpiI', { value0: controller.discardTarget.filePath })
? t('mobileDiffReviewDrawers.discardChanges', {
filePath: controller.discardTarget.filePath
})
: undefined
}
confirmLabel={t('m.-YeAYjI')}
confirmLabel={t('mobileDiffReviewDrawers.discard')}
destructive
onConfirm={() => {
const target = controller.discardTarget
@@ -69,7 +73,7 @@ function useSendActions(controller: ReturnType<typeof useMobileDiffReviewControl
const terminalActions =
controller.sendSheet?.kind === 'ready' || controller.sendSheet?.kind === 'error'
? controller.sendSheet.terminals.map((terminal) => ({
label: `${terminal.title || t('m.4GOORsg')} (${terminal.terminal.slice(0, 6)})`,
label: `${terminal.title || t('mobileDiffReviewDrawers.terminal')} (${terminal.terminal.slice(0, 6)})`,
icon: Send,
disabled: comments.length === 0,
skipAutoClose: true,
@@ -79,14 +83,14 @@ function useSendActions(controller: ReturnType<typeof useMobileDiffReviewControl
return [
...terminalActions,
{
label: t('m.qljZwNc'),
label: t('mobileDiffReviewDrawers.new'),
icon: Plus,
disabled: comments.length === 0,
skipAutoClose: true,
onPress: () => void controller.createTerminalAndSend(comments)
},
{
label: t('m.SGgu3t8'),
label: t('mobileDiffReviewDrawers.copy'),
icon: Copy,
disabled:
controller.screenState.kind !== 'ready' || controller.screenState.comments.length === 0,
@@ -100,21 +104,21 @@ function useOverflowActions(controller: ReturnType<typeof useMobileDiffReviewCon
return useMemo<ActionSheetAction[]>(
() => [
{
label: t('m.SGgu3t8'),
label: t('mobileDiffReviewDrawers.copy'),
icon: Copy,
disabled:
controller.screenState.kind !== 'ready' || controller.screenState.comments.length === 0,
onPress: () => void controller.copyNotes()
},
{
label: t('m.bboDcz4'),
label: t('mobileDiffReviewDrawers.sendUnsent'),
icon: Send,
disabled: controller.unsentComments.length === 0,
skipAutoClose: true,
onPress: () => void controller.openSendSheet()
},
{
label: t('m.AMn9p6c'),
label: t('mobileDiffReviewDrawers.clear'),
icon: Trash2,
disabled:
controller.screenState.kind !== 'ready' ||
@@ -123,14 +127,14 @@ function useOverflowActions(controller: ReturnType<typeof useMobileDiffReviewCon
onPress: () => void controller.clearSentNotes()
},
{
label: t('m.9YSL2XY'),
label: t('mobileDiffReviewDrawers.stageReviewedFiles'),
icon: Check,
disabled: controller.reviewedUnstagedCount === 0 || controller.busyAction !== null,
skipAutoClose: true,
onPress: () => void controller.stageReviewedFiles()
},
{
label: t('m.lDOi4xI'),
label: t('mobileDiffReviewDrawers.mark'),
icon: X,
disabled:
controller.screenState.kind !== 'ready' ||
@@ -140,7 +144,7 @@ function useOverflowActions(controller: ReturnType<typeof useMobileDiffReviewCon
onPress: () => void controller.markUnreviewed()
},
{
label: t('m.QyPs23A'),
label: t('mobileDiffReviewDrawers.open'),
icon: FileText,
disabled: !controller.currentItem || controller.currentItem.scope === 'branch',
onPress: () => void controller.openInSession()
@@ -154,10 +158,12 @@ function sendSheetMessage(
controller: ReturnType<typeof useMobileDiffReviewController>
): string | undefined {
return controller.sendSheet?.kind === 'loading'
? t('m.ycKwNOI')
? t('mobileDiffReviewDrawers.loading')
: controller.sendSheet?.kind === 'error'
? controller.sendSheet.message
: t('m.5Rc0T-w', { value0: controller.unsentComments.length })
: t('mobileDiffReviewDrawers.unsent', {
unsentCommentCount: controller.unsentComments.length
})
}
function NoteComposerDrawer({ controller }: Props) {
@@ -168,19 +174,23 @@ function NoteComposerDrawer({ controller }: Props) {
<View style={styles.composerHeader}>
<View>
<Text style={styles.drawerTitle}>
{composer?.mode === 'edit' ? t('m.vX8hCPw') : t('m.SqSZ_D4')}
{composer?.mode === 'edit'
? t('mobileDiffReviewDrawers.edit')
: t('mobileDiffReviewDrawers.add')}
</Text>
<Text style={styles.drawerSubtitle}>
{composer?.mode === 'create' && composer.lineNumber > 0
? t('m.YlMekyY', { value0: composer.lineNumber })
: t('m.K4ExXj0')}
? t('mobileDiffReviewDrawers.line', {
lineNumber: composer.lineNumber
})
: t('mobileDiffReviewDrawers.file')}
</Text>
</View>
<Pressable
style={({ pressed }) => [styles.iconButton, pressed && styles.iconButtonPressed]}
onPress={controller.closeComposer}
accessibilityRole="button"
accessibilityLabel={t('m.cy6hF-c')}
accessibilityLabel={t('mobileDiffReviewDrawers.cancel')}
>
<X size={18} color={colors.textPrimary} strokeWidth={2.2} />
</Pressable>
@@ -191,7 +201,7 @@ function NoteComposerDrawer({ controller }: Props) {
onChangeText={controller.setComposerBody}
multiline
autoFocus
placeholder={t('m.2lwuEDc')}
placeholder={t('mobileDiffReviewDrawers.reviewNote')}
placeholderTextColor={colors.textMuted}
accessibilityLabel={composerLabel(composer)}
/>
@@ -210,8 +220,10 @@ function composerLabel(
composer: { mode: 'create'; lineNumber: number } | { mode: 'edit'; comment: DiffComment } | null
): string {
return composer?.mode === 'create' && composer.lineNumber > 0
? t('m.972Qfrg', { value0: composer.lineNumber })
: t('m.2lwuEDc')
? t('mobileDiffReviewDrawers.saveNote', {
lineNumber: composer.lineNumber
})
: t('mobileDiffReviewDrawers.reviewNote')
}
function DeleteNoteButton({ onPress }: { onPress: () => Promise<void> }) {
@@ -220,10 +232,10 @@ function DeleteNoteButton({ onPress }: { onPress: () => Promise<void> }) {
style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}
onPress={() => void onPress()}
accessibilityRole="button"
accessibilityLabel={t('m.7BEb-8o')}
accessibilityLabel={t('mobileDiffReviewDrawers.deleteNote')}
>
<Trash2 size={14} color={colors.statusRed} strokeWidth={2.2} />
<Text style={styles.destructiveText}>{t('m.-bToH9I')}</Text>
<Text style={styles.destructiveText}>{t('mobileDiffReviewDrawers.delete')}</Text>
</Pressable>
)
}
@@ -249,7 +261,7 @@ function SaveNoteButton({
accessibilityLabel={composerLabel(composer)}
>
<Check size={14} color={colors.bgBase} strokeWidth={2.2} />
<Text style={styles.primaryButtonText}>{t('m.ls_e0g8')}</Text>
<Text style={styles.primaryButtonText}>{t('mobileDiffReviewDrawers.save')}</Text>
</Pressable>
)
}
@@ -262,17 +274,17 @@ function CompletionDrawer({ controller }: Props) {
visible={controller.showCompletion}
onClose={() => controller.setShowCompletion(false)}
>
<Text style={styles.drawerTitle}>{t('m.du7kB18')}</Text>
<Text style={styles.drawerTitle}>{t('mobileDiffReviewDrawers.reviewComplete')}</Text>
<Text style={styles.drawerSubtitle}>
{t(
controller.queue.length === 1
? noteCount === 1
? 'm.QlrZNys'
: 'm.J7M3gnY'
? 'mobileDiffReviewDrawers.reviewedFileCountFileReviewedNoteCountNote'
: 'mobileDiffReviewDrawers.reviewedFileCountFileReviewedNoteCountNotes'
: noteCount === 1
? 'm.uM2_gxc'
: 'm.JrNt3Dk',
{ value0: controller.queue.length, value1: noteCount }
? 'mobileDiffReviewDrawers.reviewedFileCountFilesReviewedNoteCountNote'
: 'mobileDiffReviewDrawers.reviewedFileCountFilesReviewedNoteCountNotes',
{ reviewedFileCount: controller.queue.length, noteCount: noteCount }
)}
</Text>
<View style={styles.drawerButtonRow}>
@@ -281,20 +293,22 @@ function CompletionDrawer({ controller }: Props) {
disabled={controller.reviewedUnstagedCount === 0}
onPress={() => void controller.stageReviewedFiles()}
accessibilityRole="button"
accessibilityLabel={t('m.XhmaYgY')}
accessibilityLabel={t('mobileDiffReviewDrawers.stageReviewedFilesAccessibility')}
>
<Check size={14} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.secondaryButtonText}>{t('m.a1hiCFE')}</Text>
<Text style={styles.secondaryButtonText}>
{t('mobileDiffReviewDrawers.stageReviewed')}
</Text>
</Pressable>
<Pressable
style={({ pressed }) => [styles.primaryButton, pressed && styles.buttonPressed]}
disabled={controller.unsentComments.length === 0}
onPress={() => void controller.openSendSheet()}
accessibilityRole="button"
accessibilityLabel={t('m.XNffV5A')}
accessibilityLabel={t('mobileDiffReviewDrawers.sendNotesAgent')}
>
<Send size={14} color={colors.bgBase} strokeWidth={2.2} />
<Text style={styles.primaryButtonText}>{t('m.voLBMwU')}</Text>
<Text style={styles.primaryButtonText}>{t('mobileDiffReviewDrawers.sendNotes')}</Text>
</Pressable>
</View>
</BottomDrawer>
@@ -66,7 +66,11 @@ export function MobileDiffReviewFileSummary({
</Text>
<Text style={styles.fileMeta} numberOfLines={1}>
{mobileReviewScopeLabel(item)}
{item.oldPath ? t('m.Tqe6zfQ', { value0: item.oldPath }) : ''}
{item.oldPath
? t('mobileDiffReviewFileSummary.old', {
oldPath: item.oldPath
})
: ''}
</Text>
</View>
</View>
@@ -74,13 +78,21 @@ export function MobileDiffReviewFileSummary({
<Text style={styles.fileMeta}>
{currentIndex + 1}/{filteredCount}
</Text>
{item.isReviewed ? <Text style={styles.reviewedPill}>{t('m.TGeBFA8')}</Text> : null}
{item.changedSinceReview ? <Text style={styles.stalePill}>{t('m.cV_5zt0')}</Text> : null}
{item.isReviewed ? (
<Text style={styles.reviewedPill}>{t('mobileDiffReviewFileSummary.reviewed')}</Text>
) : null}
{item.changedSinceReview ? (
<Text style={styles.stalePill}>{t('mobileDiffReviewFileSummary.changed')}</Text>
) : null}
{item.noteCount > 0 ? (
<Text style={styles.fileMeta}>{mobileReviewNoteCountLabel(item.noteCount)}</Text>
) : null}
{item.staleNoteCount > 0 ? (
<Text style={styles.staleText}>{t('m.OAIsqbU', { value0: item.staleNoteCount })}</Text>
<Text style={styles.staleText}>
{t('mobileDiffReviewFileSummary.staleNote', {
staleNoteCount: item.staleNoteCount
})}
</Text>
) : null}
</View>
{fileNotes.length > 0 ? (
@@ -91,13 +103,13 @@ export function MobileDiffReviewFileSummary({
style={({ pressed }) => [styles.fileNote, pressed && styles.fileNotePressed]}
onPress={() => onEditNote(note)}
accessibilityRole="button"
accessibilityLabel={t('m.6NQSDSg')}
accessibilityLabel={t('mobileDiffReviewFileSummary.edit')}
>
<Text style={styles.fileNoteText} numberOfLines={2}>
{note.body}
</Text>
{staleCommentIds.has(note.id) ? (
<Text style={styles.staleText}>{t('m.PpqbMu4')}</Text>
<Text style={styles.staleText}>{t('mobileDiffReviewFileSummary.stale')}</Text>
) : null}
</Pressable>
))}
@@ -109,20 +121,20 @@ export function MobileDiffReviewFileSummary({
disabled={hunkDisabled}
onPress={() => onJumpHunk('previous')}
accessibilityRole="button"
accessibilityLabel={t('m.co1PXQY')}
accessibilityLabel={t('mobileDiffReviewFileSummary.previous')}
>
<ArrowUp size={14} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.hunkButtonText}>{t('m.N0W7j-Y')}</Text>
<Text style={styles.hunkButtonText}>{t('mobileDiffReviewFileSummary.hunk')}</Text>
</Pressable>
<Pressable
style={({ pressed }) => [styles.hunkButton, pressed && styles.hunkButtonPressed]}
disabled={hunkDisabled}
onPress={() => onJumpHunk('next')}
accessibilityRole="button"
accessibilityLabel={t('m.-v8Njek')}
accessibilityLabel={t('mobileDiffReviewFileSummary.next')}
>
<ArrowDown size={14} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.hunkButtonText}>{t('m.N0W7j-Y')}</Text>
<Text style={styles.hunkButtonText}>{t('mobileDiffReviewFileSummary.hunk')}</Text>
</Pressable>
</View>
</View>
@@ -44,10 +44,10 @@ export function MobileDiffReviewFooter({
disabled={busyAction !== null}
onPress={() => onGitMutation('git.stage', item)}
accessibilityRole="button"
accessibilityLabel={t('m.SzcIpKs')}
accessibilityLabel={t('mobileDiffReviewFooter.stageFile')}
>
<Plus size={14} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.secondaryButtonText}>{t('m.JiM1DAU')}</Text>
<Text style={styles.secondaryButtonText}>{t('mobileDiffReviewFooter.stage')}</Text>
</Pressable>
) : null}
{item.canUnstage ? (
@@ -56,10 +56,10 @@ export function MobileDiffReviewFooter({
disabled={busyAction !== null}
onPress={() => onGitMutation('git.unstage', item)}
accessibilityRole="button"
accessibilityLabel={t('m.VZPK0FM')}
accessibilityLabel={t('mobileDiffReviewFooter.unstageFile')}
>
<Undo2 size={14} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.secondaryButtonText}>{t('m.v58oW_E')}</Text>
<Text style={styles.secondaryButtonText}>{t('mobileDiffReviewFooter.unstage')}</Text>
</Pressable>
) : null}
{item.canDiscard ? (
@@ -68,10 +68,10 @@ export function MobileDiffReviewFooter({
disabled={busyAction !== null}
onPress={() => onDiscard(item)}
accessibilityRole="button"
accessibilityLabel={t('m.TpLj1K8')}
accessibilityLabel={t('mobileDiffReviewFooter.discardFile')}
>
<Trash2 size={14} color={colors.statusRed} strokeWidth={2.2} />
<Text style={styles.destructiveText}>{t('m.lL_q8JA')}</Text>
<Text style={styles.destructiveText}>{t('mobileDiffReviewFooter.discard')}</Text>
</Pressable>
) : null}
</View>
@@ -80,7 +80,7 @@ export function MobileDiffReviewFooter({
style={({ pressed }) => [styles.navButton, pressed && styles.buttonPressed]}
onPress={() => onMoveFile('previous')}
accessibilityRole="button"
accessibilityLabel={t('m.0CC3Pjk')}
accessibilityLabel={t('mobileDiffReviewFooter.previous')}
>
<ChevronLeft size={17} color={colors.textPrimary} strokeWidth={2.2} />
</Pressable>
@@ -88,10 +88,10 @@ export function MobileDiffReviewFooter({
style={({ pressed }) => [styles.footerButton, pressed && styles.buttonPressed]}
onPress={onAddFileNote}
accessibilityRole="button"
accessibilityLabel={t('m.e_IyR5Y')}
accessibilityLabel={t('mobileDiffReviewFooter.add')}
>
<FileText size={14} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.footerButtonText}>{t('m.vRhESp0')}</Text>
<Text style={styles.footerButtonText}>{t('mobileDiffReviewFooter.note')}</Text>
</Pressable>
<Pressable
style={({ pressed }) => [
@@ -101,18 +101,20 @@ export function MobileDiffReviewFooter({
]}
onPress={onMarkReviewed}
accessibilityRole="button"
accessibilityLabel={t('m.a2xZWo8')}
accessibilityLabel={t('mobileDiffReviewFooter.markFile')}
>
<Check size={14} color={colors.bgBase} strokeWidth={2.2} />
<Text style={styles.primaryButtonText}>
{item.isReviewed ? t('m.gUbQPjk') : t('m.gLGElnY')}
{item.isReviewed
? t('mobileDiffReviewFooter.reviewed')
: t('mobileDiffReviewFooter.markReviewed')}
</Text>
</Pressable>
<Pressable
style={({ pressed }) => [styles.navButton, pressed && styles.buttonPressed]}
onPress={() => onMoveFile('next')}
accessibilityRole="button"
accessibilityLabel={t('m.n5_ZJMM')}
accessibilityLabel={t('mobileDiffReviewFooter.next')}
>
<ChevronRight size={17} color={colors.textPrimary} strokeWidth={2.2} />
</Pressable>
@@ -54,13 +54,13 @@ export function MobileDiffReviewHeader({
style={({ pressed }) => [styles.iconButton, pressed && styles.iconButtonPressed]}
onPress={onBack}
accessibilityRole="button"
accessibilityLabel={t('m.Xot6WdE')}
accessibilityLabel={t('mobileDiffReviewHeader.back')}
>
<ChevronLeft size={19} color={colors.textPrimary} strokeWidth={2.2} />
</Pressable>
<View style={styles.titleBlock}>
<Text style={styles.title} numberOfLines={1}>
{t('m.1lIaLfU')}
{t('mobileDiffReviewHeader.changes')}
</Text>
<Text style={styles.subtitle} numberOfLines={1}>
{worktreeLabel}
@@ -71,7 +71,7 @@ export function MobileDiffReviewHeader({
style={({ pressed }) => [styles.iconButton, pressed && styles.iconButtonPressed]}
onPress={onOpenPRSidebar}
accessibilityRole="button"
accessibilityLabel={t('m.GAXIVbU')}
accessibilityLabel={t('mobileDiffReviewHeader.openPull')}
>
<ListChecks size={19} color={colors.textPrimary} strokeWidth={2.2} />
</Pressable>
@@ -80,14 +80,17 @@ export function MobileDiffReviewHeader({
style={({ pressed }) => [styles.iconButton, pressed && styles.iconButtonPressed]}
onPress={onOpenActions}
accessibilityRole="button"
accessibilityLabel={t('m.LU8BxDQ')}
accessibilityLabel={t('mobileDiffReviewHeader.openReview')}
>
<MoreHorizontal size={19} color={colors.textPrimary} strokeWidth={2.2} />
</Pressable>
</View>
<View style={styles.progressRow}>
<Text style={styles.progressText}>
{t('m.fEoGCmQ', { value0: reviewedCount, value1: queueLength })}
{t('mobileDiffReviewHeader.reviewed', {
reviewedCount: reviewedCount,
queueLength: queueLength
})}
</Text>
<Text style={styles.progressText}>{mobileReviewUnsentNoteCountLabel(unsentCount)}</Text>
</View>
@@ -107,7 +110,9 @@ export function MobileDiffReviewHeader({
onPress={() => onSelectFilter(item)}
accessibilityRole="button"
accessibilityState={{ selected: filter === item }}
accessibilityLabel={t('m.qcWQKWI', { value0: mobileReviewFilterLabel(item) })}
accessibilityLabel={t('mobileDiffReviewHeader.show', {
mobileReviewFilterLabel: mobileReviewFilterLabel(item)
})}
>
<Text style={[styles.filterText, filter === item && styles.filterTextActive]}>
{mobileReviewFilterLabel(item)}
+16 -5
View File
@@ -20,10 +20,17 @@ type Props = {
function accessibilityLabelForLine(line: MobileDiffLine): string {
const number = mobileDiffLineNumber(line)
const label =
line.kind === 'add' ? t('m.bnfXRLU') : line.kind === 'delete' ? t('m.0l1OykU') : t('m.9FhY-Sk')
line.kind === 'add'
? t('mobileDiffReviewLine.added')
: line.kind === 'delete'
? t('mobileDiffReviewLine.deleted')
: t('mobileDiffReviewLine.context')
return number
? t('m.FjJb7Lo', { value0: label, value1: number })
: t('m.CKtsHVY', { value0: label })
? t('mobileDiffReviewLine.lineLabelLineLine', {
lineLabel: label,
lineNumber: number
})
: t('mobileDiffReviewLine.lineLabelLine', { lineLabel: label })
}
function canCommentOnLine(line: MobileDiffLine): boolean {
@@ -65,7 +72,9 @@ export function MobileDiffReviewLine({
accessibilityRole={canComment ? 'button' : 'text'}
accessibilityLabel={
canComment && line.newLineNumber !== undefined
? t('m.mCd0V4A', { value0: line.newLineNumber })
? t('mobileDiffReviewLine.add', {
newLineNumber: line.newLineNumber
})
: accessibilityLabelForLine(line)
}
>
@@ -81,7 +90,9 @@ export function MobileDiffReviewLine({
style={({ pressed }) => [styles.noteButton, pressed && styles.noteButtonPressed]}
onPress={() => onEditNote(comment)}
accessibilityRole="button"
accessibilityLabel={t('m.mIZ0_V0', { value0: comment.lineNumber })}
accessibilityLabel={t('mobileDiffReviewLine.edit', {
lineNumber: comment.lineNumber
})}
>
<MessageSquare
size={13}
+12 -5
View File
@@ -22,11 +22,18 @@ export function MobileHostCard(props: {
const isError = ['warning', 'unreachable', 'auth-failed'].includes(props.verdict.kind)
const worktreeSummary = props.worktreeCounts
? [
t(props.worktreeCounts.total === 1 ? 'm.Id8zorY' : 'm.t4PccRo', {
value0: props.worktreeCounts.total
}),
t(
props.worktreeCounts.total === 1
? 'mobileHostCard.worktreeCountWorktree'
: 'mobileHostCard.worktreeCountWorktrees',
{
worktreeCount: props.worktreeCounts.total
}
),
props.worktreeCounts.active > 0
? t('m.AYqemqw', { value0: props.worktreeCounts.active })
? t('mobileHostCard.active', {
activeWorktreeCount: props.worktreeCounts.active
})
: null
]
.filter(Boolean)
@@ -63,7 +70,7 @@ export function MobileHostCard(props: {
) : null}
{props.verdict.kind === 'unreachable' && !props.host.relay ? (
<Text style={styles.discoveryHint} numberOfLines={2}>
{t('m.4bPe4Ao')}
{t('mobileHostCard.update')}
</Text>
) : null}
</View>
+4 -4
View File
@@ -24,18 +24,18 @@ export function MobileHtmlPreview({ html, renderSource }: Props) {
<Pressable
style={[styles.toggle, mode === 'preview' && styles.toggleActive]}
onPress={() => setMode('preview')}
accessibilityLabel={t('m.-FhPMsI')}
accessibilityLabel={t('mobileHtmlPreview.previewRendered')}
>
<Eye size={13} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.toggleText}>{t('m.bvUfBWI')}</Text>
<Text style={styles.toggleText}>{t('mobileHtmlPreview.preview')}</Text>
</Pressable>
<Pressable
style={[styles.toggle, mode === 'source' && styles.toggleActive]}
onPress={() => setMode('source')}
accessibilityLabel={t('m.BE93iOI')}
accessibilityLabel={t('mobileHtmlPreview.view')}
>
<Code size={13} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.toggleText}>{t('m.HdJ9GVY')}</Text>
<Text style={styles.toggleText}>{t('mobileHtmlPreview.source')}</Text>
</Pressable>
</View>
{mode === 'preview' ? (
+13 -5
View File
@@ -80,7 +80,7 @@ function renderInline(text: string, onOpenFile?: (relativePath: string) => void)
if (image) {
parts.push(
<Text key={key} style={styles.link} onPress={() => openMarkdownUrl(image[2]!)}>
{image[1] || t('m.oitC92A')}
{image[1] || t('mobileMarkdown.image')}
</Text>
)
} else if (link) {
@@ -190,7 +190,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile
style={styles.imageFrame}
onPress={() => openMarkdownUrl(block.url)}
>
<Text style={styles.link}>{block.alt || t('m.fTbhbgg')}</Text>
<Text style={styles.link}>{block.alt || t('mobileMarkdown.open')}</Text>
<Text style={styles.imageCaption} numberOfLines={1}>
{block.url}
</Text>
@@ -223,9 +223,17 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile
))}
{hiddenRows > 0 || hiddenColumns > 0 ? (
<Text style={styles.tableTruncated}>
{hiddenRows > 0 ? t('m.Jmh-3ik', { value0: hiddenRows }) : ''}
{hiddenRows > 0
? t('mobileMarkdown.hiddenRow', {
hiddenRowCount: hiddenRows
})
: ''}
{hiddenRows > 0 && hiddenColumns > 0 ? ' · ' : ''}
{hiddenColumns > 0 ? t('m.kijMg1M', { value0: hiddenColumns }) : ''}
{hiddenColumns > 0
? t('mobileMarkdown.hiddenColumn', {
hiddenColumnCount: hiddenColumns
})
: ''}
</Text>
) : null}
</View>
@@ -243,7 +251,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile
? `${itemIndex + 1}.`
: '-'
: item.checked
? t('m.ywMqsWY')
? t('mobileMarkdown.x')
: '[ ]'}
</Text>
<Text style={[styles.listText, listScale]}>
+6 -5
View File
@@ -167,12 +167,12 @@ function PrSidebarContent({
return (
<View style={styles.stateArea}>
<ActivityIndicator color={colors.textSecondary} />
<Text style={styles.stateText}>{t('m.-ZCN2f0')}</Text>
<Text style={styles.stateText}>{t('mobilePrsidebar.loading')}</Text>
</View>
)
}
if (branch === 'error') {
const message = state.kind === 'error' ? state.message : t('m.nfDKqg4')
const message = state.kind === 'error' ? state.message : t('mobilePrsidebar.something')
return (
<View style={styles.stateArea}>
<Text style={styles.stateText}>{message}</Text>
@@ -180,10 +180,10 @@ function PrSidebarContent({
style={styles.retryButton}
onPress={onRetry}
accessibilityRole="button"
accessibilityLabel={t('m.Re9yZkw')}
accessibilityLabel={t('mobilePrsidebar.retryLoading')}
>
<RotateCw size={14} color={colors.textPrimary} strokeWidth={2.2} />
<Text style={styles.retryText}>{t('m.viizK3M')}</Text>
<Text style={styles.retryText}>{t('mobilePrsidebar.retry')}</Text>
</Pressable>
</View>
)
@@ -191,7 +191,8 @@ function PrSidebarContent({
if (branch === 'blocked' || actions.blocked) {
// Permanent failure (R9): explanatory, no retry-encouragement styling. A
// mutation-time block (actions.blocked) routes here even from a ready state.
const message = actions.blocked ?? (state.kind === 'blocked' ? state.message : t('m.UL6zDWw'))
const message =
actions.blocked ?? (state.kind === 'blocked' ? state.message : t('mobilePrsidebar.not'))
return (
<View style={styles.stateArea}>
<Text style={styles.blockedText}>{message}</Text>
+1 -1
View File
@@ -87,7 +87,7 @@ export function MobilePrBasePicker({
}}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder={t('m.O3OecWo')}
placeholder={t('mobilePrBasePicker.main')}
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
@@ -91,21 +91,69 @@ type ToolbarItem = {
}
const TOOLBAR_ITEMS: ToolbarItem[] = [
{ command: 'paragraph', label: t('m.3Ph2vuM'), icon: Pilcrow },
{ command: 'heading1', label: t('m.Tkw-XG4'), icon: Heading1 },
{ command: 'heading2', label: t('m.T0-0mLw'), icon: Heading2 },
{ command: 'heading3', label: t('m.qpILEoE'), icon: Heading3 },
{ command: 'bold', label: t('m.TcUIfHg'), icon: Bold },
{ command: 'italic', label: t('m.zwoUjcU'), icon: Italic },
{ command: 'strike', label: t('m.wwdkeIc'), icon: Strikethrough },
{ command: 'bulletList', label: t('m.hKyID9A'), icon: List },
{ command: 'orderedList', label: t('m._79TK6I'), icon: ListOrdered },
{ command: 'taskList', label: t('m.jPlQhig'), icon: ListTodo },
{ command: 'quote', label: t('m.dQOT7tM'), icon: Quote },
{ command: 'link', label: t('m.eLAtZAk'), icon: Link },
{ command: 'image', label: t('m.fT3N_UQ'), icon: ImageIcon },
{ command: 'inlineCode', label: t('m.h1ikg-I'), icon: Code2 },
{ command: 'codeBlock', label: t('m.sLMnsJA'), icon: FileCode2 }
{
command: 'paragraph',
label: t('mobileRichMarkdownEditor.body'),
icon: Pilcrow
},
{
command: 'heading1',
label: t('mobileRichMarkdownEditor.h1'),
icon: Heading1
},
{
command: 'heading2',
label: t('mobileRichMarkdownEditor.h2'),
icon: Heading2
},
{
command: 'heading3',
label: t('mobileRichMarkdownEditor.h3'),
icon: Heading3
},
{ command: 'bold', label: t('mobileRichMarkdownEditor.bold'), icon: Bold },
{
command: 'italic',
label: t('mobileRichMarkdownEditor.italic'),
icon: Italic
},
{
command: 'strike',
label: t('mobileRichMarkdownEditor.strike'),
icon: Strikethrough
},
{
command: 'bulletList',
label: t('mobileRichMarkdownEditor.bullet'),
icon: List
},
{
command: 'orderedList',
label: t('mobileRichMarkdownEditor.numbered'),
icon: ListOrdered
},
{
command: 'taskList',
label: t('mobileRichMarkdownEditor.checklist'),
icon: ListTodo
},
{ command: 'quote', label: t('mobileRichMarkdownEditor.quote'), icon: Quote },
{ command: 'link', label: t('mobileRichMarkdownEditor.link'), icon: Link },
{
command: 'image',
label: t('mobileRichMarkdownEditor.image'),
icon: ImageIcon
},
{
command: 'inlineCode',
label: t('mobileRichMarkdownEditor.inline'),
icon: Code2
},
{
command: 'codeBlock',
label: t('mobileRichMarkdownEditor.code'),
icon: FileCode2
}
]
function MobileRichMarkdownEditorInner({
+1 -1
View File
@@ -44,7 +44,7 @@ export function MobileSearchField({
placeholder,
onClear,
showClear,
clearAccessibilityLabel = t('m.5GRCze0'),
clearAccessibilityLabel = t('mobileSearchField.clear'),
autoFocus = false,
focusKey,
returnKeyType = 'search',
@@ -33,7 +33,7 @@ export function MobileWorkspaceNameInput({
return (
<TextInput
ref={inputRef}
placeholder={t('m.UIMZetU')}
placeholder={t('mobileWorkspaceNameInput.workspace')}
autoCapitalize="none"
autoCorrect={false}
showSoftInputOnFocus
+1 -1
View File
@@ -28,7 +28,7 @@ export function NewWorkspaceFab({ onPress, disabled }: NewWorkspaceFabProps): Re
onPress={onPress}
disabled={disabled}
accessibilityRole="button"
accessibilityLabel={t('m.ybPEhxk')}
accessibilityLabel={t('newWorkspaceFab.new')}
hitSlop={8}
>
<Plus size={24} color={colors.bgBase} strokeWidth={2.75} />
+45 -29
View File
@@ -430,7 +430,7 @@ function NewWorktreeModalContent({
setSshState({
targetId: selectedRepoConnectionId,
status: 'error',
error: err instanceof Error ? err.message : t('m.Gli-J-E'),
error: err instanceof Error ? err.message : t('newWorktreeModal.failedRead'),
reconnectAttempt: 0
})
}
@@ -554,7 +554,7 @@ function NewWorktreeModalContent({
setSshState({
targetId: selectedRepoConnectionId,
status: 'error',
error: err instanceof Error ? err.message : t('m.FYj9qCM'),
error: err instanceof Error ? err.message : t('newWorktreeModal.failedConnect'),
reconnectAttempt: 0
})
} finally {
@@ -572,7 +572,11 @@ function NewWorktreeModalContent({
try {
if (sshGate.requiresConnection) {
setError(t('m.9ZRI2cs', { value0: selectedRepo.displayName }))
setError(
t('newWorktreeModal.connectDisplay', {
displayName: selectedRepo.displayName
})
)
return
}
let latestRuntimeSettings = runtimeSettings
@@ -592,7 +596,7 @@ function NewWorktreeModalContent({
) {
setSelectedAgent(pickPreferredNewWorktreeAgent(latestRuntimeSettings, detectedAgentIds))
setAgentOverridden(false)
setError(t('m.F1iAV4A'))
setError(t('newWorktreeModal.selected'))
return
}
@@ -612,7 +616,7 @@ function NewWorktreeModalContent({
setupDecision = options.setupOverride
} else if (setupRunPolicy === 'ask') {
if (!setupDecisionChoice) {
setError(t('m.rwQDraY'))
setError(t('newWorktreeModal.choose'))
return
}
setupDecision = setupDecisionChoice
@@ -670,7 +674,7 @@ function NewWorktreeModalContent({
onClose()
onCreated(result.worktreeId, result.name)
} catch (e) {
setError(e instanceof Error ? e.message : t('m.jSxyaws'))
setError(e instanceof Error ? e.message : t('newWorktreeModal.failedCreate'))
} finally {
createInFlightRef.current = false
setCreating(false)
@@ -743,7 +747,7 @@ function NewWorktreeModalContent({
transitionDrawer('form')
await handleCreate({ setupOverride: 'run', approvedSetupContentHash: approvedHash })
} catch (err) {
setError(err instanceof Error ? err.message : t('m.C8UjQk0'))
setError(err instanceof Error ? err.message : t('newWorktreeModal.failedTrust'))
} finally {
setupTrustActionInFlightRef.current = false
if (!createInFlightRef.current) {
@@ -788,8 +792,8 @@ function NewWorktreeModalContent({
>
<BottomDrawer visible={formSheetVisible} interactive={formSheetInteractive} onClose={onClose}>
<View style={styles.header}>
<Text style={styles.title}>{t('m.ZK7Il8w')}</Text>
<Text style={styles.subtitle}>{t('m.sMWTBAA')}</Text>
<Text style={styles.title}>{t('newWorktreeModal.create')}</Text>
<Text style={styles.subtitle}>{t('newWorktreeModal.pick')}</Text>
</View>
{loading ? (
@@ -798,12 +802,12 @@ function NewWorktreeModalContent({
</View>
) : repos.length === 0 ? (
<View style={styles.loadingContainer}>
<Text style={styles.emptyText}>{t('m.osrkufA')}</Text>
<Text style={styles.emptyText}>{t('newWorktreeModal.no')}</Text>
</View>
) : (
<>
<View style={styles.field}>
<Text style={styles.label}>{t('m.38lAWJQ')}</Text>
<Text style={styles.label}>{t('newWorktreeModal.repository')}</Text>
<Pressable
style={styles.fieldButton}
onPress={() => {
@@ -820,7 +824,7 @@ function NewWorktreeModalContent({
style={[styles.fieldButtonText, !selectedRepo && styles.fieldButtonPlaceholder]}
numberOfLines={1}
>
{selectedRepo?.displayName ?? t('m.2Kl8BY4')}
{selectedRepo?.displayName ?? t('newWorktreeModal.select')}
</Text>
<ChevronDown size={14} color={colors.textMuted} />
</Pressable>
@@ -828,7 +832,9 @@ function NewWorktreeModalContent({
<SmartWorkspaceSourceField
composer={composer}
label={selectedRepoIsGit ? t('m.yymZMiQ') : t('m.EFTSrpI')}
label={
selectedRepoIsGit ? t('newWorktreeModal.name') : t('newWorktreeModal.workspace')
}
disabled={sshGate.requiresConnection}
interactive={formSheetInteractive}
onBeforeOpen={() => setError('')}
@@ -841,7 +847,7 @@ function NewWorktreeModalContent({
{selectedRepoConnectionId ? (
<View style={styles.field}>
<Text style={styles.label}>{t('m.JNDDz3c')}</Text>
<Text style={styles.label}>{t('newWorktreeModal.ssh')}</Text>
<View style={styles.sshBox}>
<View style={styles.sshRow}>
<View
@@ -856,7 +862,7 @@ function NewWorktreeModalContent({
/>
<View style={styles.sshCopy}>
<Text style={styles.sshTitle} numberOfLines={1}>
{selectedRepo?.displayName ?? t('m.AmkPWYA')}
{selectedRepo?.displayName ?? t('newWorktreeModal.remote')}
</Text>
<Text style={styles.sshSubtitle}>
{workspaceSshStatusLabel(sshGate.status)}
@@ -872,7 +878,9 @@ function NewWorktreeModalContent({
onPress={() => void connectSelectedSshRepo()}
>
<Text style={styles.sshConnectText}>
{sshGate.connectInProgress ? t('m.oroOvnM') : t('m.O93svfk')}
{sshGate.connectInProgress
? t('newWorktreeModal.connecting')
: t('newWorktreeModal.connect')}
</Text>
</Pressable>
)}
@@ -883,7 +891,7 @@ function NewWorktreeModalContent({
) : null}
<View style={styles.field}>
<Text style={styles.label}>{t('m.VEK8Z7E')}</Text>
<Text style={styles.label}>{t('newWorktreeModal.agent')}</Text>
<Pressable
style={[styles.fieldButton, sshGate.requiresConnection && styles.disabled]}
disabled={sshGate.requiresConnection}
@@ -894,14 +902,16 @@ function NewWorktreeModalContent({
>
<MobileAgentIcon agentId={selectedAgent.id} size={16} />
<Text style={styles.fieldButtonText} numberOfLines={1}>
{sshGate.requiresConnection ? t('m.M2A4pJA') : selectedAgent.label}
{sshGate.requiresConnection
? t('newWorktreeModal.connectRepositoryFirst')
: selectedAgent.label}
</Text>
<ChevronDown size={14} color={colors.textMuted} />
</Pressable>
</View>
<Pressable style={styles.advancedToggle} onPress={() => setShowAdvanced(!showAdvanced)}>
<Text style={styles.advancedText}>{t('m.ATo76K0')}</Text>
<Text style={styles.advancedText}>{t('newWorktreeModal.advanced')}</Text>
{showAdvanced ? (
<ChevronUp size={14} color={colors.textSecondary} />
) : (
@@ -917,12 +927,12 @@ function NewWorktreeModalContent({
/>
<View style={styles.field}>
<Text style={styles.label}>{t('m.LMAqKNQ')}</Text>
<Text style={styles.label}>{t('newWorktreeModal.note')}</Text>
<TextInput
style={styles.input}
value={note}
onChangeText={setNote}
placeholder={t('m.by8gktU')}
placeholder={t('newWorktreeModal.write')}
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
@@ -932,11 +942,13 @@ function NewWorktreeModalContent({
{setupCommand ? (
<View style={styles.field}>
<View style={styles.setupHeader}>
<Text style={styles.label}>{t('m.0cbkbTE')}</Text>
<Text style={styles.label}>{t('newWorktreeModal.setup')}</Text>
{setupSource && (
<View style={styles.sourceBadge}>
<Text style={styles.sourceBadgeText}>
{setupSource === 'orca.yaml' ? t('m.hiRz7rE') : t('m.8ckUTUg')}
{setupSource === 'orca.yaml'
? t('newWorktreeModal.orca')
: t('newWorktreeModal.hooks')}
</Text>
</View>
)}
@@ -951,7 +963,7 @@ function NewWorktreeModalContent({
]}
onPress={() => setSetupDecisionChoice('run')}
>
<Text style={styles.setupChoiceText}>{t('m.0qYyICI')}</Text>
<Text style={styles.setupChoiceText}>{t('newWorktreeModal.run')}</Text>
</Pressable>
<Pressable
style={[
@@ -960,12 +972,14 @@ function NewWorktreeModalContent({
]}
onPress={() => setSetupDecisionChoice('skip')}
>
<Text style={styles.setupChoiceText}>{t('m.sZHa-TU')}</Text>
<Text style={styles.setupChoiceText}>{t('newWorktreeModal.skip')}</Text>
</Pressable>
</View>
) : (
<View style={styles.setupToggleRow}>
<Text style={styles.setupToggleLabel}>{t('m.obLmzkw')}</Text>
<Text style={styles.setupToggleLabel}>
{t('newWorktreeModal.runSetup')}
</Text>
<Switch
value={runSetup}
onValueChange={setRunSetup}
@@ -996,7 +1010,9 @@ function NewWorktreeModalContent({
<ActivityIndicator size="small" color={colors.bgBase} />
) : (
<Text style={styles.createText}>
{sshGate.requiresConnection ? t('m.g5YUeP0') : t('m.ZK7Il8w')}
{sshGate.requiresConnection
? t('newWorktreeModal.connectRepository')
: t('newWorktreeModal.create')}
</Text>
)}
</Pressable>
@@ -1026,7 +1042,7 @@ function NewWorktreeModalContent({
<PickerListDrawer
visible={visible && drawerView === 'repo'}
title={t('m.38lAWJQ')}
title={t('newWorktreeModal.repository')}
items={repoPickerItems}
selectedId={selectedRepo?.id ?? ''}
onSelect={(item) => handleRepoSelected(item.repo)}
@@ -1038,7 +1054,7 @@ function NewWorktreeModalContent({
<PickerListDrawer
visible={visible && drawerView === 'agent'}
title={t('m.VEK8Z7E')}
title={t('newWorktreeModal.agent')}
items={pickerAgentOptions}
selectedId={selectedAgent.id}
onSelect={(agent) => {
@@ -17,20 +17,22 @@ export function ProtocolBlockScreen({ verdict }: Props) {
const mobileUpdateTarget =
Platform.OS === 'ios'
? {
label: t('m.oxg5VTs'),
label: t('protocolBlockScreen.openApp'),
url: IOS_APP_STORE_URL,
storeName: t('protocol.block.appStoreName')
}
: {
label: t('m.h3ZwGv0'),
label: t('protocolBlockScreen.openGit'),
url: RELEASES_URL,
storeName: t('protocol.block.githubReleasesName')
}
const primaryAction = isMobileTooOld
? { label: mobileUpdateTarget.label, url: mobileUpdateTarget.url }
: { label: t('m.h3ZwGv0'), url: RELEASES_URL }
: { label: t('protocolBlockScreen.openGit'), url: RELEASES_URL }
const title = isMobileTooOld ? t('m.NkNSfjQ') : t('m.amP19yQ')
const title = isMobileTooOld
? t('protocolBlockScreen.updateOrcaMobile')
: t('protocolBlockScreen.updateOrcaYour')
const body = isMobileTooOld
? t('protocol.block.mobileTooOldBody', { storeName: mobileUpdateTarget.storeName })
: t('protocol.block.desktopTooOldBody')
@@ -57,7 +59,7 @@ export function ProtocolBlockScreen({ verdict }: Props) {
router.replace('/')
}}
>
<Text style={styles.secondaryButtonText}>{t('m.kA_OuTE')}</Text>
<Text style={styles.secondaryButtonText}>{t('protocolBlockScreen.back')}</Text>
</Pressable>
<Text style={styles.recoveryNote}>{recoveryNote}</Text>
</View>
+13 -7
View File
@@ -41,15 +41,21 @@ export function SetupHookTrustDrawer({
<View style={styles.trustHeader}>
<Text style={styles.title}>
{prompt.previouslyApproved
? t('m.egQ-qgQ', { value0: prompt.repoName })
: t('m.SSMlLr8', { value0: prompt.repoName })}
? t('setupHookTrustDrawer.repo', {
repoName: prompt.repoName
})
: t('setupHookTrustDrawer.runSetup', {
repoName: prompt.repoName
})}
</Text>
<Text style={styles.subtitle}>{t('m.JU7DVfY')}</Text>
<Text style={styles.subtitle}>{t('setupHookTrustDrawer.repositorys')}</Text>
</View>
<View style={styles.trustScriptBox}>
<Text style={styles.trustScriptLabel}>
{prompt.previouslyApproved ? t('m.gxXtGpI') : t('m.m7jxYm8')}
{prompt.previouslyApproved
? t('setupHookTrustDrawer.new')
: t('setupHookTrustDrawer.setup')}
</Text>
<Text style={styles.trustScriptText}>{prompt.scriptContent}</Text>
</View>
@@ -57,16 +63,16 @@ export function SetupHookTrustDrawer({
<View style={styles.trustActionGroup}>
<Pressable style={styles.trustActionRow} disabled={busy} onPress={onRunOnce}>
<Check size={16} color={colors.textPrimary} />
<Text style={styles.trustActionText}>{t('m.wOCs5LA')}</Text>
<Text style={styles.trustActionText}>{t('setupHookTrustDrawer.runHooks')}</Text>
</Pressable>
<View style={styles.trustActionSeparator} />
<Pressable style={styles.trustActionRow} disabled={busy} onPress={onAlwaysTrust}>
<Check size={16} color={colors.textPrimary} />
<Text style={styles.trustActionText}>{t('m.hUSRq7I')}</Text>
<Text style={styles.trustActionText}>{t('setupHookTrustDrawer.always')}</Text>
</Pressable>
<View style={styles.trustActionSeparator} />
<Pressable style={styles.trustActionRow} disabled={busy} onPress={onDontRun}>
<Text style={styles.trustActionText}>{t('m.Pe9Dmhc')}</Text>
<Text style={styles.trustActionText}>{t('setupHookTrustDrawer.do')}</Text>
</Pressable>
</View>
</View>
@@ -18,12 +18,12 @@ export function SmartWorkspaceAdvancedFields({ composer, selectedRepoIsGit }: Pr
<>
{selection ? (
<View style={styles.field}>
<Text style={styles.label}>{t('m.jueQw6Q')}</Text>
<Text style={styles.label}>{t('smartWorkspaceAdvancedFields.name')}</Text>
<TextInput
style={styles.input}
value={composer.name}
onChangeText={composer.setName}
placeholder={t('m.MJ_zXUU')}
placeholder={t('smartWorkspaceAdvancedFields.workspace')}
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
@@ -33,12 +33,12 @@ export function SmartWorkspaceAdvancedFields({ composer, selectedRepoIsGit }: Pr
{showBranchOverride ? (
<View style={styles.field}>
<Text style={styles.label}>{t('m.CATZTEM')}</Text>
<Text style={styles.label}>{t('smartWorkspaceAdvancedFields.branch')}</Text>
<TextInput
style={styles.input}
value={composer.branchNameOverride ?? ''}
onChangeText={composer.handleBranchNameOverrideChange}
placeholder={t('m.Lkm1Ms4')}
placeholder={t('smartWorkspaceAdvancedFields.derived')}
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
@@ -50,7 +50,9 @@ export function SmartWorkspaceAdvancedFields({ composer, selectedRepoIsGit }: Pr
<View style={styles.field}>
<View style={styles.reuseRow}>
<Text style={styles.reuseLabel} numberOfLines={1}>
{t('m.Yz166iM', { value0: composer.reuseEligibleBranch })}
{t('smartWorkspaceAdvancedFields.reuse', {
reuseEligibleBranch: composer.reuseEligibleBranch
})}
</Text>
<Switch
value={composer.reuseSelectedBranch}
@@ -197,9 +197,9 @@ export function SmartWorkspaceSourceDrawer({
top; dock must stay a non-flex sibling so FlatList cannot clip it. */}
<View style={styles.root}>
<View style={styles.header}>
<Text style={styles.title}>{t('m.J6XOFqM')}</Text>
<Text style={styles.title}>{t('smartWorkspaceSourceDrawer.name')}</Text>
<Pressable onPress={onClose} hitSlop={8}>
<Text style={styles.done}>{t('m.A6zs-7I')}</Text>
<Text style={styles.done}>{t('smartWorkspaceSourceDrawer.done')}</Text>
</Pressable>
</View>
@@ -207,21 +207,25 @@ export function SmartWorkspaceSourceDrawer({
{crossRepoPrompt ? (
<View style={styles.crossRepo}>
<Text style={styles.crossRepoText}>
{t('m.WDddFyg', {
value0: crossRepoPrompt.link.slug.owner,
value1: crossRepoPrompt.link.slug.repo
{t('smartWorkspaceSourceDrawer.item', {
owner: crossRepoPrompt.link.slug.owner,
repo: crossRepoPrompt.link.slug.repo
})}
</Text>
<View style={styles.crossRepoActions}>
<Pressable style={styles.crossRepoDismiss} onPress={dismissCrossRepoPrompt}>
<Text style={styles.crossRepoDismissText}>{t('m.h8XKauY')}</Text>
<Text style={styles.crossRepoDismissText}>
{t('smartWorkspaceSourceDrawer.cancel')}
</Text>
</Pressable>
<Pressable
style={styles.crossRepoSwitch}
onPress={() => void handleAcceptCrossRepo()}
>
<Text style={styles.crossRepoSwitchText}>
{t('m.FLDQvbE', { value0: crossRepoPrompt.matchingRepo.displayName })}
{t('smartWorkspaceSourceDrawer.switch', {
displayName: crossRepoPrompt.matchingRepo.displayName
})}
</Text>
</Pressable>
</View>
@@ -229,9 +233,9 @@ export function SmartWorkspaceSourceDrawer({
) : null}
{!sshReady && effectiveMode !== 'text' && effectiveMode !== 'linear' ? (
<Text style={styles.notice}>{t('m.2sNZWLI')}</Text>
<Text style={styles.notice}>{t('smartWorkspaceSourceDrawer.connect')}</Text>
) : needsGitHubRemote ? (
<Text style={styles.notice}>{t('m.LbOI-wE')}</Text>
<Text style={styles.notice}>{t('smartWorkspaceSourceDrawer.ssh')}</Text>
) : error ? (
<Text style={styles.errorNotice}>{error}</Text>
) : null}
@@ -250,9 +254,9 @@ export function SmartWorkspaceSourceDrawer({
<ActivityIndicator size="small" color={colors.textSecondary} />
</View>
) : showEmpty ? (
<Text style={styles.empty}>{emptyHint || t('m.QjSzvc4')}</Text>
<Text style={styles.empty}>{emptyHint || t('smartWorkspaceSourceDrawer.no')}</Text>
) : rows.length === 0 && effectiveMode === 'text' ? (
<Text style={styles.empty}>{t('m.qNPX_ws')}</Text>
<Text style={styles.empty}>{t('smartWorkspaceSourceDrawer.typeWorkspace')}</Text>
) : null
}
renderItem={({ item }) => (
@@ -305,7 +309,7 @@ export function SmartWorkspaceSourceDrawer({
style={styles.search}
value={composer.name}
onChangeText={composer.setName}
placeholder={t('m.qOsqJv0')}
placeholder={t('smartWorkspaceSourceDrawer.typeName')}
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
@@ -62,7 +62,7 @@ export function SmartWorkspaceSourceField({
return (
<View style={styles.field}>
<Text style={styles.label}>
{label} <Text style={styles.labelHint}>{t('m.eMl2uLM')}</Text>
{label} <Text style={styles.labelHint}>{t('smartWorkspaceSourceField.optional')}</Text>
</Text>
{selection ? (
<View style={styles.pill}>
@@ -92,7 +92,7 @@ export function SmartWorkspaceSourceField({
onChangeText={composer.setName}
onFocus={openDrawer}
editable={!disabled && interactive}
placeholder={t('m.HHq8Y80')}
placeholder={t('smartWorkspaceSourceField.type')}
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
@@ -22,27 +22,29 @@ function resolveRowContent(row: SourceRow): RowContent {
case 'use-name':
return {
icon: <Sparkles size={16} color={colors.textSecondary} />,
title: t('m.uK2T1OQ', { value0: row.name }),
subtitle: t('m.Z7csSYQ')
title: t('smartWorkspaceSourceRow.use', { rowName: row.name }),
subtitle: t('smartWorkspaceSourceRow.name')
}
case 'create-branch':
return {
icon: <GitBranch size={16} color={colors.accentBlue} />,
title: t('m.dDSXnKk', { value0: row.name }),
subtitle: t('m.zTK803o')
title: t('smartWorkspaceSourceRow.create', {
rowName: row.name
}),
subtitle: t('smartWorkspaceSourceRow.new')
}
case 'github':
return {
icon: <TaskProviderLogo provider="github" size={16} color={colors.textSecondary} />,
title: row.item.title,
subtitle: `${row.item.type === 'pr' ? t('m.Jq988q0') : t('m.kWDpsSU')}${row.item.number}`,
subtitle: `${row.item.type === 'pr' ? t('smartWorkspaceSourceRow.pr') : t('smartWorkspaceSourceRow.issue')}${row.item.number}`,
status: row.item.state
}
case 'gitlab':
return {
icon: <TaskProviderLogo provider="gitlab" size={16} color={colors.textSecondary} />,
title: row.item.title,
subtitle: `${row.item.type === 'mr' ? t('m.G_t8Rjg') : t('m.kWDpsSU')}${row.item.number}`,
subtitle: `${row.item.type === 'mr' ? t('smartWorkspaceSourceRow.mr') : t('smartWorkspaceSourceRow.issue')}${row.item.number}`,
status: row.item.state
}
case 'branch':
@@ -55,7 +57,7 @@ function resolveRowContent(row: SourceRow): RowContent {
return {
icon: <TaskProviderLogo provider="linear" size={16} color={colors.textSecondary} />,
title: row.issue.title,
subtitle: `${row.issue.identifier} · ${row.issue.team?.key ?? t('m.17Ky5K0')}`,
subtitle: `${row.issue.identifier} · ${row.issue.team?.key ?? t('smartWorkspaceSourceRow.linear')}`,
status: row.issue.state?.name
}
default:
@@ -223,8 +223,10 @@ export function TerminalShortcutSettings({
return (
<>
<Text style={[styles.groupHeading, styles.groupTopGap]}>{t('m.u1ZHouk')}</Text>
<Text style={styles.groupDescription}>{t('m.Odmd4jg')}</Text>
<Text style={[styles.groupHeading, styles.groupTopGap]}>
{t('terminalShortcutSettings.shortcut')}
</Text>
<Text style={styles.groupDescription}>{t('terminalShortcutSettings.toggle')}</Text>
<View style={[styles.section, styles.sectionTopGap]}>
<DragReorderList
items={orderedAccessoryKeys}
@@ -248,18 +250,20 @@ export function TerminalShortcutSettings({
onPress={resetBuiltInKeys}
>
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>{t('m.P0qiUsA')}</Text>
<Text style={styles.rowSublabel}>{t('m._YyvrjM')}</Text>
<Text style={styles.rowLabel}>{t('terminalShortcutSettings.reset')}</Text>
<Text style={styles.rowSublabel}>{t('terminalShortcutSettings.show')}</Text>
</View>
</Pressable>
</View>
<Text style={[styles.groupHeading, styles.groupTopGap]}>{t('m.EGxWrwk')}</Text>
<Text style={[styles.groupHeading, styles.groupTopGap]}>
{t('terminalShortcutSettings.custom')}
</Text>
<View style={[styles.section, styles.sectionTopGap]}>
{customKeys.length === 0 ? (
<>
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>{t('m.AwkMohI')}</Text>
<Text style={styles.emptyText}>{t('terminalShortcutSettings.no')}</Text>
</View>
<View style={styles.separator} />
</>
@@ -302,8 +306,8 @@ export function TerminalShortcutSettings({
onPress={() => setShowCustomKeyModal(true)}
>
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>{t('m.d-64Q0E')}</Text>
<Text style={styles.rowSublabel}>{t('m.CgJBkqk')}</Text>
<Text style={styles.rowLabel}>{t('terminalShortcutSettings.add')}</Text>
<Text style={styles.rowSublabel}>{t('terminalShortcutSettings.create')}</Text>
</View>
<ChevronRight size={16} color={colors.textMuted} />
</Pressable>
+2 -2
View File
@@ -32,7 +32,7 @@ export function TextInputModal({
message,
defaultValue = '',
placeholder,
submitLabel = t('m.GbS1Gw0'),
submitLabel = t('textInputModal.save'),
selectTextOnFocus = false,
allowEmpty = false,
keyboardType,
@@ -91,7 +91,7 @@ export function TextInputModal({
style={({ pressed }) => [styles.cancelButton, pressed && styles.buttonPressed]}
onPress={onCancel}
>
<Text style={styles.cancelText}>{t('m.NOYO7QE')}</Text>
<Text style={styles.cancelText}>{t('textInputModal.cancel')}</Text>
</Pressable>
<Pressable
style={({ pressed }) => [
+7 -5
View File
@@ -69,21 +69,21 @@ export function VoiceModelList({
{model.label}
</Text>
{model.recommended ? (
<Text style={styles.recommended}>{t('m.B45mQIQ')}</Text>
<Text style={styles.recommended}>{t('voiceModelList.recommended')}</Text>
) : null}
</View>
<Text style={styles.modelMeta}>{modelMeta(model)}</Text>
</View>
{model.provider === 'openai' ? (
<Text style={styles.modelStateText}>
{model.status === 'ready' ? t('m.JnTRemk') : t('m.j2yOGiA')}
{model.status === 'ready' ? t('voiceModelList.api') : t('voiceModelList.set')}
</Text>
) : model.status === 'ready' ? (
<View style={styles.readyActions}>
{isSelected ? (
<View style={styles.selectedTag}>
<Check size={14} color={colors.statusGreen} strokeWidth={2.4} />
<Text style={styles.selectedText}>{t('m.kg-vs7o')}</Text>
<Text style={styles.selectedText}>{t('voiceModelList.use')}</Text>
</View>
) : (
<Pressable
@@ -97,7 +97,7 @@ export function VoiceModelList({
{selectBusy ? (
<ActivityIndicator size="small" color={colors.textSecondary} />
) : (
<Text style={styles.actionText}>{t('m.EorN5mI')}</Text>
<Text style={styles.actionText}>{t('voiceModelList.useMessage')}</Text>
)}
</Pressable>
)}
@@ -105,7 +105,9 @@ export function VoiceModelList({
style={({ pressed }) => [styles.iconButton, pressed && styles.actionPressed]}
disabled={anyBusy}
onPress={() => onDelete(model)}
accessibilityLabel={t('m.r6942Dw', { value0: model.label })}
accessibilityLabel={t('voiceModelList.delete', {
commandLabel: model.label
})}
>
{deleteBusy ? (
<ActivityIndicator size="small" color={colors.statusRed} />
@@ -11,8 +11,8 @@ export function WorkspaceDetailPlaceholder() {
<View style={styles.icon}>
<SquareTerminal size={28} color={colors.textMuted} />
</View>
<Text style={styles.title}>{t('m.qe5Mx6E')}</Text>
<Text style={styles.body}>{t('m.6qylM4Q')}</Text>
<Text style={styles.title}>{t('workspaceDetailPlaceholder.no')}</Text>
<Text style={styles.body}>{t('workspaceDetailPlaceholder.pick')}</Text>
</View>
)
}
+2 -2
View File
@@ -131,7 +131,7 @@ function WorktreeListRowComponent<T extends WorktreeListRowItem>({
)}
{isFolderWorkspace && (
<View style={styles.folderBadge}>
<Text style={styles.folderBadgeText}>{t('m.uQ24VuE')}</Text>
<Text style={styles.folderBadgeText}>{t('worktreeListRow.folder')}</Text>
</View>
)}
<WorktreeMetaGlyphs
@@ -146,7 +146,7 @@ function WorktreeListRowComponent<T extends WorktreeListRowItem>({
{lineageDepth > 0 && (
<View style={styles.childBadge}>
<GitBranch size={10} color={colors.textMuted} />
<Text style={styles.childBadgeText}>{t('m.lCIVZpI')}</Text>
<Text style={styles.childBadgeText}>{t('worktreeListRow.child')}</Text>
</View>
)}
{/* Repo glyph+name only when not already grouped under this repo;
+9 -9
View File
@@ -82,7 +82,7 @@ export const RateLimitRuntimeTargetSchema = z
if (target.runtime === 'host' && target.wslDistro !== null) {
context.addIssue({
code: 'custom',
message: t('m.EIhMdN0'),
message: t('accountsSnapshot.hostRate'),
path: ['wslDistro']
})
}
@@ -93,7 +93,7 @@ export const RateLimitRuntimeTargetSchema = z
) {
context.addIssue({
code: 'custom',
message: t('m.ZlNO6fQ'),
message: t('accountsSnapshot.wslRate'),
path: ['wslDistro']
})
}
@@ -138,7 +138,7 @@ const CodexAccountSummarySchema = z
if (runtime === 'host' && account.wslDistro != null) {
context.addIssue({
code: 'custom',
message: t('m.OZiafyo'),
message: t('accountsSnapshot.hostCodex'),
path: ['wslDistro']
})
}
@@ -149,7 +149,7 @@ const CodexAccountSummarySchema = z
) {
context.addIssue({
code: 'custom',
message: t('m.95Xks08'),
message: t('accountsSnapshot.wslCodex'),
path: ['wslDistro']
})
}
@@ -189,14 +189,14 @@ export const AccountsSnapshotSchema = z
if (snapshot.rateLimits.claude && snapshot.rateLimits.claude.provider !== 'claude') {
context.addIssue({
code: 'custom',
message: t('m.OWfo7Qc'),
message: t('accountsSnapshot.claude'),
path: ['rateLimits', 'claude', 'provider']
})
}
if (snapshot.rateLimits.codex && snapshot.rateLimits.codex.provider !== 'codex') {
context.addIssue({
code: 'custom',
message: t('m.N8kYEP4'),
message: t('accountsSnapshot.codex'),
path: ['rateLimits', 'codex', 'provider']
})
}
@@ -204,7 +204,7 @@ export const AccountsSnapshotSchema = z
if (entry.rateLimits && entry.rateLimits.provider !== 'claude') {
context.addIssue({
code: 'custom',
message: t('m.4NLLG4A'),
message: t('accountsSnapshot.inactiveClaude'),
path: ['rateLimits', 'inactiveClaudeAccounts', index, 'rateLimits', 'provider']
})
}
@@ -213,7 +213,7 @@ export const AccountsSnapshotSchema = z
if (entry.rateLimits && entry.rateLimits.provider !== 'codex') {
context.addIssue({
code: 'custom',
message: t('m.CkCg0Xk'),
message: t('accountsSnapshot.inactiveCodex'),
path: ['rateLimits', 'inactiveCodexAccounts', index, 'rateLimits', 'provider']
})
}
@@ -230,7 +230,7 @@ export type AccountsSnapshot = z.infer<typeof AccountsSnapshotSchema>
export class InvalidAccountsSnapshotError extends Error {
constructor() {
super(t('m.l0xDyns'))
super(t('accountsSnapshot.invalid'))
this.name = 'InvalidAccountsSnapshotError'
}
}
+24 -16
View File
@@ -76,16 +76,18 @@ export function getCodexResetCreditSummary(
expiryLabel =
count === 1
? duration === 'now'
? t('m.aaZ_2qE')
: t('m.1neVPtc', { value0: duration })
? t('codexResetCredit.expiresNow')
: t('codexResetCredit.expiresDuration', { duration: duration })
: duration === 'now'
? t('m.nH5aDJ4')
: t('m.fA_ezvI', { value0: duration })
? t('codexResetCredit.nextExpiresNow')
: t('codexResetCredit.nextExpiresDuration', { duration: duration })
}
return {
availableCount: count,
availabilityLabel:
count === 1 ? t('m.CfvCAaE', { value0: count }) : t('m.myZTLv0', { value0: count }),
count === 1
? t('codexResetCredit.resetCountReset', { resetCount: count })
: t('codexResetCredit.resetCountResets', { resetCount: count }),
expiryLabel
}
}
@@ -96,18 +98,24 @@ export function getCodexResetCreditOutcomeCopy(outcome: CodexResetCreditOutcome)
} {
switch (outcome) {
case 'reset':
return { title: t('m.QiJiFeQ'), message: t('m.3qHVa1w') }
return {
title: t('codexResetCredit.rate'),
message: t('codexResetCredit.codex')
}
case 'alreadyRedeemed':
return { title: t('m.9b3h0Pg'), message: t('m.3qHVa1w') }
return {
title: t('codexResetCredit.resetAlready'),
message: t('codexResetCredit.codex')
}
case 'nothingToReset':
return {
title: t('m.JMEauuw'),
message: t('m.Um-XfOo')
title: t('codexResetCredit.nothing'),
message: t('codexResetCredit.noEligible')
}
case 'noCredit':
return {
title: t('m._-grAdc'),
message: t('m.zFyydp8')
title: t('codexResetCredit.noReset'),
message: t('codexResetCredit.account')
}
}
}
@@ -164,12 +172,12 @@ function decodeResetResult(
expectedScope: CodexResetCreditExpectedScope
): CodexResetCreditRpcResult {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(t('m.dWotxRU'))
throw new Error(t('codexResetCredit.invalid'))
}
const result = value as Record<string, unknown>
const scope = CodexResetCreditExpectedScopeSchema.safeParse(result.scope)
if (!scope.success || !scopesEqual(scope.data, expectedScope)) {
throw new Error(t('m.dWotxRU'))
throw new Error(t('codexResetCredit.invalid'))
}
const snapshot = decodeAccountsSnapshot(result.snapshot)
if (result.status === 'rejectedBeforeProvider') {
@@ -184,7 +192,7 @@ function decodeResetResult(
reason !== 'offerUnavailable' &&
reason !== 'offerChanged')
) {
throw new Error(t('m.dWotxRU'))
throw new Error(t('codexResetCredit.invalid'))
}
return {
status: 'rejectedBeforeProvider',
@@ -203,7 +211,7 @@ function decodeResetResult(
outcome !== 'noCredit' &&
outcome !== 'alreadyRedeemed')
) {
throw new Error(t('m.dWotxRU'))
throw new Error(t('codexResetCredit.invalid'))
}
const snapshotAccount = snapshot.codex.accounts.find(
(account) => account.id === scope.data.accountId
@@ -214,7 +222,7 @@ function decodeResetResult(
getActiveCodexAccountIdForRateLimitTarget(snapshot) !== scope.data.accountId ||
snapshotAccount?.updatedAt !== scope.data.accountRevision
) {
throw new Error(t('m.dWotxRU'))
throw new Error(t('codexResetCredit.invalid'))
}
return {
outcome,
@@ -295,7 +295,7 @@ export function MountedBottomDrawer({
<Animated.View
style={styles.handleHitArea}
accessibilityRole="button"
accessibilityLabel={t('m.V9XaZZ8')}
accessibilityLabel={t('mountedBottomDrawer.dismiss')}
>
<View style={styles.handle} />
</Animated.View>
@@ -19,7 +19,7 @@ export const NEW_WORKTREE_AGENT_OPTIONS: NewWorktreeAgentOption[] = MOBILE_AGENT
export const NEW_WORKTREE_BLANK_AGENT: NewWorktreeAgentOption = {
id: '__blank__',
label: t('m.84CVutM')
label: t('newWorktreeAgentSelection.blank')
}
export function newWorktreeAgentOptionFor(id: string | null | undefined): NewWorktreeAgentOption {
@@ -26,7 +26,7 @@ export function MermaidDiagram({ source, base }: Props) {
return (
<View style={styles.frame}>
<View style={styles.label}>
<Text style={styles.labelText}>{t('m.W6bmrY4')}</Text>
<Text style={styles.labelText}>{t('mermaidDiagram.mermaid')}</Text>
</View>
<WebView
style={[styles.webview, { height: height || 120 }]}
@@ -65,7 +65,7 @@ function MermaidFallback({ source, base }: Props) {
return (
<View style={styles.frame}>
<View style={styles.label}>
<Text style={styles.labelText}>{t('m.W6bmrY4')}</Text>
<Text style={styles.labelText}>{t('mermaidDiagram.mermaid')}</Text>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.fallbackScroll}>
<Text style={[styles.fallbackText, { fontSize: base - 1 }]}>{source}</Text>
@@ -47,23 +47,23 @@ export function MobileLinkPrForm({ client, worktreeId, onCancel, onLinked }: Pro
return (
<View>
<View style={styles.headingRow}>
<Text style={styles.heading}>{t('m.f8ZsL9k')}</Text>
<Text style={styles.heading}>{t('mobileLinkPrForm.linkExisting')}</Text>
<Pressable
onPress={onCancel}
disabled={submitting}
accessibilityRole="button"
accessibilityLabel={t('m.ZLWuIYo')}
accessibilityLabel={t('mobileLinkPrForm.cancel')}
hitSlop={8}
>
<Text style={styles.cancelText}>{t('m.ZLWuIYo')}</Text>
<Text style={styles.cancelText}>{t('mobileLinkPrForm.cancel')}</Text>
</Pressable>
</View>
<Text style={styles.label}>{t('m.D2wJo3U')}</Text>
<Text style={styles.label}>{t('mobileLinkPrForm.pr')}</Text>
<TextInput
style={styles.input}
value={input}
onChangeText={setInput}
placeholder={t('m.5tB7Tdg')}
placeholder={t('mobileLinkPrForm.pull')}
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
@@ -83,7 +83,11 @@ export function MobileLinkPrForm({ client, worktreeId, onCancel, onLinked }: Pro
<ActivityIndicator size="small" color={colors.bgBase} />
) : (
<Text style={styles.submitText}>
{parsed ? t('m.UzLJy8Q', { value0: parsed }) : t('m.kPLcd1A')}
{parsed
? t('mobileLinkPrForm.linkLinked', {
linkedItemNumber: parsed
})
: t('mobileLinkPrForm.linkPull')}
</Text>
)}
</Pressable>
@@ -78,7 +78,7 @@ export function MobilePrComposeForm({
draft
})
if (!response.ok) {
setError(response.error?.message || t('m.Tf6JzYM'))
setError(response.error?.message || t('mobilePrComposeForm.failed'))
return
}
const result = (response as RpcSuccess).result as {
@@ -96,7 +96,7 @@ export function MobilePrComposeForm({
}
} catch (err) {
triggerError()
setError(err instanceof Error ? err.message : t('m.Tf6JzYM'))
setError(err instanceof Error ? err.message : t('mobilePrComposeForm.failed'))
} finally {
setGenerating(false)
}
@@ -167,7 +167,11 @@ export function MobilePrComposeForm({
<View style={styles.headingRow}>
<View style={styles.headingTitle}>
<ReviewIcon size={14} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.heading}>{t('m.gTT3Qik', { value0: copy.reviewLabel })}</Text>
<Text style={styles.heading}>
{t('mobilePrComposeForm.new', {
reviewType: copy.reviewLabel
})}
</Text>
</View>
<View style={styles.headingActions}>
<Pressable
@@ -175,21 +179,25 @@ export function MobilePrComposeForm({
disabled={generating || submitting}
onPress={() => void generate()}
accessibilityRole="button"
accessibilityLabel={t('m.h8LEFjY', { value0: copy.reviewLabel })}
accessibilityLabel={t('mobilePrComposeForm.generateReview', {
reviewType: copy.reviewLabel
})}
>
{generating ? (
<ActivityIndicator size="small" color={colors.textSecondary} />
) : (
<Sparkles size={13} color={colors.textSecondary} strokeWidth={2.1} />
)}
<Text style={styles.genButtonText}>{generating ? t('m.E5u75yA') : t('m.w4jN8ic')}</Text>
<Text style={styles.genButtonText}>
{generating ? t('mobilePrComposeForm.generating') : t('mobilePrComposeForm.generate')}
</Text>
</Pressable>
<Pressable
style={styles.iconButton}
onPress={onCancel}
disabled={submitting}
accessibilityRole="button"
accessibilityLabel={t('m.56ar5ss')}
accessibilityLabel={t('mobilePrComposeForm.cancel')}
hitSlop={8}
>
<X size={16} color={colors.textSecondary} strokeWidth={2.2} />
@@ -207,7 +215,7 @@ export function MobilePrComposeForm({
style={[styles.branchToken, baseConflict && styles.branchTokenError]}
numberOfLines={1}
>
{base || t('m.RIaVSo8')}
{base || t('task.base')}
</Text>
</View>
) : null}
@@ -217,32 +225,36 @@ export function MobilePrComposeForm({
style={styles.titleInput}
value={title}
onChangeText={setTitle}
placeholder={t('m.JnixruY')}
placeholder={t('mobilePrComposeForm.title')}
placeholderTextColor={colors.textMuted}
editable={!fieldsLocked}
accessibilityLabel={t('m.CYW43ss', { value0: copy.titleLabel })}
accessibilityLabel={t('mobilePrComposeForm.reviewTypeTitle', {
reviewType: copy.titleLabel
})}
/>
<TextInput
style={styles.bodyInput}
value={body}
onChangeText={setBody}
placeholder={t('m.-uhp5Eo')}
placeholder={t('mobilePrComposeForm.description')}
placeholderTextColor={colors.textMuted}
multiline
editable={!fieldsLocked}
accessibilityLabel={t('m.-QF2aQc', { value0: copy.titleLabel })}
accessibilityLabel={t('mobilePrComposeForm.reviewTypeDescription', {
reviewType: copy.titleLabel
})}
/>
</View>
{generating ? (
<View style={styles.notice}>
<Sparkles size={13} color={colors.textSecondary} strokeWidth={2.1} />
<Text style={styles.noticeText}>{t('m.UsM_A3E')}</Text>
<Text style={styles.noticeText}>{t('mobilePrComposeForm.generatingTitle')}</Text>
</View>
) : null}
<View style={styles.baseRow}>
<Text style={styles.baseLabel}>{t('m.oPjbjuk')}</Text>
<Text style={styles.baseLabel}>{t('mobilePrComposeForm.base')}</Text>
<View style={styles.baseControl}>
<MobilePrBasePicker
client={client}
@@ -255,7 +267,7 @@ export function MobilePrComposeForm({
</View>
<View style={styles.draftRow}>
<Text style={styles.draftText}>{t('m.3Yb2tD4')}</Text>
<Text style={styles.draftText}>{t('mobilePrComposeForm.createDraft')}</Text>
<Switch value={draft} onValueChange={setDraft} disabled={fieldsLocked} />
</View>
{error || submitDisabledReason ? (
@@ -282,11 +294,19 @@ export function MobilePrComposeForm({
<Text style={styles.submitText}>
{pushBeforeCreate
? draft
? t('m.aUTTsqg', { value0: copy.shortLabel })
: t('m.YpkfCr0', { value0: copy.shortLabel })
? t('mobilePrComposeForm.pushCreateDraft', {
reviewType: copy.shortLabel
})
: t('mobilePrComposeForm.pushCreateReview', {
reviewType: copy.shortLabel
})
: draft
? t('m.zouGGt0', { value0: copy.shortLabel })
: t('m.T0kbQLc', { value0: copy.shortLabel })}
? t('mobilePrComposeForm.createDraftReview', {
reviewType: copy.shortLabel
})
: t('mobilePrComposeForm.createReview', {
reviewType: copy.shortLabel
})}
</Text>
</Pressable>
</View>
@@ -41,12 +41,12 @@ export function MobilePrViewPanelBody({
: !isGithubRepo
? ({
kind: 'blocked',
message: t('m._svp6us')
message: t('mobilePrViewPanel.hosted')
} as const)
: branch === null
? ({
kind: 'error',
message: t('m.LINjP1k')
message: t('mobilePrViewPanel.current')
} as const)
: controller.prSidebarState
@@ -66,7 +66,7 @@ export function PRActionsSection({ pr, actions, client, worktreeId, onUnlinked }
setUnlinkError(outcome.error)
}
} catch (err) {
setUnlinkError(err instanceof Error ? err.message : t('m.uLhV2xU'))
setUnlinkError(err instanceof Error ? err.message : t('practionsSection.failed'))
} finally {
setUnlinking(false)
}
@@ -75,22 +75,26 @@ export function PRActionsSection({ pr, actions, client, worktreeId, onUnlinked }
const confirmCopy = (): { title: string; message: string; confirmLabel: string } => {
if (confirm?.kind === 'merge') {
return {
title: t('m._0lAHWg'),
message: t('m.zNuHY-c', { value0: pr.number }),
confirmLabel: t('m.4DTdO1w')
title: t('practionsSection.mergePullRequest'),
message: t('practionsSection.will', { pullRequestNumber: pr.number }),
confirmLabel: t('practionsSection.merge')
}
}
if (confirm?.kind === 'state' && confirm.state === 'closed') {
return {
title: t('m.02Qj2V4'),
message: t('m.b_7nPgo', { value0: pr.number }),
confirmLabel: t('m.gL1buuY')
title: t('practionsSection.closePullRequest'),
message: t('practionsSection.pullRequestNumberWillClosed', {
pullRequestNumber: pr.number
}),
confirmLabel: t('task.close')
}
}
return {
title: t('m.oCXRjL8'),
message: t('m.3XhqaKU', { value0: pr.number }),
confirmLabel: t('m.zRLzboM')
title: t('practionsSection.reopenPullRequest'),
message: t('practionsSection.pullRequestNumberWillReopened', {
pullRequestNumber: pr.number
}),
confirmLabel: t('task.reopen')
}
}
@@ -124,7 +128,7 @@ export function PRActionsSection({ pr, actions, client, worktreeId, onUnlinked }
}}
disabled={mergeBusy}
accessibilityRole="button"
accessibilityLabel={t('m.8xiSsiA')}
accessibilityLabel={t('practionsSection.mergePullRequestAccessibility')}
>
{mergeBusy ? (
<ActivityIndicator color={colors.onMergeGreen} />
@@ -132,14 +136,14 @@ export function PRActionsSection({ pr, actions, client, worktreeId, onUnlinked }
<GitMerge size={16} color={colors.onMergeGreen} strokeWidth={2.2} />
)}
<Text style={[styles.actionButtonText, styles.actionButtonTextMerge]}>
{t('m.8xiSsiA')}
{t('practionsSection.mergePullRequestAccessibility')}
</Text>
</Pressable>
) : null}
{showAutoMerge ? (
<View style={styles.toggleRow}>
<Text style={styles.toggleLabel}>{t('m.0J91CIk')}</Text>
<Text style={styles.toggleLabel}>{t('practionsSection.auto')}</Text>
<Pressable
style={[styles.togglePill, autoMerge && styles.togglePillOn]}
onPress={() => {
@@ -149,13 +153,13 @@ export function PRActionsSection({ pr, actions, client, worktreeId, onUnlinked }
disabled={autoMergeBusy}
accessibilityRole="switch"
accessibilityState={{ checked: autoMerge }}
accessibilityLabel={t('m.X0q7qvA')}
accessibilityLabel={t('practionsSection.toggle')}
>
{autoMergeBusy ? (
<ActivityIndicator color={colors.textSecondary} />
) : (
<Text style={[styles.togglePillText, autoMerge && styles.togglePillTextOn]}>
{autoMerge ? t('m.oSg4C5A') : t('m.Al6-RQA')}
{autoMerge ? t('practionsSection.on') : t('practionsSection.off')}
</Text>
)}
</Pressable>
@@ -177,7 +181,11 @@ export function PRActionsSection({ pr, actions, client, worktreeId, onUnlinked }
}}
disabled={stateBusy}
accessibilityRole="button"
accessibilityLabel={avail.canClose ? t('m.Pz82S8w') : t('m.2xdqaDc')}
accessibilityLabel={
avail.canClose
? t('practionsSection.closePullRequestAccessibility')
: t('practionsSection.reopenPullRequestAccessibility')
}
>
{stateBusy ? <ActivityIndicator color={colors.textSecondary} /> : null}
<Text
@@ -186,7 +194,7 @@ export function PRActionsSection({ pr, actions, client, worktreeId, onUnlinked }
avail.canClose && styles.actionButtonDestructiveText
]}
>
{avail.canClose ? t('m.gL1buuY') : t('m.zRLzboM')}
{avail.canClose ? t('task.close') : t('task.reopen')}
</Text>
</Pressable>
) : null}
@@ -200,14 +208,14 @@ export function PRActionsSection({ pr, actions, client, worktreeId, onUnlinked }
onPress={() => void unlink()}
disabled={unlinkBusy}
accessibilityRole="button"
accessibilityLabel={t('m.ZqSwRWA')}
accessibilityLabel={t('practionsSection.unlinkPull')}
>
{unlinking ? (
<ActivityIndicator color={colors.textSecondary} />
) : (
<Link2Off size={16} color={colors.textSecondary} strokeWidth={2.2} />
)}
<Text style={styles.actionButtonText}>{t('m.nO4i6C8')}</Text>
<Text style={styles.actionButtonText}>{t('practionsSection.unlink')}</Text>
</Pressable>
) : null}
</View>
@@ -34,7 +34,7 @@ export function PRCheckDetailView({ entry }: { entry: DetailEntry | undefined })
if (!entry.details) {
return (
<View style={styles.checkDetailArea}>
<Text style={styles.checkDetailText}>{t('m.xJbH0vA')}</Text>
<Text style={styles.checkDetailText}>{t('prcheckDetail.no')}</Text>
</View>
)
}
@@ -48,7 +48,7 @@ export function PRCheckDetailView({ entry }: { entry: DetailEntry | undefined })
return (
<View style={styles.checkDetailArea}>
{isEmpty ? (
<Text style={styles.checkDetailText}>{t('m.xJbH0vA')}</Text>
<Text style={styles.checkDetailText}>{t('prcheckDetail.no')}</Text>
) : (
<>
{content.summaryLines.map((line, index) => (
@@ -58,7 +58,7 @@ export function PRCheckDetailView({ entry }: { entry: DetailEntry | undefined })
))}
{content.annotations.length > 0 ? (
<View style={styles.checkDetailGroup}>
<Text style={styles.checkDetailGroupLabel}>{t('m.x1QqPYg')}</Text>
<Text style={styles.checkDetailGroupLabel}>{t('prcheckDetail.annotations')}</Text>
{content.annotations.map((annotation, index) => (
<View key={index}>
<Text style={styles.checkDetailLocator} numberOfLines={1}>
@@ -72,7 +72,7 @@ export function PRCheckDetailView({ entry }: { entry: DetailEntry | undefined })
</View>
))}
{content.annotationsTruncated ? (
<Text style={styles.checkDetailText}>{t('m.HW_qZBo')}</Text>
<Text style={styles.checkDetailText}>{t('prcheckDetail.showingFirst20')}</Text>
) : null}
</View>
) : null}
@@ -83,7 +83,7 @@ export function PRCheckDetailView({ entry }: { entry: DetailEntry | undefined })
<JobRow key={index} job={job} />
))}
{content.jobsTruncated ? (
<Text style={styles.checkDetailText}>{t('m.q6iGz3o')}</Text>
<Text style={styles.checkDetailText}>{t('prcheckDetail.showingFirst100')}</Text>
) : null}
</View>
) : null}
@@ -71,7 +71,7 @@ export function PRChecksSection({ checks, client, worktreeId, prRepo, actions, t
// spinning forever — fall back to an error detail.
entry = {
status: 'error',
message: err instanceof Error ? err.message : t('m.hIvuhiQ')
message: err instanceof Error ? err.message : t('prchecksSection.failed')
}
}
setDetailCache((prev) => ({ ...prev, [key]: entry }))
@@ -134,7 +134,7 @@ export function PRChecksSection({ checks, client, worktreeId, prRepo, actions, t
return (
<PRSection
title={t('m.hyZyn8M')}
title={t('prchecksSection.checks')}
trailing={
<>
<Text
@@ -152,7 +152,7 @@ export function PRChecksSection({ checks, client, worktreeId, prRepo, actions, t
onPress={() => actions.rerunFailingChecks()}
disabled={rerunBusy}
accessibilityRole="button"
accessibilityLabel={t('m.TkRK-a4')}
accessibilityLabel={t('prchecksSection.rerun')}
>
{rerunBusy ? (
<ActivityIndicator color={colors.textSecondary} />
@@ -170,12 +170,17 @@ export function PRChecksSection({ checks, client, worktreeId, prRepo, actions, t
<View style={triageStyles.triageStrip}>
<View style={triageStyles.triageStripText}>
<Text style={triageStyles.triageStripTitle} numberOfLines={1}>
{t(summary.failed === 1 ? 'm.lXLSjWw' : 'm.N7XOlnE', {
value0: summary.failed
})}
{t(
summary.failed === 1
? 'prchecksSection.failingCheckCountFailingCheck'
: 'prchecksSection.failingCheckCountFailingChecks',
{
failingCheckCount: summary.failed
}
)}
</Text>
<Text style={triageStyles.triageStripSubtitle} numberOfLines={1}>
{t('m.t67jzCk')}
{t('prchecksSection.inspect')}
</Text>
</View>
<Pressable
@@ -183,14 +188,14 @@ export function PRChecksSection({ checks, client, worktreeId, prRepo, actions, t
onPress={triage.fixChecks}
disabled={triage.isBusy}
accessibilityRole="button"
accessibilityLabel={t('m.O95nvcA')}
accessibilityLabel={t('prchecksSection.fixFailing')}
>
{triage.isBusy ? (
<ActivityIndicator color={colors.textSecondary} />
) : (
<Sparkles size={13} color={colors.textSecondary} strokeWidth={2.2} />
)}
<Text style={triageStyles.triageStripButtonText}>{t('m.QiC7wz8')}</Text>
<Text style={triageStyles.triageStripButtonText}>{t('prchecksSection.fix')}</Text>
</Pressable>
</View>
) : null}
@@ -207,7 +212,9 @@ export function PRChecksSection({ checks, client, worktreeId, prRepo, actions, t
style={styles.row}
onPress={() => toggle(check)}
accessibilityRole="button"
accessibilityLabel={t('m.wKKGPQ8', { value0: check.name })}
accessibilityLabel={t('prchecksSection.check', {
checkName: check.name
})}
>
<Chevron size={14} color={colors.textSecondary} strokeWidth={2.2} />
<View style={[styles.statusDot, { backgroundColor: statusColor(token) }]} />
@@ -227,7 +234,9 @@ export function PRChecksSection({ checks, client, worktreeId, prRepo, actions, t
onPress={() => void Linking.openURL(url).catch(() => {})}
hitSlop={6}
accessibilityRole="button"
accessibilityLabel={t('m.WPX7hnI', { value0: check.name })}
accessibilityLabel={t('prchecksSection.open', {
checkName: check.name
})}
>
<ExternalLink size={13} color={colors.textSecondary} strokeWidth={2.2} />
</Pressable>
@@ -131,7 +131,7 @@ export const PRCommentCard = memo(function PRCommentCard({
) : null}
{comment.isResolved ? (
<View style={styles.resolvedChip}>
<Text style={styles.resolvedChipText}>{t('m.C_gXfCo')}</Text>
<Text style={styles.resolvedChipText}>{t('prcommentCard.resolved')}</Text>
</View>
) : null}
{comment.url ? (
@@ -149,8 +149,8 @@ export const PRCommentCard = memo(function PRCommentCard({
{editOpen && actions ? (
<View style={styles.composer}>
<PRCommentComposer
placeholder={t('m.Bnwan2Q')}
submitLabel={t('m.pZ3OPxE')}
placeholder={t('prcommentCard.editComment')}
submitLabel={t('prcommentCard.save')}
submitting={editBusy}
initialBody={comment.body}
onSubmit={submitEdit}
@@ -172,10 +172,10 @@ export const PRCommentCard = memo(function PRCommentCard({
disabled={replyBusy}
hitSlop={6}
accessibilityRole="button"
accessibilityLabel={t('m.s3o5DeE')}
accessibilityLabel={t('prcommentCard.replyComment')}
>
<CornerDownRight size={13} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.actionButtonText}>{t('m.Ragn9hY')}</Text>
<Text style={styles.actionButtonText}>{t('prcommentCard.reply')}</Text>
</Pressable>
{canMutate ? (
<Pressable
@@ -188,10 +188,10 @@ export const PRCommentCard = memo(function PRCommentCard({
disabled={editBusy}
hitSlop={6}
accessibilityRole="button"
accessibilityLabel={t('m.Fs36PXM')}
accessibilityLabel={t('prcommentCard.editCommentAccessibility')}
>
<Pencil size={13} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.actionButtonText}>{t('m.wS1VT0I')}</Text>
<Text style={styles.actionButtonText}>{t('prcommentCard.edit')}</Text>
</Pressable>
) : null}
{canMutate ? (
@@ -201,10 +201,12 @@ export const PRCommentCard = memo(function PRCommentCard({
disabled={deleteBusy}
hitSlop={6}
accessibilityRole="button"
accessibilityLabel={t('m.u9hUgiY')}
accessibilityLabel={t('prcommentCard.deleteComment')}
>
<Trash2 size={13} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.actionButtonText}>{deleteBusy ? '…' : t('m.4idOoK8')}</Text>
<Text style={styles.actionButtonText}>
{deleteBusy ? '…' : t('prcommentCard.delete')}
</Text>
</Pressable>
) : null}
{canResolve ? (
@@ -214,7 +216,11 @@ export const PRCommentCard = memo(function PRCommentCard({
disabled={resolveBusy}
hitSlop={6}
accessibilityRole="button"
accessibilityLabel={comment.isResolved ? t('m.mkEkYMY') : t('m.2PITe20')}
accessibilityLabel={
comment.isResolved
? t('prcommentCard.unresolveThread')
: t('prcommentCard.resolveThread')
}
>
{comment.isResolved ? (
<Undo2 size={13} color={colors.textSecondary} strokeWidth={2.2} />
@@ -222,7 +228,11 @@ export const PRCommentCard = memo(function PRCommentCard({
<Check size={13} color={colors.textSecondary} strokeWidth={2.2} />
)}
<Text style={styles.actionButtonText}>
{resolveBusy ? '…' : comment.isResolved ? t('m.A6XXe-4') : t('m.7oybC2M')}
{resolveBusy
? '…'
: comment.isResolved
? t('prcommentCard.unresolve')
: t('prcommentCard.resolve')}
</Text>
</Pressable>
) : null}
@@ -231,8 +241,8 @@ export const PRCommentCard = memo(function PRCommentCard({
{replyOpen && !editOpen && actions ? (
<View style={styles.composer}>
<PRCommentComposer
placeholder={t('m.p4p6sek')}
submitLabel={t('m.Ragn9hY')}
placeholder={t('prcommentCard.write')}
submitLabel={t('prcommentCard.reply')}
submitting={replyBusy}
onSubmit={submitReply}
onCancel={() => setReplyOpen(false)}
@@ -243,9 +253,9 @@ export const PRCommentCard = memo(function PRCommentCard({
{actions ? (
<ConfirmModal
visible={confirmDelete}
title={t('m.xGeFr98')}
message={t('m.W8A4HvA')}
confirmLabel={t('m.4idOoK8')}
title={t('prcommentCard.deleteCommentTitle')}
message={t('prcommentCard.permanently')}
confirmLabel={t('prcommentCard.delete')}
destructive
onConfirm={() => void actions.deleteComment(comment.id)}
onCancel={() => setConfirmDelete(false)}
@@ -68,9 +68,9 @@ export function PRCommentComposer({
onPress={onCancel}
disabled={submitting}
accessibilityRole="button"
accessibilityLabel={t('m.EXMnl_4')}
accessibilityLabel={t('prcommentComposer.cancel')}
>
<Text style={styles.cancelText}>{t('m.EXMnl_4')}</Text>
<Text style={styles.cancelText}>{t('prcommentComposer.cancel')}</Text>
</Pressable>
) : null}
<Pressable
@@ -119,20 +119,20 @@ export function PRCommentsSection({
return (
<>
<PRSection title={t('m.6VkqcQk')}>
<PRSection title={t('prcommentsSection.description')}>
{loadingDetails ? (
<ActivityIndicator color={colors.textSecondary} />
) : detailsFailed ? (
<Text style={styles.noDescription}>{t('m.hO2eecI')}</Text>
<Text style={styles.noDescription}>{t('prcommentsSection.couldNotLoadDescription')}</Text>
) : body.trim() ? (
<CommentMarkdown content={body} variant="document" />
) : (
<Text style={styles.noDescription}>{t('m.vdfQtbs')}</Text>
<Text style={styles.noDescription}>{t('prcommentsSection.noDescription')}</Text>
)}
</PRSection>
<PRSection
title={t('m._l0TaO0')}
title={t('prcommentsSection.comments')}
trailing={
comments.length > 0 ? (
<View style={styles.countChip}>
@@ -144,11 +144,11 @@ export function PRCommentsSection({
{loadingDetails ? (
<ActivityIndicator color={colors.textSecondary} />
) : detailsFailed ? (
<Text style={styles.empty}>{t('m.FVdefgc')}</Text>
<Text style={styles.empty}>{t('prcommentsSection.couldNotLoadComments')}</Text>
) : (
<View style={styles.list}>
{comments.length === 0 ? (
<Text style={styles.empty}>{t('m.xX8eZhc')}</Text>
<Text style={styles.empty}>{t('prcommentsSection.noComments')}</Text>
) : (
<>
{isPr ? (
@@ -197,11 +197,13 @@ export function PRCommentsSection({
>
<Text style={styles.showMoreText}>
{remaining > COMMENT_PAGE
? t('m.vV46Tns', {
value0: COMMENT_PAGE,
value1: remaining
? t('prcommentsSection.showVisible', {
visibleCommentCount: COMMENT_PAGE,
remaining: remaining
})
: t('m.cHoCGwY', { value0: remaining })}
: t('prcommentsSection.showRemaining', {
remainingCommentCount: remaining
})}
</Text>
</Pressable>
) : null}
@@ -213,8 +215,8 @@ export function PRCommentsSection({
{canComment && actions ? (
<View style={styles.rootComposer}>
<PRCommentComposer
placeholder={t('m.2Mrfqx0')}
submitLabel={t('m.eVLkMyE')}
placeholder={t('prcommentsSection.add')}
submitLabel={t('prcommentsSection.comment')}
submitting={actions.isRootBusy}
onSubmit={actions.addRootComment}
/>
@@ -263,13 +265,23 @@ function CommentGroupView({
<Chevron size={14} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.resolvedHeaderText} numberOfLines={1}>
{count > 1
? t(group.kind === 'thread' ? 'm.lXnGpvs' : 'm.tQONwOQ', {
value0: root.author,
value1: count
})
: t(group.kind === 'thread' ? 'm.dG4O1e4' : 'm.njd-pFg', {
value0: root.author
})}
? t(
group.kind === 'thread'
? 'prcommentsSection.resolvedThreadAuthorReply'
: 'prcommentsSection.resolvedCommentAuthorReply',
{
author: root.author,
replyCount: count
}
)
: t(
group.kind === 'thread'
? 'prcommentsSection.resolvedThreadAuthor'
: 'prcommentsSection.resolvedCommentAuthor',
{
author: root.author
}
)}
</Text>
</Pressable>
{expanded ? <View style={shared.sectionBody}>{cards}</View> : null}
@@ -72,24 +72,29 @@ export function PRConflictingFilesSection({ pr, isRefreshing = false, triage }:
}
return (
<PRSection title={t('m.DnSMHG0')}>
<PRSection title={t('prconflictingFilesSection.conflicts')}>
{conflict.commitsBehind !== null && conflict.baseCommit !== null ? (
<Text style={styles.meta}>
{t(conflict.commitsBehind === 1 ? 'm.ngn4NjA' : 'm.93sCYQo', {
value0: conflict.commitsBehind
})}{' '}
{t(
conflict.commitsBehind === 1
? 'prconflictingFilesSection.commitsBehindCommit'
: 'prconflictingFilesSection.commitsBehindCommits',
{
commitsBehind: conflict.commitsBehind
}
)}{' '}
<Text style={styles.metaMono}>{conflict.baseCommit}</Text>)
</Text>
) : null}
{conflict.fileDetailsUnavailable ? (
<View>
<Text style={styles.noticeTitle}>{t('m.7Qv5eYo')}</Text>
<Text style={styles.noticeTitle}>{t('prconflictingFilesSection.branch')}</Text>
<Text style={styles.noticeBody}>{noticeBody}</Text>
{conflict.mergeabilityRefreshCommands ? (
<View style={styles.commandBox}>
<View style={styles.commandHeader}>
<Text style={styles.commandLabel}>{t('m.39Nl8mw')}</Text>
<Text style={styles.commandLabel}>{t('prconflictingFilesSection.run')}</Text>
<Pressable
style={({ pressed }) => [
styles.copyCommandButton,
@@ -97,7 +102,7 @@ export function PRConflictingFilesSection({ pr, isRefreshing = false, triage }:
]}
onPress={() => void copyRefreshCommands()}
accessibilityRole="button"
accessibilityLabel={t('m.6TXtA4Q')}
accessibilityLabel={t('prconflictingFilesSection.copyMergeability')}
>
{commandsCopied ? (
<Check size={13} color={colors.textPrimary} strokeWidth={2.2} />
@@ -105,7 +110,9 @@ export function PRConflictingFilesSection({ pr, isRefreshing = false, triage }:
<Copy size={13} color={colors.textPrimary} strokeWidth={2.2} />
)}
<Text style={styles.copyCommandText}>
{commandsCopied ? t('m.7E56l1g') : t('m.KuLUlYc')}
{commandsCopied
? t('prconflictingFilesSection.copied')
: t('prconflictingFilesSection.copyCommands')}
</Text>
</Pressable>
</View>
@@ -119,7 +126,7 @@ export function PRConflictingFilesSection({ pr, isRefreshing = false, triage }:
<View>
<View style={styles.filesHeader}>
<FileWarning size={14} color={colors.textSecondary} strokeWidth={2} />
<Text style={styles.filesHeaderText}>{t('m.w2WcFns')}</Text>
<Text style={styles.filesHeaderText}>{t('prconflictingFilesSection.conflicting')}</Text>
</View>
<ScrollView
style={styles.fileList}
@@ -148,14 +155,16 @@ export function PRConflictingFilesSection({ pr, isRefreshing = false, triage }:
onPress={triage.resolveConflicts}
disabled={triage.isBusy}
accessibilityRole="button"
accessibilityLabel={t('m.zrerXGc')}
accessibilityLabel={t('prconflictingFilesSection.resolve')}
>
{triage.isBusy ? (
<ActivityIndicator color={colors.textSecondary} />
) : (
<Sparkles size={14} color={colors.textSecondary} strokeWidth={2.2} />
)}
<Text style={triageStyles.triageButtonText}>{t('m.zrerXGc')}</Text>
<Text style={triageStyles.triageButtonText}>
{t('prconflictingFilesSection.resolve')}
</Text>
</Pressable>
{triage.error ? <Text style={triageStyles.triageError}>{triage.error}</Text> : null}
</View>
@@ -62,23 +62,23 @@ export function PRReviewersSection({ details, actions, client, worktreeId }: Pro
style={styles.iconButton}
onPress={() => setPickerOpen(true)}
accessibilityRole="button"
accessibilityLabel={t('m.E5rQqRQ')}
accessibilityLabel={t('prreviewersSection.add')}
>
<UserPlus size={16} color={colors.textSecondary} strokeWidth={2.2} />
</Pressable>
)
return (
<PRSection title={t('m.fX2uAEU')} trailing={addButton}>
<PRSection title={t('prreviewersSection.reviewers')} trailing={addButton}>
{loadingDetails ? (
<View style={styles.reviewersStatus}>
<ActivityIndicator color={colors.textSecondary} />
<Text style={styles.emptyText}>{t('m.Ryr3DCQ')}</Text>
<Text style={styles.emptyText}>{t('prreviewersSection.loading')}</Text>
</View>
) : detailsFailed ? (
<Text style={styles.emptyText}>{t('m.T5Uc4Ao')}</Text>
<Text style={styles.emptyText}>{t('prreviewersSection.could')}</Text>
) : rows.length === 0 ? (
<Text style={styles.emptyText}>{t('m.jBToX-I')}</Text>
<Text style={styles.emptyText}>{t('prreviewersSection.no')}</Text>
) : (
rows.map((row) => {
const busy = actions.isBusy({ kind: 'reviewer', login: row.login })
@@ -99,7 +99,7 @@ export function PRReviewersSection({ details, actions, client, worktreeId }: Pro
onPress={() => actions.removeReviewer(row.login)}
disabled={busy}
accessibilityRole="button"
accessibilityLabel={t('m.8Est-xI', { value0: row.login })}
accessibilityLabel={t('prreviewersSection.remove', { login: row.login })}
>
{busy ? (
<ActivityIndicator color={colors.textSecondary} />
@@ -50,7 +50,9 @@ export function PRSidebarHeader({
onPress={openPr}
disabled={!openPr}
accessibilityRole="link"
accessibilityLabel={t('m.nbfmLgg', { value0: pr.number })}
accessibilityLabel={t('prsidebarHeader.open', {
pullRequestNumber: pr.number
})}
style={({ pressed }) => [
styles.badge,
{ borderColor: badgeColor },
@@ -63,7 +65,9 @@ export function PRSidebarHeader({
style={styles.prMetaStrong}
onPress={openPr}
accessibilityRole="link"
accessibilityLabel={t('m.nbfmLgg', { value0: pr.number })}
accessibilityLabel={t('prsidebarHeader.open', {
pullRequestNumber: pr.number
})}
>
#{pr.number}
</Text>
@@ -74,7 +78,9 @@ export function PRSidebarHeader({
onPress={openPr}
hitSlop={8}
accessibilityRole="link"
accessibilityLabel={t('m.nbfmLgg', { value0: pr.number })}
accessibilityLabel={t('prsidebarHeader.open', {
pullRequestNumber: pr.number
})}
style={({ pressed }) => [styles.iconButton, pressed && { opacity: 0.6 }]}
>
<ExternalLink size={16} color={colors.textSecondary} strokeWidth={2.2} />
@@ -154,21 +160,21 @@ function PRTitle({
onPress={cancel}
disabled={titleAction.saving}
accessibilityRole="button"
accessibilityLabel={t('m.D-7jM08')}
accessibilityLabel={t('prsidebarHeader.cancelEditing')}
>
<Text style={composerStyles.cancelText}>{t('m.UgBLL4w')}</Text>
<Text style={composerStyles.cancelText}>{t('prsidebarHeader.cancel')}</Text>
</Pressable>
<Pressable
style={({ pressed }) => [composerStyles.submit, pressed && composerStyles.pressed]}
onPress={() => void save()}
disabled={titleAction.saving}
accessibilityRole="button"
accessibilityLabel={t('m.xOwofss')}
accessibilityLabel={t('prsidebarHeader.saveTitle')}
>
{titleAction.saving ? (
<ActivityIndicator size="small" color={colors.bgBase} />
) : (
<Text style={composerStyles.submitText}>{t('m.oBmCrB8')}</Text>
<Text style={composerStyles.submitText}>{t('prsidebarHeader.save')}</Text>
)}
</Pressable>
</View>
@@ -182,7 +188,7 @@ function PRTitle({
onPress={editable ? startEdit : undefined}
disabled={!editable}
accessibilityRole={editable ? 'button' : undefined}
accessibilityLabel={editable ? t('m.E2VHius') : undefined}
accessibilityLabel={editable ? t('prsidebarHeader.edit') : undefined}
>
<Text style={styles.prTitle}>{title}</Text>
{editable ? (
@@ -98,7 +98,7 @@ export function PrSidebarCreateEmptyState({
setLoading(true)
try {
if (!gitBranch) {
setCreateWarning(t('m.EMFMGnw'))
setCreateWarning(t('prSidebarCreateEmptyState.check'))
return
}
// Why: mobile skips the local compose step here and runs the hosted create
@@ -132,7 +132,7 @@ export function PrSidebarCreateEmptyState({
openMobilePrUrl(outcome.url)
onCreated()
} catch (err) {
setCreateWarning(err instanceof Error ? err.message : t('m.tca0V2E'))
setCreateWarning(err instanceof Error ? err.message : t('prSidebarCreateEmptyState.failed'))
} finally {
setLoading(false)
}
@@ -161,14 +161,14 @@ export function PrSidebarCreateEmptyState({
<View style={styles.header}>
<View style={styles.headerTitle}>
<GitPullRequestArrow size={14} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.headerLabel}>{t('m.TSDVcKU')}</Text>
<Text style={styles.headerLabel}>{t('prSidebarCreateEmptyState.pull')}</Text>
</View>
<View style={styles.headerActions}>
<Pressable
style={({ pressed }) => [styles.iconButton, pressed && styles.iconButtonPressed]}
onPress={refreshPrState}
accessibilityRole="button"
accessibilityLabel={t('m.YZFe3Qg')}
accessibilityLabel={t('prSidebarCreateEmptyState.refreshPull')}
hitSlop={6}
>
<RefreshCw size={16} color={colors.textSecondary} strokeWidth={2.2} />
@@ -178,27 +178,31 @@ export function PrSidebarCreateEmptyState({
onPress={() => void openComposer()}
disabled={!canCreate || loading}
accessibilityRole="button"
accessibilityLabel={t('m.Y_NnJ9M')}
accessibilityLabel={t('prSidebarCreateEmptyState.createPull')}
>
{loading ? (
<ActivityIndicator color={colors.bgBase} />
) : (
<GitPullRequestArrow size={14} color={colors.bgBase} strokeWidth={2.2} />
)}
<Text style={styles.createButtonText}>{t('m.5kE-iVM')}</Text>
<Text style={styles.createButtonText}>{t('prSidebarCreateEmptyState.createPr')}</Text>
</Pressable>
</View>
</View>
<View style={styles.body}>
<Text style={styles.bodyTitle}>
{orphanLinkedPR ? t('m.I3xSuPA', { value0: orphanLinkedPR }) : t('m.G2BaE-Y')}
{orphanLinkedPR
? t('prSidebarCreateEmptyState.linked', {
pullRequestNumber: orphanLinkedPR
})
: t('prSidebarCreateEmptyState.no')}
</Text>
<Text style={styles.bodyText}>
{orphanLinkedPR
? t('m.maf-eds')
? t('prSidebarCreateEmptyState.refreshCheck')
: gitBranch
? t('m.CmpszXU', { value0: gitBranch })
: t('m.SRr0o4U')}
? t('prSidebarCreateEmptyState.git', { gitBranch: gitBranch })
: t('prSidebarCreateEmptyState.current')}
</Text>
{commitFailureRecovery ? (
<MobileCommitFailurePanel
@@ -217,7 +221,7 @@ export function PrSidebarCreateEmptyState({
onPress={() => setMode('link')}
disabled={!client}
accessibilityRole="button"
accessibilityLabel={t('m._mT5R7M')}
accessibilityLabel={t('prSidebarCreateEmptyState.linkExistingPull')}
accessibilityState={{ disabled: !client }}
hitSlop={6}
>
@@ -226,7 +230,7 @@ export function PrSidebarCreateEmptyState({
color={client ? colors.textSecondary : colors.textMuted}
strokeWidth={2.2}
/>
<Text style={styles.linkButtonText}>{t('m.eNX9Nmo')}</Text>
<Text style={styles.linkButtonText}>{t('prSidebarCreateEmptyState.linkExistingPr')}</Text>
</Pressable>
</View>
</View>
@@ -62,7 +62,10 @@ export function ReviewerPickerDrawer({
})
.catch(() => {
if (!cancelled) {
setLoad({ status: 'error', message: t('m.dVs-M9I') })
setLoad({
status: 'error',
message: t('reviewerPickerDrawer.failed')
})
}
})
return () => {
@@ -92,12 +95,12 @@ export function ReviewerPickerDrawer({
return (
<BottomDrawer visible={visible} onClose={onClose} dragContentToDismiss={false}>
<Text style={styles.pickerTitle}>{t('m.K34xo24')}</Text>
<Text style={styles.pickerTitle}>{t('reviewerPickerDrawer.reviewers')}</Text>
<TextInput
style={styles.pickerSearch}
value={query}
onChangeText={setQuery}
placeholder={t('m.Ne-d-yA')}
placeholder={t('reviewerPickerDrawer.search')}
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
@@ -112,7 +115,7 @@ export function ReviewerPickerDrawer({
</View>
) : ordered.length === 0 ? (
<View style={styles.pickerStateArea}>
<Text style={styles.emptyText}>{t('m.6OYLM7w')}</Text>
<Text style={styles.emptyText}>{t('reviewerPickerDrawer.no')}</Text>
</View>
) : (
<View style={styles.pickerList}>
@@ -127,7 +130,9 @@ export function ReviewerPickerDrawer({
accessibilityState={{ selected: requested }}
accessibilityLabel={
requested
? t('m.8Est-xI', { value0: item.login })
? t('prreviewersSection.remove', {
login: item.login
})
: t('review.requestReviewer', { login: item.login })
}
>
@@ -68,9 +68,13 @@ function parseSegment(text: string): MarkdownBlock[] {
}
if (m[1].toLowerCase() === 'details') {
const sm = SUMMARY.exec(m[2])
const summary = sm ? stripHtmlTags(sm[1]).trim() : t('m.B0fOGkI')
const summary = sm ? stripHtmlTags(sm[1]).trim() : t('markdownBlocks.details')
const body = m[2].replace(SUMMARY, '')
blocks.push({ kind: 'details', summary: summary || t('m.B0fOGkI'), body: parseSegment(body) })
blocks.push({
kind: 'details',
summary: summary || t('markdownBlocks.details'),
body: parseSegment(body)
})
} else {
blocks.push({ kind: 'quote', text: stripHtmlTags(m[2]).trim() })
}
@@ -91,7 +91,8 @@ export function presentCheckDetail(details: PRCheckRunDetails): CheckDetailConte
summaryLines,
annotations: details.annotations.slice(0, MAX_ANNOTATIONS).map(mapAnnotation),
annotationsTruncated: details.annotations.length > MAX_ANNOTATIONS,
jobsLabel: failedJobs.length > 0 ? t('m.OJSrr6g') : t('m.g0JLO88'),
jobsLabel:
failedJobs.length > 0 ? t('prCheckDetailContent.failed') : t('prCheckDetailContent.jobs'),
jobs: visibleJobs.slice(0, MAX_JOBS).map(mapJob),
jobsTruncated: details.jobs.length > MAX_JOBS
}
@@ -74,7 +74,14 @@ const OUTCOME_BY_STATE: Record<ProviderCheckSummary['state'], CheckOutcome | 'no
export function summarizePRChecks(checks: readonly PRCheckDetail[]): PRChecksSummary {
if (checks.length === 0) {
return { total: 0, passed: 0, pending: 0, failed: 0, outcome: 'none', label: t('m.hCKjn9A') }
return {
total: 0,
passed: 0,
pending: 0,
failed: 0,
outcome: 'none',
label: t('prChecksPresentation.no')
}
}
// Counts and the worst-case rollup come from the shared summarizer; only the label wording is mobile's.
const { total, passed, pending, failed, neutral, state } = summarizeProviderChecks(checks)
@@ -122,25 +129,27 @@ export function summarizePRChecks(checks: readonly PRCheckDetail[]): PRChecksSum
// outcome is readable without expanding the row. Mirrors getCheckStatusLabel.
export function checkStatusLabel(check: PRCheckDetail): string {
if (check.status !== 'completed') {
return check.status === 'in_progress' ? t('m.9XDJxdU') : t('m.NEg29tM')
return check.status === 'in_progress'
? t('prChecksPresentation.progress')
: t('prChecksPresentation.pending')
}
switch (check.conclusion) {
case 'success':
return t('m.6lXfNVk')
return t('prChecksPresentation.successful')
case 'failure':
return t('m.NO0ZjtQ')
return t('prChecksPresentation.failed')
case 'cancelled':
return t('m.thrHxG4')
return t('prChecksPresentation.cancelled')
case 'timed_out':
return t('m.lyHptRM')
return t('prChecksPresentation.timed')
case 'action_required':
return t('prChecks.status.actionRequired')
case 'neutral':
return t('m.Ba1jOdE')
return t('prChecksPresentation.neutral')
case 'skipped':
return t('m.8I2nZuQ')
return t('prChecksPresentation.skipped')
default:
return t('m.NEg29tM')
return t('prChecksPresentation.pending')
}
}
@@ -188,10 +197,10 @@ export type PRStateBadge = {
}
const PR_STATE_LABELS: Record<PRState, string> = {
open: t('m.sbQ_yBw'),
merged: t('m.AF7gJFU'),
draft: t('m.QYCFjQw'),
closed: t('m.tNw1xEA')
open: t('prChecksPresentation.open'),
merged: t('prChecksPresentation.merged'),
draft: t('prChecksPresentation.draft'),
closed: t('prChecksPresentation.closed')
}
// State-badge color comes from the shared prStateToken so the sidebar badge and
@@ -215,20 +224,41 @@ function reviewStateLabel(state: string | null | undefined): {
} {
switch (state) {
case 'APPROVED':
return { label: t('m.IloG18k'), token: 'statusGreen' }
return {
label: t('prChecksPresentation.approved'),
token: 'statusGreen'
}
case 'CHANGES_REQUESTED':
return { label: t('m.rjj84-Y'), token: 'statusRed' }
return {
label: t('prChecksPresentation.changes'),
token: 'statusRed'
}
case 'COMMENTED':
return { label: t('m.bF0aphw'), token: 'textSecondary' }
return {
label: t('prChecksPresentation.commented'),
token: 'textSecondary'
}
case 'DISMISSED':
return { label: t('m.dmAcep8'), token: 'textSecondary' }
return {
label: t('prChecksPresentation.dismissed'),
token: 'textSecondary'
}
case 'PENDING':
return { label: t('m.NEg29tM'), token: 'statusAmber' }
return {
label: t('prChecksPresentation.pending'),
token: 'statusAmber'
}
case null:
case undefined:
return { label: t('m._IeDTL8'), token: 'textSecondary' }
return {
label: t('prChecksPresentation.reviewed'),
token: 'textSecondary'
}
default:
return { label: t('m._IeDTL8'), token: 'textSecondary' }
return {
label: t('prChecksPresentation.reviewed'),
token: 'textSecondary'
}
}
}
@@ -250,7 +280,7 @@ export function getPRReviewerRows(item: ReviewDisplayItem): ReviewerRow[] {
login,
name: user.name,
avatarUrl: user.avatarUrl,
stateLabel: t('m.RfH2kOg'),
stateLabel: t('prChecksPresentation.requested'),
token: 'statusAmber'
})
}
@@ -14,9 +14,9 @@ export { createBotAuthorOverrideSet }
export type PRCommentAudienceFilter = 'all' | 'human' | 'bot'
export const PR_COMMENT_AUDIENCE_FILTERS: { value: PRCommentAudienceFilter; label: string }[] = [
{ value: 'all', label: t('m.rkpE-Mg') },
{ value: 'human', label: t('m.4fGywsA') },
{ value: 'bot', label: t('m._RfDeuw') }
{ value: 'all', label: t('prCommentAudience.all') },
{ value: 'human', label: t('prCommentAudience.humans') },
{ value: 'bot', label: t('prCommentAudience.bots') }
]
const BOT_LOGIN_SUFFIX = '[bot]'
@@ -100,10 +100,10 @@ export function filterPRCommentsByAudience(
export function getPRCommentAudienceEmptyLabel(filter: PRCommentAudienceFilter): string {
switch (filter) {
case 'bot':
return t('m.BvW_4OE')
return t('prCommentAudience.noBot')
case 'human':
return t('m.znwGBr8')
return t('prCommentAudience.noHuman')
case 'all':
return t('m.AXw1J4E')
return t('prCommentAudience.noComments')
}
}
@@ -9,23 +9,25 @@ export function formatPrCommentRelativeTime(iso: string, nowMs: number): string
}
const delta = nowMs - ts
if (delta < 60_000) {
return t('m.xbjFHYU')
return t('prCommentTime.just')
}
const minutes = Math.floor(delta / 60_000)
if (minutes < 60) {
return t('m.XG7ieU0', { value0: minutes })
return t('prCommentTime.minutes', { minutes: minutes })
}
const hours = Math.floor(minutes / 60)
if (hours < 24) {
return t('m.J9EQWOE', { value0: hours })
return t('prCommentTime.hours', { hours: hours })
}
const days = Math.floor(hours / 24)
if (days < 30) {
return t('m.3u9-Kuw', { value0: days })
return t('prCommentTime.days', { days: days })
}
const months = Math.floor(days / 30)
if (months < 12) {
return t('m.5mTslyc', { value0: months })
return t('prCommentTime.months', { months: months })
}
return t('m.rBMjMUM', { value0: Math.floor(months / 12) })
return t('prCommentTime.years', {
years: Math.floor(months / 12)
})
}
@@ -16,9 +16,12 @@ function describeScope(snapshot: AccountsSnapshot, scope: CodexResetCreditExpect
const account = snapshot.codex.accounts.find((candidate) => candidate.id === scope.accountId)
const identity = account?.email ?? t('accounts.selectedManagedAccount')
if (scope.target.runtime === 'host') {
return t('m.TUCQXxI', { value0: identity })
return t('useCodexResetCreditAction.identityHost', { identity: identity })
}
return t('m.sDDLSuM', { value0: identity, value1: scope.target.wslDistro })
return t('useCodexResetCreditAction.identityWsl', {
identity: identity,
wslDistro: scope.target.wslDistro
})
}
export function useCodexResetCreditAction({
@@ -69,15 +72,27 @@ export function useCodexResetCreditAction({
})
onSnapshot(result.snapshot)
if ('status' in result) {
const cleanupWarning = result.attemptJournalRetained ? t('m.d5V00qM') : ''
Alert.alert(t('m.4DQGAog'), t('m.rii7ZAs', { value0: cleanupWarning }))
const cleanupWarning = result.attemptJournalRetained
? t('useCodexResetCreditAction.phone')
: ''
Alert.alert(
t('useCodexResetCreditAction.reset'),
t('useCodexResetCreditAction.account', {
cleanupWarning: cleanupWarning
})
)
return
}
const copy = getCodexResetCreditOutcomeCopy(result.outcome)
const cleanupWarning = result.attemptJournalRetained ? t('m.rXScUPU') : ''
const cleanupWarning = result.attemptJournalRetained
? t('useCodexResetCreditAction.host')
: ''
Alert.alert(copy.title, `${copy.message}${cleanupWarning}`)
} catch (error) {
Alert.alert(t('m.TKIeXbw'), error instanceof Error ? error.message : String(error))
Alert.alert(
t('useCodexResetCreditAction.could'),
error instanceof Error ? error.message : String(error)
)
} finally {
inFlightRef.current = false
setResetting(false)
@@ -92,10 +107,19 @@ export function useCodexResetCreditAction({
}
const confirmedScope = resetScope
const confirmedLabel = describeScope(snapshot, confirmedScope)
Alert.alert(t('m.EzPa9ek'), t('m.5hDxS58', { value0: confirmedLabel }), [
{ text: t('m._nuDzHY'), style: 'cancel' },
{ text: t('m.VWnR13E'), onPress: () => void consume(confirmedScope) }
])
Alert.alert(
t('useCodexResetCreditAction.useRate'),
t('useCodexResetCreditAction.spends', {
confirmedLabel: confirmedLabel
}),
[
{ text: t('useCodexResetCreditAction.cancel'), style: 'cancel' },
{
text: t('useCodexResetCreditAction.useReset'),
onPress: () => void consume(confirmedScope)
}
]
)
}, [accountMutationBusy, connected, consume, resetScope, resetting, snapshot, supported])
return { supported, resetting, resetScope, scopeLabel, confirmReset }
@@ -14,38 +14,60 @@ export function getTroubleshootCommonIssues(): TroubleshootSection[] {
{
id: 'wifi',
icon: <WifiOff size={16} color={colors.textSecondary} />,
title: t('m.vuZn-_E'),
instructions: [t('m.DFWK1QU'), t('m.h6u3Ajc'), t('m.jEL8NQc')]
title: t('troubleshootCommonIssues.different'),
instructions: [
t('troubleshootCommonIssues.both'),
t('troubleshootCommonIssues.ethernet'),
t('troubleshootCommonIssues.tryReconnecting')
]
},
{
id: 'firewall',
icon: <Shield size={16} color={colors.textSecondary} />,
title: t('m.amY3iCs'),
instructions: [t('m.REilmMs'), t('m.ayekp1A'), t('m.401zg0M'), t('m.rtC8ngI')]
title: t('troubleshootCommonIssues.firewall'),
instructions: [
t('troubleshootCommonIssues.mac'),
t('troubleshootCommonIssues.windows'),
t('troubleshootCommonIssues.linux'),
t('troubleshootCommonIssues.corporate')
]
},
{
id: 'desktop',
icon: <Monitor size={16} color={colors.textSecondary} />,
title: t('m.MED_8yU'),
instructions: [t('m.i4fV_jk'), t('m.WkJ-I-g'), t('m.neP0TKg')]
title: t('troubleshootCommonIssues.desktop'),
instructions: [
t('troubleshootCommonIssues.orca'),
t('troubleshootCommonIssues.tryRestarting'),
t('troubleshootCommonIssues.after')
]
},
{
id: 'timeout',
icon: <Clock size={16} color={colors.textSecondary} />,
title: t('m.AE3mc7A'),
instructions: [t('m.uGWzUrE'), t('m.gvS1F3o'), t('m.S9tNuKw')]
title: t('troubleshootCommonIssues.connection'),
instructions: [
t('troubleshootCommonIssues.checkWi'),
t('troubleshootCommonIssues.go'),
t('troubleshootCommonIssues.restart')
]
},
{
id: 'tailscale',
icon: <Globe size={16} color={colors.textSecondary} />,
title: t('m.ngjn4tY'),
instructions: [t('m.T6EL0oM'), t('m.rmMn9Xc'), t('m.j0KeldQ'), t('m.PxeuBQc')]
title: t('troubleshootCommonIssues.tailscale'),
instructions: [
t('troubleshootCommonIssues.host'),
t('troubleshootCommonIssues.i'),
t('troubleshootCommonIssues.checkDesktop'),
t('troubleshootCommonIssues.update')
]
},
{
id: 'vpn',
icon: <Shield size={16} color={colors.textSecondary} />,
title: t('m.jLhPXXQ'),
instructions: [t('m.n-WXtiA'), t('m.MJ1SglY')]
title: t('troubleshootCommonIssues.other'),
instructions: [t('troubleshootCommonIssues.non'), t('troubleshootCommonIssues.disable')]
}
]
}
@@ -34,9 +34,9 @@ export async function fetchDictationSetup(
const response = await fetchDictationSetupResponse(client)
if (!response.ok) {
if (isLegacyDesktopSpeechSetupError(response.error)) {
throw new Error(t('m.XOHYcMw'))
throw new Error(t('mobileDictationSetup.update'))
}
throw new Error(response.error?.message || t('m.jcVunZ4'))
throw new Error(response.error?.message || t('mobileDictationSetup.failedLoad'))
}
return (response as RpcSuccess).result as MobileSpeechSetup
}
@@ -60,7 +60,7 @@ export async function downloadDictationModel(
): Promise<void> {
const response = await client.sendRequest('speech.models.download', { modelId })
if (!response.ok) {
throw new Error(response.error?.message || t('m.hkwxfd0'))
throw new Error(response.error?.message || t('mobileDictationSetup.failedStart'))
}
}
@@ -70,7 +70,7 @@ export async function deleteDictationModel(
): Promise<MobileSpeechSetup> {
const response = await client.sendRequest('speech.models.delete', { modelId })
if (!response.ok) {
throw new Error(response.error?.message || t('m.9VJGb2s'))
throw new Error(response.error?.message || t('mobileDictationSetup.failedDelete'))
}
return (response as RpcSuccess).result as MobileSpeechSetup
}
@@ -81,7 +81,7 @@ export async function setDictationConfig(
): Promise<MobileSpeechSetup> {
const response = await client.sendRequest('speech.dictation.setup', params)
if (!response.ok) {
throw new Error(response.error?.message || t('m.CdTon5s'))
throw new Error(response.error?.message || t('mobileDictationSetup.failedUpdate'))
}
return (response as RpcSuccess).result as MobileSpeechSetup
}

Some files were not shown because too many files have changed in this diff Show More