mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
fix(mobile): harden localization review gates
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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 <Text>{renderers.lookup()}</Text>
|
||||
}
|
||||
`
|
||||
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 <Text>{value}</Text>
|
||||
}
|
||||
`
|
||||
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 = <Text>{t('Namespace raw copy')}</Text>
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -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: '无法加载检查详细信息'
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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']])
|
||||
})
|
||||
})
|
||||
@@ -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))
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 でセッションを開く",
|
||||
|
||||
@@ -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": "음성 받아쓰기 활성화",
|
||||
|
||||
@@ -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": "放弃更改?",
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user