mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
fix(mobile): harden localization catalog audit
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+921
-940
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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) => (
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' ? (
|
||||
|
||||
@@ -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]}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }) => [
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user