From fc1e03ffa735e0faeecb3f502db44656c72a71fa Mon Sep 17 00:00:00 2001
From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Date: Sat, 1 Aug 2026 09:42:47 -0700
Subject: [PATCH] fix(mobile): harden localization review gates
---
.../mobile-localization-candidate-rules.mjs | 88 +++---
...bile-localization-candidate-rules.test.mjs | 75 +++++
.../mobile-localization-product-glossary.mjs | 287 ++++++++++++++++++
.../mobile-localization-rendered-variable.mjs | 127 ++++++--
.../mobile-localization-source-bindings.mjs | 11 +-
...bile-localization-translation-bindings.mjs | 185 +++++++----
...localization-translation-bindings.test.mjs | 50 +++
.../verify-mobile-localization-catalog.mjs | 10 +-
...erify-mobile-localization-catalog.test.mjs | 47 +++
mobile/src/i18n/locales/es.json | 84 ++---
mobile/src/i18n/locales/ja.json | 78 ++---
mobile/src/i18n/locales/ko.json | 78 ++---
mobile/src/i18n/locales/zh.json | 72 ++---
.../terminal-accessory-key-catalog.ts | 34 ++-
14 files changed, 938 insertions(+), 288 deletions(-)
create mode 100644 config/scripts/mobile-localization-translation-bindings.test.mjs
diff --git a/config/scripts/mobile-localization-candidate-rules.mjs b/config/scripts/mobile-localization-candidate-rules.mjs
index c6e25f65f1d..08380394585 100644
--- a/config/scripts/mobile-localization-candidate-rules.mjs
+++ b/config/scripts/mobile-localization-candidate-rules.mjs
@@ -8,6 +8,8 @@ import {
isReturnedByRenderedFunction
} from './mobile-localization-rendered-variable.mjs'
+const NOTIFICATION_CHANNEL_NAME_NODES = new WeakMap()
+
const USER_VISIBLE_JSX_ATTRIBUTES = new Set([
'ariaLabel',
'aria-label',
@@ -388,57 +390,80 @@ function isUserVisibleErrorArgument(node, userVisibleErrorSource) {
)
}
-function resolveObjectLiteral(node, bindings, seen = new Set()) {
+function resolveBoundInitializer(node, bindings, seen = new Set()) {
if (
ts.isParenthesizedExpression(node) ||
ts.isAsExpression(node) ||
ts.isSatisfiesExpression(node)
) {
- return resolveObjectLiteral(node.expression, bindings, seen)
- }
- if (ts.isObjectLiteralExpression(node)) {
- return node
+ return resolveBoundInitializer(node.expression, bindings, seen)
}
if (!bindings || !ts.isIdentifier(node)) {
- return undefined
+ return node
}
const binding = bindings.resolveBinding(node)
if (!binding || seen.has(binding) || !ts.isVariableDeclaration(binding) || !binding.initializer) {
- return undefined
+ return node
}
seen.add(binding)
- return resolveObjectLiteral(binding.initializer, bindings, seen)
+ return resolveBoundInitializer(binding.initializer, bindings, seen)
+}
+
+function objectPropertyValues(node, propertyName, bindings, seen = new Set()) {
+ const expression = resolveBoundInitializer(node, bindings, seen)
+ if (!ts.isObjectLiteralExpression(expression)) {
+ return []
+ }
+ return expression.properties.flatMap((property) => {
+ if (ts.isPropertyAssignment(property) && propertyNameText(property.name) === propertyName) {
+ return [property.initializer]
+ }
+ if (ts.isShorthandPropertyAssignment(property) && property.name.text === propertyName) {
+ return [property.name]
+ }
+ return ts.isSpreadAssignment(property)
+ ? objectPropertyValues(property.expression, propertyName, bindings, new Set(seen))
+ : []
+ })
+}
+
+function collectBoundStringNodes(expression, bindings, nodes, seen = new Set()) {
+ const resolved = resolveBoundInitializer(expression, bindings, seen)
+ if (ts.isStringLiteralLike(resolved) || ts.isTemplateExpression(resolved)) {
+ nodes.add(resolved)
+ return
+ }
+ ts.forEachChild(resolved, (child) =>
+ collectBoundStringNodes(child, bindings, nodes, new Set(seen))
+ )
}
function isNotificationChannelName(node, bindings) {
- const property = findAncestor(node, ts.isPropertyAssignment)
- const object = property?.parent
- if (
- !property ||
- propertyNameText(property.name) !== 'name' ||
- !object ||
- !ts.isObjectLiteralExpression(object)
- ) {
- return false
+ const sourceFile = node.getSourceFile()
+ const cached = NOTIFICATION_CHANNEL_NAME_NODES.get(sourceFile)
+ if (cached) {
+ return cached.has(node)
}
- let found = false
+ const nodes = new Set()
function visit(current) {
- if (found) {
- return
- }
if (
ts.isCallExpression(current) &&
current.arguments[1] &&
- expressionNameText(current.expression)?.endsWith('setNotificationChannelAsync') &&
- resolveObjectLiteral(current.arguments[1], bindings) === object
+ expressionNameText(current.expression)?.endsWith('setNotificationChannelAsync')
) {
- found = true
- return
+ for (const value of objectPropertyValues(current.arguments[1], 'name', bindings)) {
+ collectBoundStringNodes(value, bindings, nodes)
+ }
}
ts.forEachChild(current, visit)
}
- visit(node.getSourceFile())
- return found
+ visit(sourceFile)
+ NOTIFICATION_CHANNEL_NAME_NODES.set(sourceFile, nodes)
+ return nodes.has(node)
+}
+
+function isRenderedMobileExpression(expression) {
+ return isRenderedJsxExpression(expression) || isAssignedToRenderedVariable(expression)
}
function hasDisqualifyingBinaryAncestor(node) {
@@ -581,14 +606,7 @@ export function classifyMobileStringNode(node, userVisibleErrorSource, bindings)
) {
return 'user-visible-return'
}
- if (
- returnFunctionName &&
- isReturnedByRenderedFunction(
- node,
- (expression) =>
- isRenderedJsxExpression(expression) || isAssignedToRenderedVariable(expression)
- )
- ) {
+ if (returnFunctionName && isReturnedByRenderedFunction(node, isRenderedMobileExpression)) {
return 'rendered-function-return'
}
if (isUserVisibleErrorArgument(node, userVisibleErrorSource)) {
diff --git a/config/scripts/mobile-localization-candidate-rules.test.mjs b/config/scripts/mobile-localization-candidate-rules.test.mjs
index 53b750e1783..1ba6f8cd196 100644
--- a/config/scripts/mobile-localization-candidate-rules.test.mjs
+++ b/config/scripts/mobile-localization-candidate-rules.test.mjs
@@ -191,6 +191,42 @@ export function Example() {
expect(candidates.map((candidate) => candidate.text)).toEqual(['Extracting model'])
})
+ it('finds copy returned through a rendered helper object', () => {
+ const source = `
+function lookup() {
+ return 'Extracting object model'
+}
+export function Example() {
+ const renderers = { lookup }
+ return {renderers.lookup()}
+}
+`
+ const candidates = collectLocalizationCandidates(
+ '/repo/mobile/src/components/Example.tsx',
+ source,
+ '/repo'
+ )
+
+ expect(candidates.map((candidate) => candidate.text)).toEqual(['Extracting object model'])
+ })
+
+ it('matches rendered variables by binding instead of identifier text', () => {
+ const source = `
+const value = 'internal mode name'
+export function Example() {
+ const value = 'Visible nested value'
+ return {value}
+}
+`
+ const candidates = collectLocalizationCandidates(
+ '/repo/mobile/src/components/Example.tsx',
+ source,
+ '/repo'
+ )
+
+ expect(candidates.map((candidate) => candidate.text)).toEqual(['Visible nested value'])
+ })
+
it('finds Android notification-channel names', () => {
const source = `
const channel = {
@@ -208,6 +244,45 @@ Notifications.setNotificationChannelAsync('orca-desktop', channel)
expect(candidates.map((candidate) => candidate.text)).toEqual(['Desktop Notifications'])
})
+ it('finds bound and spread Android notification-channel names', () => {
+ const source = `
+const channelName = 'Desktop Notifications'
+const channel = { name: channelName }
+const baseChannel = { name: 'Background Notifications' }
+const spreadChannel = { ...baseChannel }
+Notifications.setNotificationChannelAsync('orca-desktop', channel)
+Notifications.setNotificationChannelAsync('orca-background', spreadChannel)
+`
+ const candidates = collectLocalizationCandidates(
+ '/repo/mobile/src/notifications/local-notification-scheduling.ts',
+ source,
+ '/repo'
+ )
+
+ expect(candidates.map((candidate) => candidate.text)).toEqual([
+ 'Desktop Notifications',
+ 'Background Notifications'
+ ])
+ })
+
+ it('reports raw copy from a namespace-local translator', () => {
+ const source = `
+import { t } from '@/i18n/mobile-i18n'
+namespace Local {
+ var t = (value) => value
+ export const label = {t('Namespace raw copy')}
+}
+export const translated = t('example.translated')
+`
+ const candidates = collectLocalizationCandidates(
+ '/repo/mobile/src/components/Example.tsx',
+ source,
+ '/repo'
+ )
+
+ expect(candidates.map((candidate) => candidate.text)).toEqual(['Namespace raw copy'])
+ })
+
it('finds user-visible subject fallbacks in returned rows', () => {
const source = `
export function toRow(item) {
diff --git a/config/scripts/mobile-localization-product-glossary.mjs b/config/scripts/mobile-localization-product-glossary.mjs
index 9de023a5054..0e2117c0cab 100644
--- a/config/scripts/mobile-localization-product-glossary.mjs
+++ b/config/scripts/mobile-localization-product-glossary.mjs
@@ -4,6 +4,151 @@ export const REQUIRED_TARGET_TRANSLATIONS = new Set([
'pullRequest.unresolvedComments.other'
])
+export const EXACT_LANGUAGE_NEUTRAL_KEYS = new Set([
+ 'mobileRichMarkdownEditor.h2',
+ 'terminalAccessoryKeyCatalog.del',
+ 'terminalAccessoryKeyCatalog.esc',
+ 'terminalAccessoryKeyCatalog.shiftPlus',
+ 'terminalAccessoryKeys.alt',
+ 'terminalAccessoryKeys.ctrl',
+ 'terminalAccessoryKeys.ins',
+ 'terminalAccessoryKeys.pgDn',
+ 'terminalAccessoryKeys.pgUp',
+ 'terminalAccessoryKeys.shift'
+])
+
+export const REVIEWED_KEY_TRANSLATIONS = new Map([
+ [
+ 'task.gitHubReportsMergeConflictsOpen',
+ { es: 'GitHub informa de conflictos de fusión. Abre GitHub para continuar.' }
+ ],
+ ['task.viewOrder', { es: 'Orden de vista', ja: '表示順', ko: '보기 순서', zh: '视图顺序' }],
+ ['workspaceListPickerOptions.server', { ko: '서버 순서' }],
+ [
+ 'task.orderLinearOrder',
+ {
+ ja: '順序: {{linearOrderLabel}}',
+ ko: '순서: {{linearOrderLabel}}',
+ zh: '顺序:{{linearOrderLabel}}'
+ }
+ ],
+ ['terminalAccessoryKeyCatalog.escape', { ja: 'Escapeキー', ko: 'Esc 키', zh: 'Esc 键' }],
+ ['terminalAccessoryKeyCatalog.enter', { ja: 'Enterキー', ko: 'Enter 키', zh: 'Enter 键' }],
+ ['terminalAccessoryKeyCatalog.space', { ja: 'スペース', ko: '스페이스', zh: '空格键' }],
+ ['customKeyModal.key', { es: 'Tecla', ja: 'キー', zh: '按键' }],
+ ['customKeyModal.pick', { es: 'Elige una tecla', ja: 'キーを選択してください', zh: '选择按键' }],
+ [
+ 'voiceSettings.togglePress',
+ {
+ ko: '토글: 한 번 누르면 시작되고 다시 누르면 중지됩니다. 길게 누르기: 누르고 있는 동안 받아씁니다.'
+ }
+ ],
+ [
+ 'mobileHostedReviewCreateIntent.force',
+ {
+ es: 'Haciendo push forzado con lease...',
+ ja: 'リースを使用して強制的にプッシュします...',
+ ko: '리스를 사용해 강제 푸시 중...',
+ zh: '正在使用租约强制推送...'
+ }
+ ],
+ [
+ 'mobileHostedReviewCreateIntent.pushing',
+ {
+ es: 'Haciendo push de commits...',
+ ja: 'commits をプッシュしています...',
+ ko: 'commits 푸시 중...',
+ zh: '正在推送 commits...'
+ }
+ ],
+ [
+ 'mobilePrCreate.push',
+ {
+ es: 'Haz push de la rama base antes de crear un {{reviewType}}.',
+ ja: '{{reviewType}} を作成する前にベース ブランチをプッシュしてください。',
+ ko: '{{reviewType}}을 생성하기 전에 기본 브랜치를 푸시하세요.',
+ zh: '在创建 {{reviewType}} 之前推送基础分支。'
+ }
+ ],
+ [
+ 'mobileSourceControlPrimaryAction.forcePushLease',
+ {
+ es: 'Haz un push forzado con lease para actualizar la rama remota.',
+ ja: 'リモート ブランチを更新するには、リースを使用して強制的にプッシュします。',
+ ko: '원격 브랜치를 업데이트하려면 리스를 사용해 강제 푸시하세요.',
+ zh: '使用租约强制推送以更新远程分支。'
+ }
+ ],
+ [
+ 'mobileSourceControlPrimaryAction.forcePushProgress',
+ {
+ es: 'Push forzado en curso.',
+ ja: '強制プッシュが進行中です。',
+ ko: '강제 푸시가 진행 중입니다.',
+ zh: '正在进行强制推送。'
+ }
+ ],
+ [
+ 'mobileSourceControlPrimaryAction.pullCommitCountCommit',
+ {
+ es: 'Haz pull de {{commitCount}} commit.',
+ ja: '{{commitCount}} commit をプルします。',
+ ko: '{{commitCount}}개 commit을 풀합니다.',
+ zh: '拉取 {{commitCount}} commit。'
+ }
+ ],
+ [
+ 'mobileSourceControlPrimaryAction.pullCommitCountCommits',
+ {
+ es: 'Haz pull de {{commitCount}} commits.',
+ ja: '{{commitCount}} commits をプルします。',
+ ko: '{{commitCount}}개 commits을 풀합니다.',
+ zh: '拉取 {{commitCount}} commits。'
+ }
+ ],
+ [
+ 'mobileSourceControlPrimaryAction.pushCommitCountCommit',
+ {
+ es: 'Haz push de {{commitCount}} commit.',
+ ja: '{{commitCount}} commit をプッシュします。',
+ ko: '{{commitCount}} commit을 푸시합니다.',
+ zh: '推送 {{commitCount}} commit。'
+ }
+ ],
+ [
+ 'mobileSourceControlPrimaryAction.pushCommitCountCommits',
+ {
+ es: 'Haz push de {{commitCount}} commits.',
+ ja: '{{commitCount}} commits をプッシュします。',
+ ko: '{{commitCount}} commits을 푸시합니다.',
+ zh: '推送 {{commitCount}} commits。'
+ }
+ ],
+ [
+ 'mobilePrComposeForm.pushCreateReview',
+ {
+ es: 'Hacer push y crear {{reviewType}}',
+ ja: 'プッシュして {{reviewType}} を作成',
+ ko: '푸시하고 {{reviewType}} 생성',
+ zh: '推送并创建 {{reviewType}}'
+ }
+ ],
+ [
+ 'mobilePrComposeForm.pushCreateDraft',
+ {
+ es: 'Hacer push y crear borrador de {{reviewType}}',
+ ja: 'プッシュしてドラフト {{reviewType}} を作成',
+ ko: '푸시하고 초안 {{reviewType}} 생성',
+ zh: '推送并创建草稿 {{reviewType}}'
+ }
+ ],
+ [
+ 'mobileOnboarding.notification',
+ { ja: '通知設定を更新できませんでした。もう一度お試しください。' }
+ ],
+ ['mobileOnboarding.your', { ja: '選択内容を保存できませんでした。もう一度お試しください。' }]
+])
+
export const PRODUCT_GLOSSARY = new Map([
[
'Sparse Checkout',
@@ -40,5 +185,147 @@ export const PRODUCT_GLOSSARY = new Map([
ko: '소스 제어 열기',
zh: '打开源代码管理'
}
+ ],
+ [
+ '{{pipelineJobCount}} jobs',
+ {
+ es: '{{pipelineJobCount}} trabajos del pipeline',
+ ja: '{{pipelineJobCount}} 件のジョブ',
+ ko: '파이프라인 작업 {{pipelineJobCount}}개',
+ zh: '{{pipelineJobCount}} 个作业'
+ }
+ ],
+ [
+ 'No checks found.',
+ {
+ es: 'No se encontraron comprobaciones.',
+ ja: 'チェックが見つかりませんでした。',
+ ko: '검사를 찾을 수 없습니다.',
+ zh: '未找到检查项。'
+ }
+ ],
+ [
+ 'Invalid checks response',
+ {
+ es: 'Respuesta de comprobaciones no válida',
+ ja: 'チェックの応答が無効です',
+ ko: '잘못된 검사 응답',
+ zh: '检查响应无效'
+ }
+ ],
+ [
+ 'Order Linear Issues',
+ {
+ es: 'Ordenar incidencias de Linear',
+ ja: 'Linear の課題を並べ替える',
+ ko: 'Linear 이슈 정렬',
+ zh: '排序 Linear 议题'
+ }
+ ],
+ [
+ 'Group Linear Issues',
+ {
+ es: 'Agrupar incidencias de Linear',
+ ja: 'Linear の課題をグループ化',
+ ko: 'Linear 이슈 그룹화',
+ zh: '对 Linear 议题分组'
+ }
+ ],
+ [
+ 'No states available',
+ {
+ es: 'No hay estados disponibles',
+ ja: '利用可能なステータスがありません',
+ ko: '사용 가능한 상태가 없습니다',
+ zh: '没有可用的状态'
+ }
+ ],
+ ['Body', { es: 'Texto normal', ja: '本文', ko: '본문', zh: '正文' }],
+ ['SPEECH MODEL', { es: 'MODELO DE VOZ', ja: '音声モデル', ko: '음성 모델', zh: '语音模型' }],
+ ['Open a shell', { es: 'Abrir un shell', ja: 'シェルを開く', ko: '셸 열기', zh: '打开 shell' }],
+ [
+ 'Save terminal artifact',
+ {
+ es: 'Guardar artefacto de terminal',
+ ja: 'ターミナルの成果物を保存',
+ ko: '터미널 아티팩트 저장',
+ zh: '保存终端产物'
+ }
+ ],
+ [
+ 'Push & Create PR',
+ {
+ es: 'Hacer push y crear PR',
+ ja: 'プッシュして PR を作成',
+ ko: '푸시 및 PR 생성',
+ zh: '推送并创建 PR'
+ }
+ ],
+ [
+ 'Commit & Push',
+ { es: 'Commit y push', ja: 'Commit とプッシュ', ko: 'Commit 및 푸시', zh: 'Commit 并推送' }
+ ],
+ [
+ 'Nothing to pull',
+ {
+ es: 'No hay cambios para pull',
+ ja: 'プルするものはありません',
+ ko: '풀할 내용이 없습니다',
+ zh: '没有可拉取的内容'
+ }
+ ],
+ [
+ 'Pull ({{behindCommitCount}})',
+ {
+ es: 'Hacer pull ({{behindCommitCount}})',
+ ja: 'プル ({{behindCommitCount}})',
+ ko: '풀 ({{behindCommitCount}})',
+ zh: '拉取 ({{behindCommitCount}})'
+ }
+ ],
+ [
+ 'Nothing to push',
+ {
+ es: 'No hay commits para push',
+ ja: 'プッシュするものはありません',
+ ko: '푸시할 내용이 없습니다',
+ zh: '没有可推送的内容'
+ }
+ ],
+ [
+ 'Push ({{aheadCommitCount}})',
+ {
+ es: 'Hacer push ({{aheadCommitCount}})',
+ ja: 'プッシュ ({{aheadCommitCount}})',
+ ko: '푸시 ({{aheadCommitCount}})',
+ zh: '推送 ({{aheadCommitCount}})'
+ }
+ ],
+ [
+ 'Pull {{behindCommitCount}}, push {{aheadCommitCount}}.',
+ {
+ es: 'Pull: {{behindCommitCount}}; push: {{aheadCommitCount}}.',
+ ja: '{{behindCommitCount}} 件をプルし、{{aheadCommitCount}} 件をプッシュします。',
+ ko: '{{behindCommitCount}}개를 풀하고 {{aheadCommitCount}}개를 푸시합니다.',
+ zh: '拉取 {{behindCommitCount}},推送 {{aheadCommitCount}}。'
+ }
+ ],
+ [
+ '{{failingCheckCount}} failing check',
+ {
+ es: '{{failingCheckCount}} comprobación fallida',
+ ja: '失敗したチェック {{failingCheckCount}} 件',
+ ko: '실패한 검사 {{failingCheckCount}}개',
+ zh: '{{failingCheckCount}} 项检查失败'
+ }
+ ],
+ [
+ 'Failed to load check details',
+ {
+ es: 'No se pudieron cargar los detalles de la comprobación',
+ ja: 'チェックの詳細を読み込めませんでした',
+ ko: '검사 세부 정보를 불러오지 못했습니다.',
+ zh: '无法加载检查详细信息'
+ }
]
])
diff --git a/config/scripts/mobile-localization-rendered-variable.mjs b/config/scripts/mobile-localization-rendered-variable.mjs
index b346132f118..e19eae7a72f 100644
--- a/config/scripts/mobile-localization-rendered-variable.mjs
+++ b/config/scripts/mobile-localization-rendered-variable.mjs
@@ -4,6 +4,8 @@ import ts from 'typescript-api'
import { collectSourceBindings } from './mobile-localization-source-bindings.mjs'
const SOURCE_BINDINGS = new WeakMap()
+const SOURCE_IDENTIFIERS = new WeakMap()
+const RENDERED_FUNCTION_RESULTS = new WeakMap()
function isDirectDisplayExpressionParent(parent, child) {
if (
@@ -72,7 +74,7 @@ export function isRenderedJsxExpression(node) {
return false
}
-function assignedVariableName(node) {
+function assignedVariableIdentifier(node) {
let current = node
while (current.parent && isDirectDisplayExpressionParent(current.parent, current)) {
current = current.parent
@@ -84,14 +86,14 @@ function assignedVariableName(node) {
parent.initializer === current &&
ts.isIdentifier(parent.name)
) {
- return parent.name.text
+ return parent.name
}
return parent &&
ts.isBinaryExpression(parent) &&
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
parent.right === current &&
ts.isIdentifier(parent.left)
- ? parent.left.text
+ ? parent.left
: undefined
}
@@ -139,6 +141,24 @@ function bindingsFor(sourceFile) {
return bindings
}
+function identifiersFor(sourceFile, name) {
+ let byName = SOURCE_IDENTIFIERS.get(sourceFile)
+ if (!byName) {
+ byName = new Map()
+ function visit(node) {
+ if (ts.isIdentifier(node)) {
+ const matches = byName.get(node.text) ?? []
+ matches.push(node)
+ byName.set(node.text, matches)
+ }
+ ts.forEachChild(node, visit)
+ }
+ visit(sourceFile)
+ SOURCE_IDENTIFIERS.set(sourceFile, byName)
+ }
+ return byName.get(name) ?? []
+}
+
function unwrapAliasExpression(node) {
if (
ts.isParenthesizedExpression(node) ||
@@ -150,6 +170,68 @@ function unwrapAliasExpression(node) {
return node
}
+function propertyNameText(name) {
+ return ts.isIdentifier(name) || ts.isStringLiteralLike(name) ? name.text : undefined
+}
+
+function propertyValueExpressions(node, propertyName, bindings, seen = new Set()) {
+ const expression = unwrapAliasExpression(node)
+ if (ts.isIdentifier(expression)) {
+ const binding = bindings.resolveBinding(expression)
+ if (
+ !binding ||
+ seen.has(binding) ||
+ !ts.isVariableDeclaration(binding) ||
+ !binding.initializer
+ ) {
+ return []
+ }
+ seen.add(binding)
+ return propertyValueExpressions(binding.initializer, propertyName, bindings, seen)
+ }
+ if (!ts.isObjectLiteralExpression(expression)) {
+ return []
+ }
+ return expression.properties.flatMap((property) => {
+ if (ts.isPropertyAssignment(property) && propertyNameText(property.name) === propertyName) {
+ return [property.initializer]
+ }
+ if (ts.isShorthandPropertyAssignment(property) && property.name.text === propertyName) {
+ return [property.name]
+ }
+ return ts.isSpreadAssignment(property)
+ ? propertyValueExpressions(property.expression, propertyName, bindings, new Set(seen))
+ : []
+ })
+}
+
+function expressionTargetsBinding(node, targets, bindings, seen = new Set()) {
+ const expression = unwrapAliasExpression(node)
+ if (ts.isIdentifier(expression)) {
+ const binding = bindings.resolveBinding(expression)
+ return Boolean(binding && targets.has(binding))
+ }
+ let object
+ let propertyName
+ if (ts.isPropertyAccessExpression(expression)) {
+ object = expression.expression
+ propertyName = expression.name.text
+ } else if (
+ ts.isElementAccessExpression(expression) &&
+ expression.argumentExpression &&
+ ts.isStringLiteralLike(expression.argumentExpression)
+ ) {
+ object = expression.expression
+ propertyName = expression.argumentExpression.text
+ }
+ if (!object || !propertyName) {
+ return false
+ }
+ return propertyValueExpressions(object, propertyName, bindings, seen).some((value) =>
+ expressionTargetsBinding(value, targets, bindings, seen)
+ )
+}
+
function renderedFunctionBindings(owner, sourceFile, bindings) {
const targets = new Set()
if (owner.name && ts.isIdentifier(owner.name)) {
@@ -212,6 +294,10 @@ export function isReturnedByRenderedFunction(node, isRenderedExpression = isRend
if (!owner) {
return false
}
+ const cached = RENDERED_FUNCTION_RESULTS.get(owner)?.get(isRenderedExpression)
+ if (cached !== undefined) {
+ return cached
+ }
const sourceFile = node.getSourceFile()
const bindings = bindingsFor(sourceFile)
const targetBindings = renderedFunctionBindings(owner, sourceFile, bindings)
@@ -222,8 +308,7 @@ export function isReturnedByRenderedFunction(node, isRenderedExpression = isRend
}
if (
ts.isCallExpression(current) &&
- ts.isIdentifier(current.expression) &&
- targetBindings.has(bindings.resolveBinding(current.expression)) &&
+ expressionTargetsBinding(current.expression, targetBindings, bindings) &&
isRenderedExpression(current)
) {
rendered = true
@@ -232,6 +317,9 @@ export function isReturnedByRenderedFunction(node, isRenderedExpression = isRend
ts.forEachChild(current, visit)
}
visit(sourceFile)
+ const results = RENDERED_FUNCTION_RESULTS.get(owner) ?? new Map()
+ results.set(isRenderedExpression, rendered)
+ RENDERED_FUNCTION_RESULTS.set(owner, results)
return rendered
}
@@ -239,8 +327,13 @@ export function isAssignedToRenderedVariable(node, isRenderedExpression = isRend
if (ts.isTemplateExpression(node)) {
return false
}
- const name = assignedVariableName(node)
- if (!name) {
+ const identifier = assignedVariableIdentifier(node)
+ if (!identifier) {
+ return false
+ }
+ const bindings = bindingsFor(node.getSourceFile())
+ const targetBinding = bindings.resolveBinding(identifier)
+ if (!targetBinding) {
return false
}
let owner = node.parent
@@ -250,17 +343,11 @@ export function isAssignedToRenderedVariable(node, isRenderedExpression = isRend
if (!owner) {
return false
}
- let rendered = false
- function visit(current) {
- if (rendered) {
- return
- }
- if (ts.isIdentifier(current) && current.text === name && isRenderedExpression(current)) {
- rendered = true
- return
- }
- ts.forEachChild(current, visit)
- }
- visit(owner)
- return rendered
+ return identifiersFor(node.getSourceFile(), identifier.text).some(
+ (current) =>
+ current.pos >= owner.pos &&
+ current.end <= owner.end &&
+ bindings.resolveBinding(current) === targetBinding &&
+ isRenderedExpression(current)
+ )
}
diff --git a/config/scripts/mobile-localization-source-bindings.mjs b/config/scripts/mobile-localization-source-bindings.mjs
index 00e43e2e254..36cc074ea62 100644
--- a/config/scripts/mobile-localization-source-bindings.mjs
+++ b/config/scripts/mobile-localization-source-bindings.mjs
@@ -19,6 +19,7 @@ function isLexicalScope(node) {
return (
ts.isSourceFile(node) ||
ts.isFunctionLike(node) ||
+ ts.isModuleBlock(node) ||
ts.isBlock(node) ||
ts.isCaseBlock(node) ||
ts.isCatchClause(node) ||
@@ -39,8 +40,12 @@ function nearestScope(node, sourceFile, predicate = isLexicalScope) {
return sourceFile
}
-function functionScope(node, sourceFile) {
- return nearestScope(node, sourceFile, ts.isFunctionLike)
+function varScope(node, sourceFile) {
+ return nearestScope(
+ node,
+ sourceFile,
+ (scope) => ts.isFunctionLike(scope) || ts.isModuleBlock(scope)
+ )
}
function collectDeclarations(sourceFile) {
@@ -77,7 +82,7 @@ function collectDeclarations(sourceFile) {
const scope =
ts.isVariableDeclarationList(declarationList) &&
(declarationList.flags & ts.NodeFlags.BlockScoped) === 0
- ? functionScope(node, sourceFile)
+ ? varScope(node, sourceFile)
: nearestScope(node, sourceFile)
for (const [name, declaration] of bindingDeclarations(node.name, node)) {
add(scope, name, declaration)
diff --git a/config/scripts/mobile-localization-translation-bindings.mjs b/config/scripts/mobile-localization-translation-bindings.mjs
index 176dd20cf9d..37fdf49c336 100644
--- a/config/scripts/mobile-localization-translation-bindings.mjs
+++ b/config/scripts/mobile-localization-translation-bindings.mjs
@@ -5,6 +5,17 @@ import { collectSourceBindings } from './mobile-localization-source-bindings.mjs
const MOBILE_I18N_MODULE_RE = /(?:^|\/)mobile-i18n$/
+function unwrapAliasExpression(node) {
+ if (
+ ts.isParenthesizedExpression(node) ||
+ ts.isAsExpression(node) ||
+ ts.isSatisfiesExpression(node)
+ ) {
+ return unwrapAliasExpression(node.expression)
+ }
+ return node
+}
+
function resolvesTo(identifier, bindings) {
return bindings.resolveBinding(identifier)
}
@@ -153,79 +164,141 @@ export function collectMobileTranslationBindings(sourceFile) {
function addBinding(identifier, bindingSet, nameSet) {
const binding = bindings.resolveBinding(identifier)
if (!binding) {
- return
+ return false
}
+ const changed = !bindingSet.has(binding)
bindingSet.add(binding)
nameSet?.add(identifier.text)
+ return changed
}
- function collectDestructuredAliases(node) {
- if (!ts.isObjectBindingPattern(node.name) || !node.initializer) {
- return
+ function destructuredTargets(name) {
+ const expression = unwrapAliasExpression(name)
+ if (ts.isObjectBindingPattern(expression)) {
+ return expression.elements.flatMap((element) => {
+ if (!ts.isIdentifier(element.name) || element.dotDotDotToken) {
+ return []
+ }
+ const propertyName = element.propertyName ?? element.name
+ return ts.isIdentifier(propertyName) || ts.isStringLiteralLike(propertyName)
+ ? [{ propertyName: propertyName.text, target: element.name }]
+ : []
+ })
}
- const namespaceSource = isMobileI18nNamespace(node.initializer, bindings)
- const instanceSource = isMobileI18nInstance(node.initializer, bindings)
+ if (!ts.isObjectLiteralExpression(expression)) {
+ return []
+ }
+ return expression.properties.flatMap((property) => {
+ if (ts.isShorthandPropertyAssignment(property)) {
+ return [{ propertyName: property.name.text, target: property.name }]
+ }
+ if (!ts.isPropertyAssignment(property)) {
+ return []
+ }
+ const target = unwrapAliasExpression(property.initializer)
+ const propertyName = property.name
+ return ts.isIdentifier(target) &&
+ (ts.isIdentifier(propertyName) || ts.isStringLiteralLike(propertyName))
+ ? [{ propertyName: propertyName.text, target }]
+ : []
+ })
+ }
+
+ function collectDestructuredAliases(name, initializer) {
+ const namespaceSource = isMobileI18nNamespace(initializer, bindings)
+ const instanceSource = isMobileI18nInstance(initializer, bindings)
if (!namespaceSource && !instanceSource) {
- return
+ return false
}
- for (const element of node.name.elements) {
- if (!ts.isIdentifier(element.name) || element.dotDotDotToken) {
- continue
- }
- const propertyName = element.propertyName ?? element.name
- if (!ts.isIdentifier(propertyName) && !ts.isStringLiteralLike(propertyName)) {
- continue
- }
- if (propertyName.text === 't') {
- addBinding(element.name, translatorBindings, translatorNames)
- } else if (namespaceSource && propertyName.text === 'createMobileTranslator') {
- addBinding(element.name, factoryBindings)
- } else if (namespaceSource && propertyName.text === 'mobileI18n') {
- addBinding(element.name, instanceBindings, instanceNames)
- } else if (instanceSource && propertyName.text === 'getFixedT') {
- addBinding(element.name, fixedFactoryBindings)
+ let changed = false
+ for (const { propertyName, target } of destructuredTargets(name)) {
+ if (propertyName === 't') {
+ changed = addBinding(target, translatorBindings, translatorNames) || changed
+ } else if (namespaceSource && propertyName === 'createMobileTranslator') {
+ changed = addBinding(target, factoryBindings) || changed
+ } else if (namespaceSource && propertyName === 'mobileI18n') {
+ changed = addBinding(target, instanceBindings, instanceNames) || changed
+ } else if (instanceSource && propertyName === 'getFixedT') {
+ changed = addBinding(target, fixedFactoryBindings) || changed
}
}
+ return changed
+ }
+
+ function addPrefixedBinding(identifier, prefix) {
+ const binding = bindings.resolveBinding(identifier)
+ if (!binding || prefixedTranslatorBindings.has(binding)) {
+ return false
+ }
+ prefixedTranslatorBindings.set(binding, prefix)
+ prefixedTranslatorNames.set(identifier.text, prefix)
+ return true
+ }
+
+ function collectIdentifierAlias(identifier, initializer) {
+ let changed = false
+ if (isMobileI18nNamespace(initializer, bindings)) {
+ changed = addBinding(identifier, namespaceBindings, namespaceNames) || changed
+ } else if (isTranslatorFactory(initializer, bindings)) {
+ changed = addBinding(identifier, factoryBindings) || changed
+ }
+ if (isFixedTranslatorFactory(initializer, bindings)) {
+ changed = addBinding(identifier, fixedFactoryBindings) || changed
+ }
+ if (isMobileI18nInstance(initializer, bindings)) {
+ changed = addBinding(identifier, instanceBindings, instanceNames) || changed
+ }
+ if (isEnglishFixedTranslator(initializer, bindings)) {
+ changed = addBinding(identifier, fixedTranslatorBindings, fixedTranslatorNames) || changed
+ }
+ const prefix =
+ mobileTranslatorPrefix(initializer, bindings) ?? directTranslatorPrefix(initializer, bindings)
+ return prefix === undefined ? changed : addPrefixedBinding(identifier, prefix) || changed
}
function collectLocalTranslators(node) {
- if (ts.isVariableDeclaration(node)) {
- collectDestructuredAliases(node)
+ let name
+ let initializer
+ if (ts.isVariableDeclaration(node) && node.initializer) {
+ name = node.name
+ initializer = node.initializer
+ } else if (
+ ts.isBinaryExpression(node) &&
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken
+ ) {
+ name = node.left
+ initializer = node.right
}
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
- const binding = bindings.resolveBinding(node.name)
- if (!binding) {
- ts.forEachChild(node, collectLocalTranslators)
- return
- }
- if (isMobileI18nNamespace(node.initializer, bindings)) {
- namespaceBindings.add(binding)
- namespaceNames.add(node.name.text)
- } else if (isTranslatorFactory(node.initializer, bindings)) {
- factoryBindings.add(binding)
- }
- if (isFixedTranslatorFactory(node.initializer, bindings)) {
- fixedFactoryBindings.add(binding)
- }
- if (isMobileI18nInstance(node.initializer, bindings)) {
- instanceBindings.add(binding)
- instanceNames.add(node.name.text)
- }
- if (isEnglishFixedTranslator(node.initializer, bindings)) {
- fixedTranslatorBindings.add(binding)
- fixedTranslatorNames.add(node.name.text)
- }
- const prefix =
- mobileTranslatorPrefix(node.initializer, bindings) ??
- directTranslatorPrefix(node.initializer, bindings)
- if (prefix !== undefined) {
- prefixedTranslatorBindings.set(binding, prefix)
- prefixedTranslatorNames.set(node.name.text, prefix)
+ let changed = false
+ if (name && initializer) {
+ changed = collectDestructuredAliases(name, initializer) || changed
+ const target = unwrapAliasExpression(name)
+ if (ts.isIdentifier(target)) {
+ changed = collectIdentifierAlias(target, initializer) || changed
}
}
- ts.forEachChild(node, collectLocalTranslators)
+ ts.forEachChild(node, (child) => {
+ changed = collectLocalTranslators(child) || changed
+ })
+ return changed
}
- collectLocalTranslators(sourceFile)
+ function bindingCount() {
+ return [
+ factoryBindings,
+ fixedFactoryBindings,
+ fixedTranslatorBindings,
+ instanceBindings,
+ namespaceBindings,
+ prefixedTranslatorBindings,
+ translatorBindings
+ ].reduce((total, collection) => total + collection.size, 0)
+ }
+
+ let previousCount
+ do {
+ previousCount = bindingCount()
+ collectLocalTranslators(sourceFile)
+ } while (bindingCount() > previousCount)
return bindings
}
diff --git a/config/scripts/mobile-localization-translation-bindings.test.mjs b/config/scripts/mobile-localization-translation-bindings.test.mjs
new file mode 100644
index 00000000000..2e186abab1e
--- /dev/null
+++ b/config/scripts/mobile-localization-translation-bindings.test.mjs
@@ -0,0 +1,50 @@
+import { describe, expect, it } from 'vitest'
+
+import { collectMobileTranslationCalls } from './verify-mobile-localization-catalog.mjs'
+
+describe('mobile-localization-translation-bindings', () => {
+ it('finds forward and assignment translator aliases to a fixed point', () => {
+ const sourceText = `
+import { createMobileTranslator, t } from '@/i18n/mobile-i18n'
+import * as i18n from '@/i18n/mobile-i18n'
+export function forwardLabel() {
+ const tr = laterAlias
+ return tr('example.forward')
+}
+const laterAlias = t
+let assigned
+assigned = t
+let destructured
+;({ t: destructured } = i18n)
+let prefixed
+prefixed = createMobileTranslator('example')
+export const labels = [
+ assigned('example.assigned'),
+ destructured('example.destructured'),
+ prefixed('prefixed')
+]
+`
+ const calls = collectMobileTranslationCalls('/repo/mobile/app/Example.tsx', sourceText, '/repo')
+
+ expect(calls.map((call) => call.keys)).toEqual([
+ ['example.forward'],
+ ['example.assigned'],
+ ['example.destructured'],
+ ['example.prefixed']
+ ])
+ })
+
+ it('keeps namespace var translators inside their module scope', () => {
+ const sourceText = `
+import { t } from '@/i18n/mobile-i18n'
+namespace Local {
+ var t = (value) => value
+ export const raw = t('not-a-translation-key')
+}
+export const translated = t('example.translated')
+`
+ const calls = collectMobileTranslationCalls('/repo/mobile/app/Example.tsx', sourceText, '/repo')
+
+ expect(calls.map((call) => call.keys)).toEqual([['example.translated']])
+ })
+})
diff --git a/config/scripts/verify-mobile-localization-catalog.mjs b/config/scripts/verify-mobile-localization-catalog.mjs
index 197a430ed72..f1e992ebaaa 100644
--- a/config/scripts/verify-mobile-localization-catalog.mjs
+++ b/config/scripts/verify-mobile-localization-catalog.mjs
@@ -12,7 +12,9 @@ import {
mobileTranslationCallPrefix
} from './mobile-localization-translation-bindings.mjs'
import {
+ EXACT_LANGUAGE_NEUTRAL_KEYS,
PRODUCT_GLOSSARY,
+ REVIEWED_KEY_TRANSLATIONS,
REQUIRED_TARGET_TRANSLATIONS
} from './mobile-localization-product-glossary.mjs'
@@ -294,7 +296,8 @@ function verifyTerminology(englishValue, localeValue, locale, catalogName, key)
if (locale === 'zh' && /\bagent/i.test(englishValue) && localeValue.includes('检测剂')) {
issues.push(`${catalogName} agent terminology mismatch: ${key} uses 检测剂`)
}
- const expectedValue = PRODUCT_GLOSSARY.get(englishValue)?.[locale]
+ const expectedValue =
+ REVIEWED_KEY_TRANSLATIONS.get(key)?.[locale] ?? PRODUCT_GLOSSARY.get(englishValue)?.[locale]
if (expectedValue && localeValue !== expectedValue) {
issues.push(`${catalogName} product terminology mismatch: ${key} must use ${expectedValue}`)
}
@@ -415,7 +418,10 @@ function verifyLocaleEntries(
if (!sameValues(englishPlaceholders, localePlaceholders)) {
issues.push(`${catalogName} placeholder mismatch: ${key}`)
}
- if (EXACT_LANGUAGE_NEUTRAL_VALUES.has(englishValue) && localeValue !== englishValue) {
+ if (
+ (EXACT_LANGUAGE_NEUTRAL_VALUES.has(englishValue) || EXACT_LANGUAGE_NEUTRAL_KEYS.has(key)) &&
+ localeValue !== englishValue
+ ) {
issues.push(`${catalogName} must preserve language-neutral value: ${key}`)
}
issues.push(...verifyProtectedLiterals(englishValue, localeValue, catalogName, key))
diff --git a/config/scripts/verify-mobile-localization-catalog.test.mjs b/config/scripts/verify-mobile-localization-catalog.test.mjs
index 6bb4417de01..1f32f1079be 100644
--- a/config/scripts/verify-mobile-localization-catalog.test.mjs
+++ b/config/scripts/verify-mobile-localization-catalog.test.mjs
@@ -486,6 +486,53 @@ export const labels = [
expect(report).toContain('es.json product terminology mismatch: example.openSource')
})
+ it('protects terminal legends and reviewed domain terminology', async () => {
+ const root = makeProject({
+ sourceText: `
+import { t } from '@/i18n/mobile-i18n'
+export const labels = [
+ t('terminalAccessoryKeyCatalog.del'),
+ t('mobileRichMarkdownEditor.h2'),
+ t('task.pipelineJob', { pipelineJobCount: 2 }),
+ t('task.openShell'),
+ t('voiceSettings.togglePress')
+]
+`,
+ catalogs: {
+ en: {
+ terminalAccessoryKeyCatalog: { del: 'Del' },
+ mobileRichMarkdownEditor: { h2: 'H2' },
+ task: { pipelineJob: '{{pipelineJobCount}} jobs', openShell: 'Open a shell' },
+ voiceSettings: {
+ togglePress: 'Toggle: press once to start, again to stop. Hold: dictate while held.'
+ }
+ },
+ es: {
+ terminalAccessoryKeyCatalog: { del: 'Supr' },
+ task: { pipelineJob: '{{pipelineJobCount}} empleos', openShell: 'Abrir una concha' }
+ },
+ ko: {
+ voiceSettings: {
+ togglePress:
+ '토글: 한 번 누르면 시작되고 다시 누르면 중지됩니다. 보류: 누르고 있는 동안 지시합니다.'
+ }
+ },
+ zh: { mobileRichMarkdownEditor: { h2: '氢2' } }
+ }
+ })
+
+ const report = await runFailedVerification(root)
+ expect(report).toContain(
+ 'es.json must preserve language-neutral value: terminalAccessoryKeyCatalog.del'
+ )
+ expect(report).toContain(
+ 'zh.json must preserve language-neutral value: mobileRichMarkdownEditor.h2'
+ )
+ expect(report).toContain('es.json product terminology mismatch: task.pipelineJob')
+ expect(report).toContain('es.json product terminology mismatch: task.openShell')
+ expect(report).toContain('ko.json product terminology mismatch: voiceSettings.togglePress')
+ })
+
it('validates placeholders and extra keys in present translations', async () => {
const en = { m: { greeting: 'Hello {{name}}' } }
const root = makeProject({
diff --git a/mobile/src/i18n/locales/es.json b/mobile/src/i18n/locales/es.json
index ffcb2f075bd..ff95bed3c20 100644
--- a/mobile/src/i18n/locales/es.json
+++ b/mobile/src/i18n/locales/es.json
@@ -60,7 +60,7 @@
"subIssues": "Subtemas",
"noPipeline": "No hay ejecuciones de pipeline para este MR.",
"none": "Ninguno",
- "pipelineJob": "{{pipelineJobCount}} empleos",
+ "pipelineJob": "{{pipelineJobCount}} trabajos del pipeline",
"fileContents": "Contenido del archivo no disponible.",
"binary": "Archivo binario.",
"markViewed": "Marcar visto",
@@ -68,7 +68,7 @@
"view": "Vista",
"hide": "Ocultar",
"changed": "Archivos modificados",
- "noChecks": "No se encontraron cheques.",
+ "noChecks": "No se encontraron comprobaciones.",
"rerunAll": "Vuelva a ejecutar todo",
"rerunFailed": "Error al volver a ejecutar",
"refresh": "Actualizar",
@@ -188,8 +188,8 @@
"createIssueSelectedRepository": "Cree un problema en el repositorio seleccionado.",
"sortTasks": "Ordenar tareas",
"displayProperties": "Propiedades de visualización",
- "orderLinearIssues": "Ordenar cuestiones Lineares",
- "groupLinearIssues": "Problemas Lineares del grupo",
+ "orderLinearIssues": "Ordenar incidencias de Linear",
+ "groupLinearIssues": "Agrupar incidencias de Linear",
"linearView": "Vista Linear",
"noStates": "No hay estados disponibles",
"loadingStates": "Cargando estados...",
@@ -266,7 +266,7 @@
"display": "Mostrar",
"tasks": "Tareas",
"allWorkspaces": "Todos los espacios de trabajo",
- "viewOrder": "Ver pedido",
+ "viewOrder": "Orden de vista",
"send": "Enviar comentario",
"addComment": "Agregar un comentario",
"failedUpdateIssueSource": "No se pudo actualizar la fuente del problema",
@@ -276,7 +276,7 @@
"failedAddLinear": "No se pudo agregar el comentario Linear",
"failedUpdateLinear": "No se pudo actualizar el problema Linear",
"failedMerge": "No se pudo fusionar",
- "gitHubReportsMergeConflictsOpen": "GitHub informa conflictos de fusión. Open in GitHub to continue.",
+ "gitHubReportsMergeConflictsOpen": "GitHub informa de conflictos de fusión. Abre GitHub para continuar.",
"failedReply": "No se pudo responder",
"failedAddReview": "No se pudo agregar el comentario de revisión",
"unableComment": "No se puede comentar sin el SHA del head del PR.",
@@ -310,7 +310,7 @@
"failedSave": "No se pudo guardar el ajuste preestablecido disperso.",
"failedSearch": "No se pudieron buscar ramas.",
"failedLoadSparse": "No se pudieron cargar ajustes preestablecidos escasos.",
- "openShell": "abrir un caparazón",
+ "openShell": "Abrir un shell",
"failedLoadIssue": "No se pudieron cargar los tipos de problemas",
"failedLoadAssignees": "No se pudieron cargar los asignados",
"failedLoadLabels": "No se pudieron cargar las etiquetas",
@@ -471,7 +471,7 @@
"failedReopen": "No se pudo reabrir el hilo",
"failedResolve": "No se pudo resolver el hilo",
"failedSync": "No se pudo sincronizar el estado visto con GitHub.",
- "invalid": "Respuesta de cheques no válidos",
+ "invalid": "Respuesta de comprobaciones no válida",
"failedUpdateGitHubStatus": "No se pudo actualizar el estado de GitHub",
"failedUpdateGitHubItem": "No se pudo actualizar el elemento de GitHub",
"selectedAgent": "El agent seleccionado está deshabilitado. Elige un agent habilitado antes de crear.",
@@ -762,7 +762,7 @@
"backspace": "Retroceso",
"space": "Espacio",
"shiftTab": "Pestaña Mayús",
- "shiftPlus": "Mayús+Tab",
+ "shiftPlus": "Shift+Tab",
"escape": "Escapar"
},
"terminalWebviewEngineErrorState": {
@@ -787,11 +787,11 @@
"arrowDown": "Flecha hacia abajo",
"arrowUp": "flecha arriba",
"insert": "Insertar",
- "pgDn": "AvPág",
- "pgUp": "Re Pág",
- "shift": "Cambio",
- "alt": "Alt.",
- "ctrl": "Control"
+ "pgDn": "PgDn",
+ "pgUp": "PgUp",
+ "shift": "Shift",
+ "alt": "Alt",
+ "ctrl": "Ctrl"
},
"terminalWebviewReadyWatchdog": {
"terminal": "El terminal no se inicializó: no hay señal de listo desde la vista del terminal"
@@ -961,8 +961,8 @@
"resolveStage": "Resuelva o organice cambios antes de crear una solicitud de extracción.",
"resolveConflicts": "Resuelva los conflictos antes de crear una solicitud de extracción.",
"creating": "Creando reseña...",
- "force": "Fuerza empujando con arrendamiento...",
- "pushing": "Impulsando commits...",
+ "force": "Haciendo push forzado con lease...",
+ "pushing": "Haciendo push de commits...",
"publishing": "Publicando rama...",
"committing": "Confirmando cambios...",
"generating": "Generando mensaje de commit...",
@@ -970,7 +970,7 @@
"failed": "No se han podido realizar cambios en la etapa"
},
"mobileHostedReviewService": {
- "pushFailed": "El empujón falló. Resuelva el error de inserción y vuelva a intentarlo.",
+ "pushFailed": "Falló el push. Resuelve el error de push y vuelve a intentarlo.",
"pushSucceeded": "El push se realizó correctamente, pero falló la creación de {{reviewType}}: {{errorMessage}}",
"failed": "No se pudo crear la solicitud de extracción"
},
@@ -978,10 +978,10 @@
"switch": "Cambiar rama",
"rebase": "Rebase sobre la base",
"publishBranch": "Publicar rama",
- "pushCreate": "Impulsar y crear PR",
+ "pushCreate": "Hacer push y crear PR",
"create": "Crear PR",
"commitSync": "Commit y sincronizar",
- "commitPush": "Commit y presionar",
+ "commitPush": "Commit y push",
"pullRequests": "Las solicitudes de extracción no están disponibles para este repo",
"checking": "Comprobando el estado de la rama...",
"publishBranchFirst": "Publicar rama primero",
@@ -995,10 +995,10 @@
"branchUp": "La rama está actualizada.",
"sync": "Sincronizar",
"syncBehind": "Sincronización (↓{{behindCommitCount}} ↑{{aheadCommitCount}})",
- "nothingPull": "Nada que tirar",
- "pullBehind": "Tirar ({{behindCommitCount}})",
- "nothingPush": "Nada que empujar",
- "pushAhead": "Empujar ({{aheadCommitCount}})"
+ "nothingPull": "No hay cambios para pull",
+ "pullBehind": "Hacer pull ({{behindCommitCount}})",
+ "nothingPush": "No hay commits para push",
+ "pushAhead": "Hacer push ({{aheadCommitCount}})"
},
"useMobileCommitFailureRecovery": {
"failed": "No se pudo iniciar el agent",
@@ -1048,7 +1048,7 @@
},
"mobilePrCreate": {
"branch": "Esta rama aún no está lista para un {{reviewType}}.",
- "push": "Empuje la rama base antes de crear un {{reviewType}}.",
+ "push": "Haz push de la rama base antes de crear un {{reviewType}}.",
"creatingReviewTypeFork": "No se admite la creación de un {{reviewType}} desde esta bifurcación.",
"reviewTypeAlreadyExists": "Ya existe un {{reviewType}} para esta rama.",
"creatingReviewTypeS": "La creación de {{reviewType}} no es compatible con este repo.",
@@ -1065,7 +1065,7 @@
},
"mobileSourceControlPrimaryAction": {
"nothing": "No hay nada para hacer commit. La rama está actualizada.",
- "forcePushLease": "Fuerce el envío con arrendamiento para actualizar la rama remota.",
+ "forcePushLease": "Haz un push forzado con lease para actualizar la rama remota.",
"publishBranchOrigin": "Publicar esta rama en origen.",
"check": "Haz checkout de una rama antes de publicar commits.",
"stageLeast": "Prepare al menos un archivo para commit.",
@@ -1075,16 +1075,16 @@
"resolve": "Resolver conflictos antes de comprometerse.",
"try": "Inténtelo nuevamente una vez que finalice la operación remota.",
"remote": "Operación remota en progreso.",
- "forcePushProgress": "Empuje forzado en curso.",
+ "forcePushProgress": "Push forzado en curso.",
"commitProgress": "Compromiso en progreso.",
"publishBranch": "Publicar rama",
"sync": "Sincronizar",
"stageAll": "Hacer stage de todo",
- "pullBehind": "Tire de {{behindCommitCount}}, presione {{aheadCommitCount}}.",
- "pullCommitCountCommit": "Tire del commit {{commitCount}}.",
- "pullCommitCountCommits": "Extraiga las commits de {{commitCount}}.",
- "pushCommitCountCommit": "Empuje el commit {{commitCount}}.",
- "pushCommitCountCommits": "Empuje {{commitCount}} commits."
+ "pullBehind": "Pull: {{behindCommitCount}}; push: {{aheadCommitCount}}.",
+ "pullCommitCountCommit": "Haz pull de {{commitCount}} commit.",
+ "pullCommitCountCommits": "Haz pull de {{commitCount}} commits.",
+ "pushCommitCountCommit": "Haz push de {{commitCount}} commit.",
+ "pushCommitCountCommits": "Haz push de {{commitCount}} commits."
},
"mobileHostedReviewGitPreparation": {
"commit": "La Commit falló",
@@ -1496,7 +1496,7 @@
"label": "Etiqueta",
"add": "Agregar",
"more": "Más teclas: tabulador, flechas, F1–F12…",
- "key": "Clave",
+ "key": "Tecla",
"modifiers": "Modificadores",
"show": "Mostrar, ocultar o reordenar las teclas de método abreviado",
"manage": "Administrar atajos",
@@ -1509,8 +1509,8 @@
"navigation": "Navegación",
"editing": "Edición",
"eGPnpm": "p. ej. pnpm build",
- "eGBuild": "p.ej. Construir",
- "pick": "Elige una llave"
+ "eGBuild": "p. ej., Compilar",
+ "pick": "Elige una tecla"
},
"dragReorderList": {
"moveDown": "Bajar",
@@ -1670,7 +1670,7 @@
"strike": "Tachado",
"italic": "Cursiva",
"bold": "Negrita",
- "body": "Cuerpo"
+ "body": "Texto normal"
},
"mobileWorkspaceNameInput": {
"workspace": "Nombre del espacio de trabajo"
@@ -1833,8 +1833,8 @@
"mobilePrComposeForm": {
"createReview": "Crear {{reviewType}}",
"createDraftReview": "Crear borrador {{reviewType}}",
- "pushCreateReview": "Empujar y crear {{reviewType}}",
- "pushCreateDraft": "Enviar y crear borrador {{reviewType}}",
+ "pushCreateReview": "Hacer push y crear {{reviewType}}",
+ "pushCreateDraft": "Hacer push y crear borrador de {{reviewType}}",
"createDraft": "Crear como borrador",
"generatingTitle": "Generando título y descripción…",
"reviewTypeDescription": "{{reviewType}} descripción",
@@ -1883,9 +1883,9 @@
"fixFailing": "Solucionar controles fallidos con IA",
"inspect": "Inspecciona los detalles o inicia una pasada de corrección con IA.",
"rerun": "Volver a ejecutar comprobaciones fallidas",
- "failingCheckCountFailingCheck": "{{failingCheckCount}} cheque fallido",
- "failingCheckCountFailingChecks": "{{failingCheckCount}} controles fallidos",
- "failed": "No se pudieron cargar los detalles del cheque"
+ "failingCheckCountFailingCheck": "{{failingCheckCount}} comprobación fallida",
+ "failingCheckCountFailingChecks": "{{failingCheckCount}} comprobaciones fallidas",
+ "failed": "No se pudieron cargar los detalles de la comprobación"
},
"prcommentCard": {
"permanently": "Esto elimina permanentemente el comentario en GitHub.",
@@ -2059,7 +2059,7 @@
},
"mobileFilePreviewScreen": {
"unableLoad": "No se puede cargar la vista previa",
- "save": "Guardar artefacto terminal",
+ "save": "Guardar artefacto de terminal",
"back": "Volver a archivos",
"unsaved": "Las ediciones no guardadas se perderán.",
"discardChanges": "¿Descartar cambios?",
@@ -2386,7 +2386,7 @@
},
"voiceSettings": {
"speechModelMessage": "Modelo de voz",
- "speechModel": "MODELO DE DISCURSO",
+ "speechModel": "MODELO DE VOZ",
"togglePress": "Alternar: presione una vez para comenzar y nuevamente para detener. Mantener: dictar mientras se mantiene presionado.",
"dictationMode": "Modo de dictado",
"dictate": "Dicta texto en cualquier panel enfocado de tu escritorio.",
diff --git a/mobile/src/i18n/locales/ja.json b/mobile/src/i18n/locales/ja.json
index 500c120d992..132d3ae8614 100644
--- a/mobile/src/i18n/locales/ja.json
+++ b/mobile/src/i18n/locales/ja.json
@@ -61,7 +61,7 @@
"subIssues": "サブ Issue",
"noPipeline": "この MR ではパイプラインは実行されません。",
"none": "なし",
- "pipelineJob": "{{pipelineJobCount}} の求人",
+ "pipelineJob": "{{pipelineJobCount}} 件のジョブ",
"pipeline": "パイプライン",
"fileContents": "ファイルの内容は利用できません。",
"binary": "バイナリファイル。",
@@ -192,10 +192,10 @@
"createIssueSelectedRepository": "選択したリポジトリに Issue を作成します。",
"sortTasks": "タスクの並べ替え",
"displayProperties": "表示プロパティ",
- "orderLinearIssues": "順序 Linear Issue",
- "groupLinearIssues": "グループの Linear Issue",
+ "orderLinearIssues": "Linear の課題を並べ替える",
+ "groupLinearIssues": "Linear の課題をグループ化",
"linearView": "リニアビュー",
- "noStates": "利用可能な州がありません",
+ "noStates": "利用可能なステータスがありません",
"loadingStates": "状態を読み込み中...",
"changeStatus": "ステータスの変更",
"allTeams": "全チーム",
@@ -272,7 +272,7 @@
"display": "画面",
"tasks": "タスク",
"allWorkspaces": "すべてのワークスペース",
- "viewOrder": "注文を見る",
+ "viewOrder": "表示順",
"send": "コメントを送信",
"addComment": "コメントを追加",
"failedUpdateIssueSource": "Issue ソースの更新に失敗しました",
@@ -383,7 +383,7 @@
"sortGithub": "並べ替え: {{githubProjectSortLabel}}",
"fieldsGithub": "フィールド: {{githubProjectFieldsLabel}}",
"groupLinearGroup": "グループ: {{linearGroupLabel}}",
- "orderLinearOrder": "注文: {{linearOrderLabel}}",
+ "orderLinearOrder": "順序: {{linearOrderLabel}}",
"preferred": "{{repoName}} では、アップストリームの優先 Issue ソースが利用できません。原点を使用します。",
"plus": "+{{githubProjectSummaryFieldCount}} フィールド",
"baseBranch": "{{baseBranch}}に",
@@ -483,7 +483,7 @@
"failedReopen": "スレッドを再開できませんでした",
"failedResolve": "スレッドを解決できませんでした",
"failedSync": "表示された状態を GitHub と同期できませんでした。",
- "invalid": "無効なチェックの応答",
+ "invalid": "チェックの応答が無効です",
"failedUpdateGitHubStatus": "GitHub ステータスの更新に失敗しました",
"failedUpdateGitHubItem": "GitHub アイテムの更新に失敗しました",
"selectedAgent": "選択した agent は無効になっています。作成する前に、有効な agent を選択。",
@@ -762,25 +762,25 @@
"clearLine": "カーソル前の行をクリア",
"delete": "単語を後方に削除",
"end": "行末",
- "start": "行の始まり",
- "reverse": "逆引き検索",
- "suspend": "プロセスを一時停止する",
- "clearScreen": "画面をクリアする",
+ "start": "行頭",
+ "reverse": "逆方向検索",
+ "suspend": "プロセスを一時停止",
+ "clearScreen": "画面をクリア",
"send": "EOFを送信",
- "interrupt": "割り込み terminal",
+ "interrupt": "ターミナルに割り込み",
"arrowRight": "右矢印",
"arrowLeft": "左矢印",
"arrowDown": "下矢印",
"arrowUp": "上矢印",
"forward": "前方削除",
- "del": "デル",
+ "del": "Del",
"backspace": "バックスペース",
"space": "スペース",
- "shiftTab": "シフトタブ",
- "enter": "入力",
- "tab": "タブ",
- "escape": "逃げる",
- "esc": "ESC"
+ "shiftTab": "Shift+Tab",
+ "enter": "Enterキー",
+ "tab": "Tabキー",
+ "escape": "Escapeキー",
+ "esc": "Esc"
},
"terminalWebviewEngineErrorState": {
"reload": "リロード",
@@ -797,17 +797,17 @@
"terminalAccessoryKeys": {
"pageDown": "ページダウン",
"pageUp": "ページアップ",
- "end": "終わり",
- "home": "ホーム",
+ "end": "Endキー",
+ "home": "Homeキー",
"arrowRight": "右矢印",
"arrowLeft": "左矢印",
"arrowDown": "下矢印",
"arrowUp": "上矢印",
- "insert": "入れる",
- "pgDn": "PGDN",
- "ins": "インス",
- "shift": "シフト",
- "alt": "代替"
+ "insert": "Insertキー",
+ "pgDn": "PgDn",
+ "ins": "Ins",
+ "shift": "Shift",
+ "alt": "Alt"
},
"terminalWebviewReadyWatchdog": {
"terminal": "Terminal が初期化されませんでした - Terminal ビューから準備完了信号がありません"
@@ -995,7 +995,7 @@
"rebase": "ベースにリベースする",
"publishBranch": "ブランチを公開",
"fetch": "フェッチ",
- "pushCreate": "PR のプッシュと作成",
+ "pushCreate": "プッシュして PR を作成",
"create": "PR を作成",
"commitSync": "Commit と同期",
"commitPush": "Commit とプッシュ",
@@ -1012,10 +1012,10 @@
"branchUp": "ブランチは最新です",
"sync": "同期",
"syncBehind": "同期 (↓{{behindCommitCount}} ↑{{aheadCommitCount}})",
- "nothingPull": "何も引っ張るものがない",
+ "nothingPull": "プルするものはありません",
"pull": "プル",
"pullBehind": "プル ({{behindCommitCount}})",
- "nothingPush": "押し付けるものは何もない",
+ "nothingPush": "プッシュするものはありません",
"push": "プッシュ",
"pushAhead": "プッシュ ({{aheadCommitCount}})"
},
@@ -1102,7 +1102,7 @@
"sync": "同期",
"stageAll": "ステージオール",
"forcePush": "フォースプッシュ",
- "pullBehind": "{{behindCommitCount}} を引いて、{{aheadCommitCount}} を押します。",
+ "pullBehind": "{{behindCommitCount}} 件をプルし、{{aheadCommitCount}} 件をプッシュします。",
"pullCommitCountCommit": "{{commitCount}} commit をプルします。",
"pullCommitCountCommits": "{{commitCount}} commits をプルします。",
"pushCommitCountCommit": "{{commitCount}} commit をプッシュします。",
@@ -1528,7 +1528,7 @@
"label": "ラベル",
"add": "追加",
"more": "その他のキー — Tab、矢印、F1 ~ F12…",
- "key": "鍵",
+ "key": "キー",
"modifiers": "修飾子",
"show": "ショートカット キーの表示、非表示、または並べ替え",
"manage": "ショートカットの管理",
@@ -1541,7 +1541,7 @@
"navigation": "ナビゲーション",
"editing": "編集",
"eGPnpm": "例: pnpm build",
- "eGBuild": "例えば建てる",
+ "eGBuild": "例: ビルド",
"pick": "キーを選択してください"
},
"dragReorderList": {
@@ -1702,7 +1702,7 @@
"strike": "取り消し線",
"italic": "イタリック",
"bold": "太字",
- "body": "体"
+ "body": "本文"
},
"mobileWorkspaceNameInput": {
"workspace": "ワークスペース名"
@@ -1866,8 +1866,8 @@
"mobilePrComposeForm": {
"createReview": "{{reviewType}} を作成する",
"createDraftReview": "ドラフト {{reviewType}} を作成する",
- "pushCreateReview": "プッシュして{{reviewType}}を作成",
- "pushCreateDraft": "プッシュしてドラフトを作成 {{reviewType}}",
+ "pushCreateReview": "プッシュして {{reviewType}} を作成",
+ "pushCreateDraft": "プッシュしてドラフト {{reviewType}} を作成",
"createDraft": "下書きとして作成",
"base": "ベース",
"generatingTitle": "タイトルと説明を生成しています…",
@@ -1919,9 +1919,9 @@
"inspect": "詳細を検査するか、AI 修正パスを開始します。",
"rerun": "失敗したチェックを再実行する",
"checks": "チェック",
- "failingCheckCountFailingCheck": "{{failingCheckCount}} チェックに失敗しました",
+ "failingCheckCountFailingCheck": "失敗したチェック {{failingCheckCount}} 件",
"failingCheckCountFailingChecks": "{{failingCheckCount}} チェックに失敗しました",
- "failed": "チェックの詳細をロードできませんでした"
+ "failed": "チェックの詳細を読み込めませんでした"
},
"prcommentCard": {
"permanently": "これにより、GitHub 上のコメントが完全に削除されます。",
@@ -2096,7 +2096,7 @@
},
"mobileFilePreviewScreen": {
"unableLoad": "プレビューを読み込めません",
- "save": "terminal アーティファクトを保存する",
+ "save": "ターミナルの成果物を保存",
"back": "ファイルに戻る",
"unsaved": "保存されていない編集内容は失われます。",
"discardChanges": "変更を破棄しますか?",
@@ -2309,8 +2309,8 @@
},
"mobileOnboarding": {
"onboarding": "オンボーディングの進捗状況",
- "notification": "Notification settings could not be updated.再試行。",
- "your": "Your choice could not be saved.再試行。"
+ "notification": "通知設定を更新できませんでした。もう一度お試しください。",
+ "your": "選択内容を保存できませんでした。もう一度お試しください。"
},
"nativeChatSettings": {
"open": "チャット UI でセッションを開く",
diff --git a/mobile/src/i18n/locales/ko.json b/mobile/src/i18n/locales/ko.json
index a541c92ead0..3327e5d060f 100644
--- a/mobile/src/i18n/locales/ko.json
+++ b/mobile/src/i18n/locales/ko.json
@@ -9,7 +9,7 @@
"pr": "PR 현황",
"status": "상태",
"no": "그룹화 없음",
- "server": "서버 주문",
+ "server": "서버 순서",
"manual": "수동",
"repository": "저장소, 워크스페이스 이름",
"most": "가장 최근 출력 먼저",
@@ -61,7 +61,7 @@
"subIssues": "하위 이슈",
"noPipeline": "이 MR에 실행된 파이프라인이 없습니다.",
"none": "없음",
- "pipelineJob": "{{pipelineJobCount}} 직업",
+ "pipelineJob": "파이프라인 작업 {{pipelineJobCount}}개",
"pipeline": "파이프라인",
"fileContents": "파일 콘텐츠를 사용할 수 없습니다.",
"binary": "바이너리 파일.",
@@ -70,7 +70,7 @@
"view": "보기",
"hide": "숨기기",
"changed": "변경된 파일",
- "noChecks": "수표를 찾을 수 없습니다.",
+ "noChecks": "검사를 찾을 수 없습니다.",
"rerunAll": "모두 다시 실행",
"rerunFailed": "재실행 실패",
"refresh": "새로고침",
@@ -192,10 +192,10 @@
"createIssueSelectedRepository": "선택한 저장소에서 이슈를 생성합니다.",
"sortTasks": "작업 정렬",
"displayProperties": "디스플레이 속성",
- "orderLinearIssues": "주문 Linear 문제",
- "groupLinearIssues": "그룹 Linear 문제",
+ "orderLinearIssues": "Linear 이슈 정렬",
+ "groupLinearIssues": "Linear 이슈 그룹화",
"linearView": "Linear 보기",
- "noStates": "사용 가능한 주 없음",
+ "noStates": "사용 가능한 상태가 없습니다",
"loadingStates": "상태 로드 중...",
"changeStatus": "상태 변경",
"allTeams": "모든 팀",
@@ -272,7 +272,7 @@
"display": "표시",
"tasks": "작업",
"allWorkspaces": "모든 워크스페이스",
- "viewOrder": "주문 보기",
+ "viewOrder": "보기 순서",
"send": "댓글 보내기",
"addComment": "댓글 추가",
"failedUpdateIssueSource": "이슈 소스를 업데이트하지 못했습니다.",
@@ -383,7 +383,7 @@
"sortGithub": "정렬: {{githubProjectSortLabel}}",
"fieldsGithub": "필드: {{githubProjectFieldsLabel}}",
"groupLinearGroup": "그룹: {{linearGroupLabel}}",
- "orderLinearOrder": "주문: {{linearOrderLabel}}",
+ "orderLinearOrder": "순서: {{linearOrderLabel}}",
"preferred": "{{repoName}}에는 기본 이슈 소스 업스트림을 사용할 수 없습니다. 원산지를 사용합니다.",
"plus": "+{{githubProjectSummaryFieldCount}} 필드",
"baseBranch": "{{baseBranch}}에",
@@ -483,7 +483,7 @@
"failedReopen": "스레드를 다시 열지 못했습니다.",
"failedResolve": "스레드를 해결하지 못했습니다.",
"failedSync": "본 상태를 GitHub와 동기화하지 못했습니다.",
- "invalid": "잘못된 수표 응답",
+ "invalid": "잘못된 검사 응답",
"failedUpdateGitHubStatus": "GitHub 상태를 업데이트하지 못했습니다.",
"failedUpdateGitHubItem": "GitHub 항목을 업데이트하지 못했습니다.",
"selectedAgent": "선택한 agent가 비활성화되었습니다. 생성하기 전에 활성화된 agent를 선택하세요.",
@@ -767,18 +767,18 @@
"suspend": "프로세스 일시 중지",
"clearScreen": "화면 지우기",
"send": "EOF 보내기",
- "interrupt": "인터럽트 terminal",
+ "interrupt": "터미널 중단",
"arrowRight": "오른쪽 화살표",
"arrowLeft": "왼쪽 화살표",
"arrowDown": "아래쪽 화살표",
"arrowUp": "위쪽 화살표",
"forward": "앞으로 삭제",
- "del": "델",
- "backspace": "역행 키이",
- "space": "공간",
- "shiftTab": "시프트 탭",
- "enter": "입력",
- "escape": "탈출하다"
+ "del": "Del",
+ "backspace": "Backspace 키",
+ "space": "스페이스",
+ "shiftTab": "Shift+Tab",
+ "enter": "Enter 키",
+ "escape": "Esc 키"
},
"terminalWebviewEngineErrorState": {
"reload": "새로고침",
@@ -795,16 +795,16 @@
"terminalAccessoryKeys": {
"pageDown": "페이지 아래로",
"pageUp": "페이지 위로",
- "end": "끝",
- "home": "홈",
+ "end": "End 키",
+ "home": "Home 키",
"arrowRight": "오른쪽 화살표",
"arrowLeft": "왼쪽 화살표",
"arrowDown": "아래쪽 화살표",
"arrowUp": "위쪽 화살표",
- "insert": "삽입",
- "ins": "인",
- "shift": "옮기다",
- "ctrl": "Ctrl 키"
+ "insert": "Insert 키",
+ "ins": "Ins",
+ "shift": "Shift",
+ "ctrl": "Ctrl"
},
"terminalWebviewReadyWatchdog": {
"terminal": "Terminal이 초기화되지 않았습니다. Terminal 보기에 준비 신호가 없습니다."
@@ -972,7 +972,7 @@
"resolveStage": "PR을 생성하기 전에 변경 사항을 해결하거나 준비하세요.",
"resolveConflicts": "PR을 생성하기 전에 충돌을 해결하세요.",
"creating": "리뷰 작성 중...",
- "force": "임대로 강제 추진 중...",
+ "force": "리스를 사용해 강제 푸시 중...",
"pushing": "commits 푸시 중...",
"publishing": "브랜치 게시 중...",
"committing": "변경사항을 커밋하는 중...",
@@ -1007,12 +1007,12 @@
"branchUp": "브랜치이 최신 상태입니다.",
"sync": "동기화",
"syncBehind": "동기화(↓{{behindCommitCount}} ↑{{aheadCommitCount}})",
- "nothingPull": "당길 게 하나도 없어",
+ "nothingPull": "풀할 내용이 없습니다",
"pull": "풀",
- "pullBehind": "당기기({{behindCommitCount}})",
- "nothingPush": "밀어붙일 게 하나도 없어",
+ "pullBehind": "풀 ({{behindCommitCount}})",
+ "nothingPush": "푸시할 내용이 없습니다",
"push": "푸시",
- "pushAhead": "푸시({{aheadCommitCount}})"
+ "pushAhead": "푸시 ({{aheadCommitCount}})"
},
"useMobileCommitFailureRecovery": {
"failed": "agent를 시작하지 못했습니다.",
@@ -1081,7 +1081,7 @@
"nothing": "commit 할 항목이 없습니다. 브랜치가 최신 상태입니다.",
"push": "푸시",
"pull": "풀",
- "forcePushLease": "원격 브랜치를 업데이트하려면 임대를 통해 강제로 푸시하세요.",
+ "forcePushLease": "원격 브랜치를 업데이트하려면 리스를 사용해 강제 푸시하세요.",
"publishBranchOrigin": "이 브랜치를 원본에 게시합니다.",
"check": "commits을 게시하기 전에 브랜치를 체크아웃하세요.",
"stageLeast": "commit 할 파일을 하나 이상 준비하세요.",
@@ -1097,9 +1097,9 @@
"sync": "동기화",
"stageAll": "모두 스테이지",
"forcePush": "강제 푸시",
- "pullBehind": "{{behindCommitCount}}을 당기고, {{aheadCommitCount}}을 누르세요.",
- "pullCommitCountCommit": "{{commitCount}} commit을 가져옵니다.",
- "pullCommitCountCommits": "{{commitCount}} commits을 가져옵니다.",
+ "pullBehind": "{{behindCommitCount}}개를 풀하고 {{aheadCommitCount}}개를 푸시합니다.",
+ "pullCommitCountCommit": "{{commitCount}}개 commit을 풀합니다.",
+ "pullCommitCountCommits": "{{commitCount}}개 commits을 풀합니다.",
"pushCommitCountCommit": "{{commitCount}} commit을 푸시합니다.",
"pushCommitCountCommits": "{{commitCount}} commits을 푸시합니다."
},
@@ -1698,7 +1698,7 @@
"strike": "취소선",
"italic": "기울임",
"bold": "굵게",
- "body": "몸"
+ "body": "본문"
},
"mobileWorkspaceNameInput": {
"workspace": "워크스페이스 이름"
@@ -1862,8 +1862,8 @@
"mobilePrComposeForm": {
"createReview": "{{reviewType}} 만들기",
"createDraftReview": "초안 {{reviewType}} 만들기",
- "pushCreateReview": "{{reviewType}} 푸시 및 생성",
- "pushCreateDraft": "초안 푸시 및 생성 {{reviewType}}",
+ "pushCreateReview": "푸시하고 {{reviewType}} 생성",
+ "pushCreateDraft": "푸시하고 초안 {{reviewType}} 생성",
"createDraft": "초안으로 만들기",
"base": "베이스",
"generatingTitle": "제목 및 설명 생성 중…",
@@ -1915,9 +1915,9 @@
"inspect": "세부 정보를 검사하거나 AI 수정 패스를 시작하세요.",
"rerun": "실패한 검사 다시 실행",
"checks": "체크",
- "failingCheckCountFailingCheck": "{{failingCheckCount}} 확인 실패",
+ "failingCheckCountFailingCheck": "실패한 검사 {{failingCheckCount}}개",
"failingCheckCountFailingChecks": "{{failingCheckCount}} 검사 실패",
- "failed": "수표 세부정보를 로드하지 못했습니다."
+ "failed": "검사 세부 정보를 불러오지 못했습니다."
},
"prcommentCard": {
"permanently": "GitHub의 댓글이 영구적으로 삭제됩니다.",
@@ -2094,7 +2094,7 @@
},
"mobileFilePreviewScreen": {
"unableLoad": "미리보기를 로드할 수 없습니다.",
- "save": "terminal 아티팩트 저장",
+ "save": "터미널 아티팩트 저장",
"back": "파일로 돌아가기",
"unsaved": "저장되지 않은 수정사항은 손실됩니다.",
"discardChanges": "변경사항을 삭제하시겠습니까?",
@@ -2424,8 +2424,8 @@
},
"voiceSettings": {
"speechModelMessage": "음성 모델",
- "speechModel": "연설 모델",
- "togglePress": "토글: 한 번 누르면 시작되고 다시 누르면 중지됩니다. 보류: 누르고 있는 동안 지시합니다.",
+ "speechModel": "음성 모델",
+ "togglePress": "토글: 한 번 누르면 시작되고 다시 누르면 중지됩니다. 길게 누르기: 누르고 있는 동안 받아씁니다.",
"dictationMode": "받아쓰기 모드",
"dictate": "데스크탑의 초점이 맞춰진 창에 텍스트를 받아쓰세요.",
"enable": "음성 받아쓰기 활성화",
diff --git a/mobile/src/i18n/locales/zh.json b/mobile/src/i18n/locales/zh.json
index dbbb2fd2189..47c2b3e75b5 100644
--- a/mobile/src/i18n/locales/zh.json
+++ b/mobile/src/i18n/locales/zh.json
@@ -61,7 +61,7 @@
"subIssues": "子问题",
"noPipeline": "此 MR 没有运行任何管道。",
"none": "没有任何",
- "pipelineJob": "{{pipelineJobCount}} 职位",
+ "pipelineJob": "{{pipelineJobCount}} 个作业",
"pipeline": "管道",
"fileContents": "文件内容不可用。",
"binary": "二进制文件。",
@@ -70,7 +70,7 @@
"view": "查看",
"hide": "隐藏",
"changed": "已更改文件",
- "noChecks": "没有找到检查。",
+ "noChecks": "未找到检查项。",
"rerunAll": "重新运行全部",
"rerunFailed": "重新运行失败",
"refresh": "刷新",
@@ -194,8 +194,8 @@
"createIssueSelectedRepository": "在选定的存储库中创建议题。",
"sortTasks": "排序任务",
"displayProperties": "显示属性",
- "orderLinearIssues": "阶 Linear 议题",
- "groupLinearIssues": "群 Linear 议题",
+ "orderLinearIssues": "排序 Linear 议题",
+ "groupLinearIssues": "对 Linear 议题分组",
"linearView": "Linear 视图",
"noStates": "没有可用的状态",
"loadingStates": "加载状态...",
@@ -272,7 +272,7 @@
"display": "展示",
"tasks": "任务",
"allWorkspaces": "所有工作区",
- "viewOrder": "查看订单",
+ "viewOrder": "视图顺序",
"send": "发送评论",
"addComment": "添加评论",
"failedUpdateIssueSource": "无法更新议题来源",
@@ -316,7 +316,7 @@
"failedSave": "无法保存稀疏预设。",
"failedSearch": "搜索分支失败。",
"failedLoadSparse": "无法加载稀疏预设。",
- "openShell": "打开一个外壳",
+ "openShell": "打开 shell",
"failedLoadIssue": "无法加载议题类型",
"failedLoadAssignees": "无法加载负责人",
"failedLoadLabels": "加载标签失败",
@@ -383,7 +383,7 @@
"sortGithub": "排序:{{githubProjectSortLabel}}",
"fieldsGithub": "字段:{{githubProjectFieldsLabel}}",
"groupLinearGroup": "组:{{linearGroupLabel}}",
- "orderLinearOrder": "订单:{{linearOrderLabel}}",
+ "orderLinearOrder": "顺序:{{linearOrderLabel}}",
"preferred": "{{repoName}} 的首选议题源上游不可用。使用原点。",
"plus": "+{{githubProjectSummaryFieldCount}} 字段",
"baseBranch": "进入 {{baseBranch}}",
@@ -482,7 +482,7 @@
"failedReopen": "无法重新打开线程",
"failedResolve": "无法解析线程",
"failedSync": "无法将查看的状态与 GitHub 同步。",
- "invalid": "无效检查响应",
+ "invalid": "检查响应无效",
"failedUpdateGitHubStatus": "无法更新 GitHub 状态",
"failedUpdateGitHubItem": "无法更新 GitHub 项目",
"selectedAgent": "所选 agent 已禁用。创建之前选择启用的 agent。",
@@ -765,22 +765,22 @@
"start": "行首",
"reverse": "反向搜索",
"suspend": "暂停进程",
- "clearScreen": "清晰的屏幕",
+ "clearScreen": "清除屏幕",
"send": "发送文件尾",
- "interrupt": "中断 terminal",
+ "interrupt": "中断终端",
"arrowRight": "向右箭头",
"arrowLeft": "向左箭头",
"arrowDown": "向下箭头",
"arrowUp": "向上箭头",
- "forward": "转发删除",
- "del": "德尔",
+ "forward": "向前删除",
+ "del": "Del",
"backspace": "退格键",
- "space": "空间",
- "shiftTab": "切换选项卡",
- "enter": "进入",
- "tab": "选项卡",
- "escape": "逃脱",
- "esc": "Esc键"
+ "space": "空格键",
+ "shiftTab": "Shift+Tab 键",
+ "enter": "Enter 键",
+ "tab": "Tab 键",
+ "escape": "Esc 键",
+ "esc": "Esc"
},
"terminalWebviewEngineErrorState": {
"reload": "重新加载",
@@ -797,19 +797,19 @@
"terminalAccessoryKeys": {
"pageDown": "向下翻页",
"pageUp": "向上翻页",
- "end": "结尾",
- "home": "家",
+ "end": "End 键",
+ "home": "Home 键",
"arrowRight": "向右箭头",
"arrowLeft": "向左箭头",
"arrowDown": "向下箭头",
"arrowUp": "向上箭头",
- "insert": "插入",
- "pgDn": "前列腺素",
- "pgUp": "上页",
- "ins": "INS",
- "shift": "转移",
- "alt": "替代",
- "ctrl": "控制键"
+ "insert": "Insert 键",
+ "pgDn": "PgDn",
+ "pgUp": "PgUp",
+ "ins": "Ins",
+ "shift": "Shift",
+ "alt": "Alt",
+ "ctrl": "Ctrl"
},
"terminalWebviewReadyWatchdog": {
"terminal": "Terminal 未初始化 - Terminal 视图中没有就绪信号"
@@ -977,8 +977,8 @@
"resolveStage": "在创建拉取请求之前解决或暂存更改。",
"resolveConflicts": "在创建拉取请求之前解决冲突。",
"creating": "正在创建评论...",
- "force": "用租约强制推动...",
- "pushing": "推动 commits...",
+ "force": "正在使用租约强制推送...",
+ "pushing": "正在推送 commits...",
"publishing": "正在发布分支...",
"committing": "正在提交更改...",
"generating": "正在生成 commit 消息...",
@@ -1528,7 +1528,7 @@
"label": "标签",
"add": "添加",
"more": "更多按键 — Tab、箭头、F1–F12...",
- "key": "标识",
+ "key": "按键",
"modifiers": "修饰符",
"show": "显示、隐藏或重新排序快捷键",
"manage": "管理快捷键",
@@ -1541,8 +1541,8 @@
"navigation": "导航",
"editing": "编辑",
"eGPnpm": "例如:pnpm build",
- "eGBuild": "例如建造",
- "pick": "选择一把钥匙"
+ "eGBuild": "例如:构建",
+ "pick": "选择按键"
},
"dragReorderList": {
"moveDown": "下移",
@@ -1703,8 +1703,8 @@
"strike": "删除线",
"italic": "斜体",
"bold": "粗体",
- "h2": "氢2",
- "body": "身体"
+ "h2": "H2",
+ "body": "正文"
},
"mobileWorkspaceNameInput": {
"workspace": "工作区名称"
@@ -1921,7 +1921,7 @@
"inspect": "检查详细信息或启动 AI 修复过程。",
"rerun": "重新运行失败的检查",
"checks": "检查项",
- "failingCheckCountFailingCheck": "{{failingCheckCount}} 检查失败",
+ "failingCheckCountFailingCheck": "{{failingCheckCount}} 项检查失败",
"failingCheckCountFailingChecks": "{{failingCheckCount}} 检查失败",
"failed": "无法加载检查详细信息"
},
@@ -2100,7 +2100,7 @@
},
"mobileFilePreviewScreen": {
"unableLoad": "无法加载预览",
- "save": "保存 terminal 神器",
+ "save": "保存终端产物",
"back": "返回文件",
"unsaved": "未保存的编辑将会丢失。",
"discardChanges": "放弃更改?",
diff --git a/mobile/src/terminal/terminal-accessory-key-catalog.ts b/mobile/src/terminal/terminal-accessory-key-catalog.ts
index f63bdbbf8bc..d2140e69b09 100644
--- a/mobile/src/terminal/terminal-accessory-key-catalog.ts
+++ b/mobile/src/terminal/terminal-accessory-key-catalog.ts
@@ -1,35 +1,37 @@
-import { t } from '@/i18n/mobile-i18n'
+import { mobileI18n, t } from '@/i18n/mobile-i18n'
import type { TerminalAccessoryKey } from './terminal-accessory-keys'
+const canonicalTerminalLabel = mobileI18n.getFixedT('en')
+
export const TERMINAL_ACCESSORY_KEYS: TerminalAccessoryKey[] = [
{
id: 'escape',
- label: t('terminalAccessoryKeyCatalog.esc'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.esc'),
bytes: '\x1b',
accessibilityLabel: t('terminalAccessoryKeyCatalog.escape')
},
{
id: 'tab',
- label: t('terminalAccessoryKeyCatalog.tab'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.tab'),
bytes: '\t',
accessibilityLabel: t('terminalAccessoryKeyCatalog.tab')
},
{
id: 'enter',
- label: t('terminalAccessoryKeyCatalog.enter'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.enter'),
bytes: '\r',
accessibilityLabel: t('terminalAccessoryKeyCatalog.enter')
},
// Why: terminal apps recognize ESC [ Z as the reverse-tab sequence.
{
id: 'shiftTab',
- label: t('terminalAccessoryKeyCatalog.shiftPlus'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.shiftPlus'),
bytes: '\x1b[Z',
accessibilityLabel: t('terminalAccessoryKeyCatalog.shiftTab')
},
{
id: 'space',
- label: t('terminalAccessoryKeyCatalog.space'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.space'),
bytes: ' ',
accessibilityLabel: t('terminalAccessoryKeyCatalog.space')
},
@@ -42,7 +44,7 @@ export const TERMINAL_ACCESSORY_KEYS: TerminalAccessoryKey[] = [
},
{
id: 'delete',
- label: t('terminalAccessoryKeyCatalog.del'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.del'),
bytes: '\x1b[3~',
accessibilityLabel: t('terminalAccessoryKeyCatalog.forward'),
repeatable: true
@@ -77,55 +79,55 @@ export const TERMINAL_ACCESSORY_KEYS: TerminalAccessoryKey[] = [
},
{
id: 'ctrlC',
- label: t('terminalAccessoryKeyCatalog.ctrlPlusC'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.ctrlPlusC'),
bytes: '\x03',
accessibilityLabel: t('terminalAccessoryKeyCatalog.interrupt')
},
{
id: 'ctrlD',
- label: t('terminalAccessoryKeyCatalog.ctrlPlusD'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.ctrlPlusD'),
bytes: '\x04',
accessibilityLabel: t('terminalAccessoryKeyCatalog.send')
},
{
id: 'ctrlL',
- label: t('terminalAccessoryKeyCatalog.ctrlPlusL'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.ctrlPlusL'),
bytes: '\x0c',
accessibilityLabel: t('terminalAccessoryKeyCatalog.clearScreen')
},
{
id: 'ctrlZ',
- label: t('terminalAccessoryKeyCatalog.ctrlPlusZ'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.ctrlPlusZ'),
bytes: '\x1a',
accessibilityLabel: t('terminalAccessoryKeyCatalog.suspend')
},
{
id: 'ctrlR',
- label: t('terminalAccessoryKeyCatalog.ctrlPlusR'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.ctrlPlusR'),
bytes: '\x12',
accessibilityLabel: t('terminalAccessoryKeyCatalog.reverse')
},
{
id: 'ctrlA',
- label: t('terminalAccessoryKeyCatalog.ctrlPlus'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.ctrlPlus'),
bytes: '\x01',
accessibilityLabel: t('terminalAccessoryKeyCatalog.start')
},
{
id: 'ctrlE',
- label: t('terminalAccessoryKeyCatalog.ctrlPlusE'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.ctrlPlusE'),
bytes: '\x05',
accessibilityLabel: t('terminalAccessoryKeyCatalog.end')
},
{
id: 'ctrlW',
- label: t('terminalAccessoryKeyCatalog.ctrlPlusW'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.ctrlPlusW'),
bytes: '\x17',
accessibilityLabel: t('terminalAccessoryKeyCatalog.delete')
},
{
id: 'ctrlU',
- label: t('terminalAccessoryKeyCatalog.ctrlPlusU'),
+ label: canonicalTerminalLabel('terminalAccessoryKeyCatalog.ctrlPlusU'),
bytes: '\x15',
accessibilityLabel: t('terminalAccessoryKeyCatalog.clearLine')
}