orca/config/scripts/bootstrap-localization-stat...

248 lines
9.3 KiB
JavaScript

import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { main as bootstrapLocalizationState } from './bootstrap-localization-state.mjs'
import { sourceHash } from './localization-catalog-model.mjs'
import { shouldPreserveEnglishValue } from './locale-translation-policy.mjs'
const LOCALES = ['es', 'ja', 'ko', 'zh']
const MIGRATION_ARGS = ['--migration-rewrite-locales']
const MIGRATION_ENVIRONMENT = { ORCA_I18N_MIGRATION_REWRITE: '1' }
function writeJson(filePath, value) {
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`)
}
function runGit(root, args) {
return execFileSync('git', args, { cwd: root, encoding: 'utf8' })
}
function commitAll(root, subject) {
runGit(root, ['add', '.'])
runGit(root, [
'-c',
'user.name=Orca Test',
'-c',
'user.email=orca@example.com',
'commit',
'-m',
subject
])
}
describe('bootstrap-localization-state', () => {
it('keeps the mobile Continue action translatable despite the product-name policy', () => {
expect(
shouldPreserveEnglishValue('Continue', 'auto.components.mobile.MobileHero.a8fb43cf1c')
).toBe(false)
})
it('classifies provenance deterministically and removes only fallback leaves', async () => {
const root = mkdtempSync(path.join(tmpdir(), 'orca-localization-bootstrap-'))
const localesDir = path.join(root, 'src', 'renderer', 'src', 'i18n', 'locales')
mkdirSync(localesDir, { recursive: true })
mkdirSync(path.join(root, 'config'), { recursive: true })
const enCatalog = {
auto: {
example: {
filler: 'Hello',
machine: 'Welcome',
reviewed: 'Explore Orca'
},
lib: { agent: { catalog: { codex: 'Codex' } } }
}
}
writeJson(path.join(localesDir, 'en.json'), enCatalog)
for (const locale of LOCALES) {
writeJson(path.join(localesDir, `${locale}.json`), {
auto: {
example: {
filler: 'Hello',
machine: `translated-${locale}`,
reviewed: locale === 'es' ? 'Explorar Orca' : `translated-review-${locale}`
},
lib: { agent: { catalog: { codex: 'Codex' } } }
}
})
}
writeJson(path.join(root, 'config', 'localization-reviewed-corrections.json'), {
version: 3,
algorithm: 'classified-first-parent-target-only-current-value-v2',
provenance: [],
messages: Object.fromEntries(LOCALES.map((locale) => [locale, {}]))
})
await bootstrapLocalizationState(root, MIGRATION_ARGS, {
environment: MIGRATION_ENVIRONMENT
})
const firstState = readFileSync(
path.join(root, 'config', 'localization-state', 'es.json'),
'utf8'
)
await bootstrapLocalizationState(root, MIGRATION_ARGS, {
environment: MIGRATION_ENVIRONMENT
})
expect(readFileSync(path.join(root, 'config', 'localization-state', 'es.json'), 'utf8')).toBe(
firstState
)
const state = JSON.parse(firstState)
expect(state.messages['auto.example.filler']).toBeUndefined()
expect(state.messages['auto.lib.agent.catalog.codex']).toEqual({
state: 'intentional-english'
})
expect(state.messages['auto.example.machine']).toEqual({
state: 'machine',
sourceHash: sourceHash('Welcome')
})
expect(state.messages['auto.example.reviewed']).toEqual({
state: 'reviewed',
sourceHash: sourceHash('Explore Orca')
})
const target = JSON.parse(readFileSync(path.join(localesDir, 'es.json'), 'utf8'))
expect(target.auto.example).toEqual({
machine: 'translated-es',
reviewed: 'Explorar Orca'
})
expect(target.auto.lib).toBeUndefined()
})
it('preserves reviewed target-only history for #6749 and #6706 fixtures', async () => {
const root = mkdtempSync(path.join(tmpdir(), 'orca-localization-history-'))
const localesDir = path.join(root, 'src', 'renderer', 'src', 'i18n', 'locales')
mkdirSync(localesDir, { recursive: true })
mkdirSync(path.join(root, 'config'), { recursive: true })
runGit(root, ['init'])
writeJson(path.join(localesDir, 'en.json'), {
auto: {
lib: { worktree: { palette: { search: { ca40ffcbec: 'PR' } } } },
components: {
right: { sidebar: { FileExplorerRow: { '2de3b21934': 'Explorer' } } }
}
}
})
for (const locale of LOCALES) {
writeJson(path.join(localesDir, `${locale}.json`), {
auto: {
lib: {
worktree: {
palette: { search: { ca40ffcbec: locale === 'es' ? 'relaciones públicas' : 'PR' } }
}
},
components: {
right: {
sidebar: {
FileExplorerRow: {
'2de3b21934': locale === 'zh' ? '探险家' : `Explorer-${locale}`
}
}
}
}
}
})
}
commitAll(root, 'initial locale import')
const es = JSON.parse(readFileSync(path.join(localesDir, 'es.json'), 'utf8'))
es.auto.lib.worktree.palette.search.ca40ffcbec = 'PR'
writeJson(path.join(localesDir, 'es.json'), es)
commitAll(
root,
'fix(i18n): correct Spanish "PR" mistranslated as "relaciones públicas" (#6749)'
)
const zh = JSON.parse(readFileSync(path.join(localesDir, 'zh.json'), 'utf8'))
zh.auto.components.right.sidebar.FileExplorerRow['2de3b21934'] = '资源管理器'
writeJson(path.join(localesDir, 'zh.json'), zh)
commitAll(root, 'fix(i18n): correct zh "Explorer" mistranslation (探险家 → 资源管理器) (#6706)')
const zhCommit = runGit(root, ['rev-parse', 'HEAD']).trim()
const esCommit = runGit(root, ['rev-parse', 'HEAD^']).trim()
const ja = JSON.parse(readFileSync(path.join(localesDir, 'ja.json'), 'utf8'))
ja.auto.components.right.sidebar.FileExplorerRow['2de3b21934'] = 'エクスプローラー'
writeJson(path.join(localesDir, 'ja.json'), ja)
commitAll(root, 'Apply bulk spacing and translation normalization')
const bulkCommit = runGit(root, ['rev-parse', 'HEAD']).trim()
const classifications = {
version: 1,
commits: {
[esCommit]: {
classification: 'reviewed-correction',
reason: 'Single Spanish correction review.'
},
[zhCommit]: {
classification: 'reviewed-correction',
reason: 'Single Chinese correction review.'
},
[bulkCommit]: {
classification: 'mechanical-or-bulk',
reason: 'Synthetic bulk spacing pass.'
}
}
}
writeJson(
path.join(root, 'config', 'localization-reviewed-commit-classifications.json'),
classifications
)
writeJson(path.join(root, 'config', 'localization-reviewed-corrections.json'), {
version: 3,
algorithm: 'classified-first-parent-target-only-current-value-v2',
provenance: [],
messages: Object.fromEntries(LOCALES.map((locale) => [locale, {}]))
})
const initialGitProcesses = []
await bootstrapLocalizationState(root, [...MIGRATION_ARGS, '--audit-reviewed-commits'], {
environment: MIGRATION_ENVIRONMENT,
onGitProcess: (args) => initialGitProcesses.push(args)
})
expect(initialGitProcesses).toHaveLength(3)
const audit = JSON.parse(
readFileSync(path.join(root, 'config', 'localization-reviewed-corrections.json'), 'utf8')
)
expect(audit.messages.es['auto.lib.worktree.palette.search.ca40ffcbec'].value).toBe('PR')
expect(
audit.messages.zh['auto.components.right.sidebar.FileExplorerRow.2de3b21934'].value
).toBe('资源管理器')
expect(
audit.messages.ja['auto.components.right.sidebar.FileExplorerRow.2de3b21934']
).toBeUndefined()
expect(
JSON.parse(readFileSync(path.join(root, 'config', 'localization-state', 'es.json'), 'utf8'))
.messages['auto.lib.worktree.palette.search.ca40ffcbec'].state
).toBe('reviewed')
expect(
JSON.parse(readFileSync(path.join(root, 'config', 'localization-state', 'zh.json'), 'utf8'))
.messages['auto.components.right.sidebar.FileExplorerRow.2de3b21934'].state
).toBe('reviewed')
expect(
JSON.parse(readFileSync(path.join(root, 'config', 'localization-state', 'ja.json'), 'utf8'))
.messages['auto.components.right.sidebar.FileExplorerRow.2de3b21934'].state
).toBe('machine')
for (let index = 0; index < 12; index += 1) {
const growingEs = JSON.parse(readFileSync(path.join(localesDir, 'es.json'), 'utf8'))
growingEs.auto.lib.worktree.palette.search.ca40ffcbec = `PR-${index}`
writeJson(path.join(localesDir, 'es.json'), growingEs)
commitAll(root, `grow localization history ${index}`)
classifications.commits[runGit(root, ['rev-parse', 'HEAD']).trim()] = {
classification: 'mechanical-or-bulk',
reason: 'Synthetic bulk history growth.'
}
}
const grownGitProcesses = []
writeJson(
path.join(root, 'config', 'localization-reviewed-commit-classifications.json'),
classifications
)
await bootstrapLocalizationState(root, [...MIGRATION_ARGS, '--audit-reviewed-commits'], {
environment: MIGRATION_ENVIRONMENT,
onGitProcess: (args) => grownGitProcesses.push(args)
})
expect(grownGitProcesses).toHaveLength(initialGitProcesses.length)
})
})