diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..6c76a2fe4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# Product source remains owned by its normal reviewers; localization inputs need focused review. +/src/renderer/src/i18n/locales/ @brennanb2025 +/config/scripts/*localization*.mjs @brennanb2025 +/config/scripts/*locale*.mjs @brennanb2025 +/i18next.config.ts @brennanb2025 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7723e28a7..4888dd6f3 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -62,6 +62,11 @@ jobs: - name: Verify localization catalog run: pnpm run verify:localization-catalog + # Why: extraction writes sorted evidence to an isolated temporary path, + # so feature PRs need one normalized AST pass rather than a three-OS matrix. + - name: Verify localization extraction + run: pnpm run verify:localization-extraction + - name: Verify localization coverage run: pnpm run verify:localization-coverage diff --git a/config/localization-audit.md b/config/localization-audit.md index a90475042..2090dcbb7 100644 --- a/config/localization-audit.md +++ b/config/localization-audit.md @@ -52,11 +52,27 @@ Sync catalog keys after adding or removing `translate(...)` calls: pnpm run sync:localization-catalog ``` -The sync command adds missing `en.json` entries from each call's string fallback, -copies untranslated English placeholders into other locale catalogs to keep -parity, removes locale entries whose English key was deleted, and repairs -placeholder mismatches. Run the machine-translation bootstrap commands only when -refreshing real translations, not for ordinary UI copy changes. +The sync command adds missing `en.json` entries from each call's string fallback. +It never edits target catalogs: missing values remain absent and use the existing +runtime English fallback. Existing placeholder mismatches fail validation until +a localization PR fixes or retires the target entry. + +Run maintained source extraction without committing a second English catalog: + +```sh +pnpm run verify:localization-extraction +``` + +Extraction fails when a statically extracted key is absent from `en.json` or an +inline default has incompatible placeholders. Existing unreferenced English keys +and wording-only fallback drift are reported as migration debt; the permanent +bilingual translation source will reconcile them without a large disposition +database. Reviewed and stale translation state is likewise deferred to that +source rather than inferred permanently from Git history. + +The legacy free-endpoint bootstrap and whole-catalog repair scripts intentionally +have no package-script entry points. Ordinary product and localization work must +not invoke tools that can overwrite an entire target catalog. The coverage gate compares current candidates against `config/localization-coverage-allowlist.json`. The committed allowlist is empty: @@ -97,8 +113,8 @@ Recommended migration order: The final gate should combine three checks: 1. Scanner coverage: no unclassified localizable candidates remain. -2. Catalog coverage: every supported locale has the same keys as English, with - matching interpolation variables. +2. Catalog correctness: existing translations have matching interpolation + variables; missing target entries are reported rather than rejected. 3. Runtime coverage: pseudo-localization and real locale smoke tests show no obvious English leftovers or layout clipping in core screens. diff --git a/config/scripts/localization-package-contract.test.mjs b/config/scripts/localization-package-contract.test.mjs new file mode 100644 index 000000000..bc3bb05b0 --- /dev/null +++ b/config/scripts/localization-package-contract.test.mjs @@ -0,0 +1,22 @@ +import { readFileSync } from 'node:fs' + +import { describe, expect, it } from 'vitest' + +describe('localization package scripts', () => { + const scripts = JSON.parse(readFileSync('package.json', 'utf8')).scripts + + it('keeps safe catalog and extraction verification available', () => { + expect(scripts['verify:localization-catalog']).toBeDefined() + expect(scripts['sync:localization-catalog']).toBeDefined() + expect(scripts['verify:localization-extraction']).toBeDefined() + }) + + it('does not expose whole-catalog translation and repair commands', () => { + expect(scripts['bootstrap:locale-catalog']).toBeUndefined() + expect(scripts['bootstrap:zh-catalog']).toBeUndefined() + expect(scripts['bootstrap:ko-catalog']).toBeUndefined() + expect(scripts['bootstrap:ja-catalog']).toBeUndefined() + expect(scripts['bootstrap:es-catalog']).toBeUndefined() + expect(scripts['repair:locale-catalog']).toBeUndefined() + }) +}) diff --git a/config/scripts/verify-localization-catalog.mjs b/config/scripts/verify-localization-catalog.mjs index 1c16279a2..901c65179 100644 --- a/config/scripts/verify-localization-catalog.mjs +++ b/config/scripts/verify-localization-catalog.mjs @@ -227,44 +227,6 @@ function setCatalogEntry(catalog, key, value) { cursor[parts.at(-1)] = value } -function deleteCatalogEntry(catalog, key) { - const parts = key.split('.') - const stack = [] - let cursor = catalog - - for (const part of parts.slice(0, -1)) { - if ( - typeof cursor?.[part] !== 'object' || - cursor[part] === null || - Array.isArray(cursor[part]) - ) { - return false - } - stack.push([cursor, part]) - cursor = cursor[part] - } - - const leafKey = parts.at(-1) - if (!Object.hasOwn(cursor, leafKey)) { - return false - } - - delete cursor[leafKey] - for (let index = stack.length - 1; index >= 0; index -= 1) { - const [parent, part] = stack[index] - const child = parent[part] - if ( - typeof child === 'object' && - child !== null && - !Array.isArray(child) && - Object.keys(child).length === 0 - ) { - delete parent[part] - } - } - return true -} - function collectLocaleParityIssues(enCatalog, localeCatalog) { const enEntries = flattenCatalogEntries(enCatalog) const localeEntries = flattenCatalogEntries(localeCatalog) @@ -286,30 +248,6 @@ function collectLocaleParityIssues(enCatalog, localeCatalog) { return { enEntries, localeEntries, missingInLocale, extraInLocale, interpolationMismatches } } -function repairLocaleParity(enCatalog, localeCatalog) { - const { enEntries, missingInLocale, extraInLocale, interpolationMismatches } = - collectLocaleParityIssues(enCatalog, localeCatalog) - let changed = 0 - - for (const key of missingInLocale) { - setCatalogEntry(localeCatalog, key, enEntries.get(key)) - changed += 1 - } - - for (const key of extraInLocale) { - if (deleteCatalogEntry(localeCatalog, key)) { - changed += 1 - } - } - - for (const key of interpolationMismatches) { - setCatalogEntry(localeCatalog, key, enEntries.get(key)) - changed += 1 - } - - return changed -} - function referencesMissingFallbacks(missing) { return missing.filter((reference) => typeof reference.fallback !== 'string') } @@ -344,23 +282,18 @@ function applyMissingEnglishEntries(catalog, missing) { return changed } -function verifyLocaleParity(enCatalog, localeName, localeCatalog) { - const { localeEntries, missingInLocale, extraInLocale, interpolationMismatches } = +function verifyLocaleCatalog(enCatalog, localeName, localeCatalog) { + const { enEntries, localeEntries, missingInLocale, extraInLocale, interpolationMismatches } = collectLocaleParityIssues(enCatalog, localeCatalog) - if ( - missingInLocale.length > 0 || - extraInLocale.length > 0 || - interpolationMismatches.length > 0 - ) { - console.error(`Locale catalog parity failed for ${localeName}.json.`) - if (missingInLocale.length > 0) { - console.error('') - console.error(formatMissingKeys('missing', missingInLocale.slice(0, 20))) - if (missingInLocale.length > 20) { - console.error(`...and ${missingInLocale.length - 20} more missing keys`) - } - } + // Why: feature PRs own English declarations; absent target leaves deliberately + // use i18next's existing English fallback until a localization PR supplies them. + console.log( + `${localeName}.json coverage: ${enEntries.size - missingInLocale.length}/${enEntries.size} translated, ${missingInLocale.length} missing.` + ) + + if (extraInLocale.length > 0 || interpolationMismatches.length > 0) { + console.error(`Locale catalog validation failed for ${localeName}.json.`) if (extraInLocale.length > 0) { console.error('') console.error(formatMissingKeys('extra', extraInLocale.slice(0, 20))) @@ -380,7 +313,7 @@ function verifyLocaleParity(enCatalog, localeName, localeCatalog) { return 1 } - console.log(`Verified locale parity for ${localeName}.json (${localeEntries.size} keys).`) + console.log(`Verified ${localeEntries.size} existing ${localeName}.json entries.`) return 0 } @@ -527,19 +460,10 @@ export async function main(root = process.cwd(), options = parseArgs(process.arg const localeName = fileName.replace(/\.json$/, '') const localeCatalogPath = path.join(localesDir, fileName) const localeCatalog = JSON.parse(await fs.readFile(localeCatalogPath, 'utf8')) - if (options.fix) { - const repaired = repairLocaleParity(catalog, localeCatalog) - if (repaired > 0) { - await fs.writeFile(localeCatalogPath, `${JSON.stringify(localeCatalog, null, 2)}\n`, 'utf8') - console.log(`Repaired ${fileName} parity (${repaired} key update(s)).`) - } - } - const exitCode = verifyLocaleParity(catalog, localeName, localeCatalog) + const exitCode = verifyLocaleCatalog(catalog, localeName, localeCatalog) if (exitCode !== 0) { - if (!options.fix) { - console.error('') - console.error('Run `pnpm run sync:localization-catalog` to repair locale parity.') - } + console.error('') + console.error('Fix or retire the existing target entry in a localization PR.') return exitCode } } diff --git a/config/scripts/verify-localization-catalog.test.mjs b/config/scripts/verify-localization-catalog.test.mjs index 14a41fcef..89e24bc0c 100644 --- a/config/scripts/verify-localization-catalog.test.mjs +++ b/config/scripts/verify-localization-catalog.test.mjs @@ -33,7 +33,7 @@ function makeProject({ sourceText, enCatalog = {}, esCatalog = {} }) { } describe('verify-localization-catalog', () => { - it('bootstraps missing catalog entries from string fallbacks', async () => { + it('bootstraps English entries without fabricating target translations', async () => { const { root, localesDir } = makeProject({ sourceText: "import { translate } from '@/i18n/i18n'\nexport const label = translate('auto.example.greeting', 'Hello {{name}}', { name: 'Orca' })\n" @@ -45,12 +45,10 @@ describe('verify-localization-catalog', () => { expect(readJson(path.join(localesDir, 'en.json'))).toEqual({ auto: { example: { greeting: 'Hello {{name}}' } } }) - expect(readJson(path.join(localesDir, 'es.json'))).toEqual({ - auto: { example: { greeting: 'Hello {{name}}' } } - }) + expect(readJson(path.join(localesDir, 'es.json'))).toEqual({}) }) - it('repairs stale locale keys and interpolation mismatches', async () => { + it('never overwrites mismatched translations or removes target-only entries', async () => { const { root, localesDir } = makeProject({ sourceText: "import { translate } from '@/i18n/i18n'\nexport const label = translate('auto.example.greeting', 'Hello {{name}}', { name: 'Orca' })\n", @@ -63,13 +61,29 @@ describe('verify-localization-catalog', () => { } }) - await expect(verifyLocalizationCatalog(root, { fix: true })).resolves.toBe(0) + await expect(verifyLocalizationCatalog(root, { fix: true })).resolves.toBe(1) expect(readJson(path.join(localesDir, 'es.json'))).toEqual({ - auto: { example: { greeting: 'Hello {{name}}' } } + auto: { + example: { greeting: 'Hola' }, + stale: { removed: 'Viejo' } + } }) }) + it('accepts sparse target catalogs when existing placeholders match', async () => { + const { root } = makeProject({ + sourceText: + "import { translate } from '@/i18n/i18n'\nexport const label = translate('auto.example.greeting', 'Hello {{name}}', { name: 'Orca' })\n", + enCatalog: { + auto: { example: { greeting: 'Hello {{name}}', untranslated: 'English only' } } + }, + esCatalog: { auto: { example: { greeting: 'Hola {{name}}' } } } + }) + + await expect(verifyLocalizationCatalog(root, { fix: false })).resolves.toBe(0) + }) + it('does not invent values for keys without string fallbacks', async () => { const { root, localesDir } = makeProject({ sourceText: diff --git a/config/scripts/verify-localization-extraction.mjs b/config/scripts/verify-localization-extraction.mjs new file mode 100644 index 000000000..df101e652 --- /dev/null +++ b/config/scripts/verify-localization-extraction.mjs @@ -0,0 +1,123 @@ +import { execFile } from 'node:child_process' +import fs from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import process from 'node:process' +import { promisify } from 'node:util' +import { pathToFileURL } from 'node:url' + +const execFileAsync = promisify(execFile) +const EN_CATALOG_PATH = path.join('src', 'renderer', 'src', 'i18n', 'locales', 'en.json') +const PLACEHOLDER_RE = /\{\{[^}]+\}\}/g + +function flattenCatalog(value, prefix = '', entries = new Map()) { + if (typeof value === 'string') { + entries.set(prefix, value) + return entries + } + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + return entries + } + for (const [key, child] of Object.entries(value)) { + flattenCatalog(child, prefix ? `${prefix}.${key}` : key, entries) + } + return entries +} + +function placeholders(value) { + return [...(value.match(PLACEHOLDER_RE) ?? [])].sort().join('|') +} + +export function compareExtraction(extractedCatalog, englishCatalog) { + const extracted = flattenCatalog(extractedCatalog) + const english = flattenCatalog(englishCatalog) + const dynamicDefaults = [...extracted.entries()] + .filter(([, value]) => value.length === 0) + .map(([key]) => key) + const missingFromEnglish = [...extracted.keys()].filter((key) => !english.has(key)) + const orphans = [...english.keys()].filter((key) => !extracted.has(key)) + const fallbackDrift = [] + const placeholderMismatches = [] + + for (const [key, extractedValue] of extracted) { + const englishValue = english.get(key) + if ( + extractedValue.length === 0 || + englishValue === undefined || + englishValue === extractedValue + ) { + continue + } + fallbackDrift.push(key) + if (placeholders(extractedValue) !== placeholders(englishValue)) { + placeholderMismatches.push(key) + } + } + + return { + extracted, + dynamicDefaults, + missingFromEnglish, + orphans, + fallbackDrift, + placeholderMismatches + } +} + +function printKeys(label, keys) { + if (keys.length === 0) { + return + } + console.error(`${label}:`) + for (const key of keys.slice(0, 20)) { + console.error(` ${key}`) + } + if (keys.length > 20) { + console.error(` ...and ${keys.length - 20} more`) + } +} + +async function extractToTemporaryCatalog(root, tempDir) { + const cliPath = path.join(root, 'node_modules', 'i18next-cli', 'dist', 'esm', 'cli.js') + const outputPattern = path.join(tempDir, '{{language}}.json') + // Why: extraction output is evidence for this check, not another committed + // catalog that feature authors must keep synchronized. + await execFileAsync(process.execPath, [cliPath, 'extract', '--sync-primary', '--quiet'], { + cwd: root, + env: { + ...process.env, + ORCA_I18N_EXTRACTION_OUTPUT: outputPattern.split(path.sep).join('/') + } + }) + return JSON.parse(await fs.readFile(path.join(tempDir, 'en.json'), 'utf8')) +} + +export async function main(root = process.cwd()) { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'orca-i18next-extraction-')) + + try { + const [extractedCatalog, englishCatalog] = await Promise.all([ + extractToTemporaryCatalog(root, tempDir), + fs.readFile(path.join(root, EN_CATALOG_PATH), 'utf8').then(JSON.parse) + ]) + const result = compareExtraction(extractedCatalog, englishCatalog) + + console.log( + `Extracted ${result.extracted.size} keys; ${result.dynamicDefaults.length} dynamic defaults are report-only, ${result.orphans.length} existing English entries are not statically referenced, and ${result.fallbackDrift.length} inline defaults differ.` + ) + + if (result.missingFromEnglish.length > 0 || result.placeholderMismatches.length > 0) { + printKeys('Extracted keys missing from en.json', result.missingFromEnglish) + printKeys('Extracted defaults with incompatible placeholders', result.placeholderMismatches) + return 1 + } + + return 0 + } finally { + await fs.rm(tempDir, { recursive: true, force: true }) + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exit(await main()) +} diff --git a/config/scripts/verify-localization-extraction.test.mjs b/config/scripts/verify-localization-extraction.test.mjs new file mode 100644 index 000000000..ae7556e93 --- /dev/null +++ b/config/scripts/verify-localization-extraction.test.mjs @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest' + +import { compareExtraction } from './verify-localization-extraction.mjs' + +describe('verify-localization-extraction', () => { + it('reports legacy drift without requiring a committed disposition database', () => { + const result = compareExtraction( + { menu: { open: 'Open now {{name}}' } }, + { menu: { open: 'Open {{name}}', legacy: 'Legacy copy' } } + ) + + expect(result.orphans).toEqual(['menu.legacy']) + expect(result.fallbackDrift).toEqual(['menu.open']) + expect(result.placeholderMismatches).toEqual([]) + }) + + it('reports dynamic defaults without treating empty extractor values as catalog copy', () => { + const result = compareExtraction( + { menu: { dynamic: '' } }, + { menu: { dynamic: '{{count}} items' } } + ) + + expect(result.dynamicDefaults).toEqual(['menu.dynamic']) + expect(result.fallbackDrift).toEqual([]) + expect(result.placeholderMismatches).toEqual([]) + }) + + it('rejects undeclared keys even when their defaults are dynamic', () => { + const result = compareExtraction({ menu: { dynamic: '' } }, {}) + + expect(result.missingFromEnglish).toEqual(['menu.dynamic']) + }) + + it('rejects missing English declarations and incompatible placeholders', () => { + const result = compareExtraction( + { + menu: { + missing: 'Missing', + open: 'Open {{name}}' + } + }, + { menu: { open: 'Open {{path}}' } } + ) + + expect(result.missingFromEnglish).toEqual(['menu.missing']) + expect(result.placeholderMismatches).toEqual(['menu.open']) + }) +}) diff --git a/i18next.config.ts b/i18next.config.ts new file mode 100644 index 000000000..577375bd2 --- /dev/null +++ b/i18next.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'i18next-cli' + +const output = + process.env.ORCA_I18N_EXTRACTION_OUTPUT ?? 'tmp/localization-extraction/{{language}}.json' + +export default defineConfig({ + locales: ['en'], + extract: { + input: ['src/**/*.{js,jsx,ts,tsx,mts,cts}'], + ignore: [ + '**/*.test.*', + '**/*.spec.*', + '**/__tests__/**', + '**/__snapshots__/**', + '**/assets/**' + ], + output, + defaultNS: false, + functions: ['t', '*.t', 'translate', 'translateMain'], + useTranslationNames: ['useTranslation'], + sort: true, + disablePlurals: true, + removeUnusedKeys: true + } +}) diff --git a/package.json b/package.json index c47164875..792cf4aff 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "main": "./out/main/index.js", "scripts": { "format": "oxfmt --write .", - "lint": "oxlint && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:max-lines-ratchet && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalog && pnpm run verify:localization-coverage", + "lint": "oxlint && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:max-lines-ratchet && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalog && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage", "audit:code-quality": "pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run audit:react-doctor", "audit:code-quality:native": "oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings", "audit:code-quality:type-aware": "oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests --deny-warnings", @@ -60,12 +60,7 @@ "verify:cli-bin": "node config/scripts/verify-cli-bin.mjs", "verify:localization-catalog": "node config/scripts/verify-localization-catalog.mjs", "sync:localization-catalog": "node config/scripts/verify-localization-catalog.mjs --fix", - "bootstrap:locale-catalog": "node config/scripts/bootstrap-locale-catalog.mjs", - "bootstrap:zh-catalog": "node config/scripts/bootstrap-zh-catalog.mjs", - "bootstrap:ko-catalog": "node config/scripts/bootstrap-locale-catalog.mjs --locale ko", - "bootstrap:ja-catalog": "node config/scripts/bootstrap-locale-catalog.mjs --locale ja", - "bootstrap:es-catalog": "node config/scripts/bootstrap-locale-catalog.mjs --locale es", - "repair:locale-catalog": "node config/scripts/repair-locale-catalog.mjs", + "verify:localization-extraction": "node config/scripts/verify-localization-extraction.mjs", "verify:localization-coverage": "node config/scripts/audit-localization-coverage.mjs --check", "audit:localization": "node config/scripts/audit-localization-coverage.mjs", "build:cli": "tsc -p config/tsconfig.cli.json --outDir out --composite false --incremental false && node config/scripts/verify-cli-bin.mjs --fix-executable --fix-package-json && node config/scripts/install-dev-cli.mjs", @@ -198,6 +193,7 @@ "happy-dom": "^20.9.0", "html-to-image": "^1.11.13", "husky": "^9.1.7", + "i18next-cli": "1.65.0", "katex": "^0.16.45", "lint-staged": "^16.4.0", "lowlight": "^3.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 177451f71..491c53677 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,7 +248,7 @@ importers: version: 26.15.3(dmg-builder@26.15.3) electron-vite: specifier: ^5.0.0 - version: 5.0.0(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4)) + version: 5.0.0(@swc/core@1.15.46)(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4)) emoji-picker-react: specifier: ^4.19.1 version: 4.19.1(react@19.2.7) @@ -264,6 +264,9 @@ importers: husky: specifier: ^9.1.7 version: 9.1.7 + i18next-cli: + specifier: 1.65.0 + version: 1.65.0(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(typescript@7.0.2) katex: specifier: ^0.16.45 version: 0.16.45 @@ -575,6 +578,12 @@ packages: '@chevrotain/types@11.1.2': resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + '@croct/json5-parser@0.2.2': + resolution: {integrity: sha512-0NJMLrbeLbQ0eCVj3UoH/kG2QckUgOASfwmfDTjyW1xAYPyTNJXcWVT/dssJdTJd0pRchW+qF0VFWQHcxs1OVw==} + + '@croct/json@2.1.0': + resolution: {integrity: sha512-UrWfjNQVlBxN+OVcFwHmkjARMW55MBN04E9KfGac8ac8z1QnFVuiOOFtMWXCk3UwsyRqhsNaFoYLZC+xxqsVjQ==} + '@dnd-kit/accessibility@3.1.1': resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} peerDependencies: @@ -867,6 +876,15 @@ packages: resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/checkbox@5.2.1': + resolution: {integrity: sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/confirm@6.1.1': resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} @@ -885,10 +903,100 @@ packages: '@types/node': optional: true + '@inquirer/editor@5.2.2': + resolution: {integrity: sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@5.1.1': + resolution: {integrity: sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@3.0.3': + resolution: {integrity: sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/figures@2.0.7': resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/input@5.1.2': + resolution: {integrity: sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@4.1.1': + resolution: {integrity: sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@5.1.1': + resolution: {integrity: sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@8.5.2': + resolution: {integrity: sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@5.3.1': + resolution: {integrity: sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@4.2.1': + resolution: {integrity: sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@5.2.1': + resolution: {integrity: sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/type@4.0.7': resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} @@ -2429,6 +2537,99 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/core-darwin-arm64@1.15.46': + resolution: {integrity: sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.46': + resolution: {integrity: sha512-4Tj4ppVIPCmUMpmGFiGtyEriwLyJ+yi/US4WfBrP/ok8COGddDZXLEzQETnKyK46mjvr1v0jevrS23zjoff7vA==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.46': + resolution: {integrity: sha512-i8tUGnNjyOgMmfmgFSg4aeJLQoFyfpIHK5FjpQAwpRyQIqEUB2w1e8zIDQzY1WhOxx8NoS1S5iUL813Un4Sf5A==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.46': + resolution: {integrity: sha512-c0OnhqzdhfOvv6qhNCcByepB+sNYOGZyhtr2Qa6ZCHvAWTYhSRw4j/u92Stue9PbZ/6q74b9nHzi76+kVzqQHQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-arm64-musl@1.15.46': + resolution: {integrity: sha512-imyRpNEcUzFQFV2LE4jL68ErvmKEuZCbvZru77iQREunJ+bR4i658cupTgtG1mLYM3F1Tzy3Sb9xYb02KghWTg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc/core-linux-ppc64-gnu@1.15.46': + resolution: {integrity: sha512-ctEfcl/HcUeomK33cbySiHZm98GEDIxTm1EkpBsYCiHxElYBzvTXVeuQT2YwbUXn9XCrjiw4ipyUNk33k26qRg==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-s390x-gnu@1.15.46': + resolution: {integrity: sha512-DxlMdnt84TtRVTv7WL/thWyz9+QU8QZNNoAP9rrk0P68LziuhfePp8MjQ44zIprpTHTsEwyziIuGUUN5iSC1bQ==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-gnu@1.15.46': + resolution: {integrity: sha512-SKxI7J6t90XPl8hRUqtJi9NfGdunN/E/vZMc7Bc0figeRdOPDBT+Tm8g7cx9xM0T0mewh2l+8dewa3Am27/P+A==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-musl@1.15.46': + resolution: {integrity: sha512-qj9T6B7bosI0VEsrWOVXZN1OXxS8Tp63ywyrLxNdOycnUtLdkgYcoBsN5y8ImnDDsnwrEWZOy1e+J4xSe7mA3Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/core-win32-arm64-msvc@1.15.46': + resolution: {integrity: sha512-8p7l4c3LU+eA5g9Et1JPhNeMC1oQwXTGU+uah8DPIBX7YXzqswvaBtyKVmXefVGi/DJU1x3YJsc3mbAp9aWzSQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.46': + resolution: {integrity: sha512-tUEnfr3Bn9u6FOjUb3PN9p+09qZC2j+wNDLKHzXXZn22rqGcUqR/ohCRSS+nG9B9+X+U+3FewNEHJkTmdIvMjQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.46': + resolution: {integrity: sha512-Vux7UDzBJYQggSuPfcl2w9iu+IJpgpRCxHzgCaVkELnAXAE4XZMOTX9HNcaNiwfeIDqdu2rkr69RuDm6wY8neA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.46': + resolution: {integrity: sha512-Ri3em2mBpq3h2zSPliCYl63otDGqek8PPEfv2nWgRQEbZ/VBCNyypVTVQ6cEbTCXBhy+WE2T3fQb08moIyuYaw==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.27': + resolution: {integrity: sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==} + '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} @@ -3461,6 +3662,13 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -4316,6 +4524,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -4473,6 +4685,15 @@ packages: engines: {node: '>=18'} hasBin: true + i18next-cli@1.65.0: + resolution: {integrity: sha512-sak+2Ry4P7wtl7xMAZg2sWG2vup1lRHFBKA7h5IeEqFUog51QEgeYUhHxd2x85+MvS4BhVOZZs833clnd1WgYA==} + engines: {node: '>=22'} + hasBin: true + + i18next-resources-for-ts@2.1.0: + resolution: {integrity: sha512-n5UexwEVt0OoIAhG2MWpSnAVJW1U8mQrQTmXyxc5DMAx+NLhcLZhSMJo/FnUsA5JQ3obTYqTgB7YIuZKWpDgow==} + hasBin: true + i18next@26.3.1: resolution: {integrity: sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==} peerDependencies: @@ -4515,6 +4736,15 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + inquirer@14.0.2: + resolution: {integrity: sha512-VsSx1JneSNp3ld1veMTLe+UDcUD8Tw2/jjOthhkX3/IX2q+xHhVELifeb/hsb1fBw31pabEPNUf/xUOyb+KZjA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + inspect-webkit@0.0.5: resolution: {integrity: sha512-584wP/2nJO1LX74nqHP2j0tQzlK9ZTi+D0Z9qeLQjtUR/LCMXQHGX8M0vrsqBwTeGakF7q5GMFpg81nxUYlCvw==} hasBin: true @@ -5390,6 +5620,10 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -5693,6 +5927,10 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + recast@0.23.11: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} @@ -5856,6 +6094,10 @@ packages: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} + run-async@4.0.6: + resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} + engines: {node: '>=0.12.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -6543,6 +6785,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -6819,6 +7066,12 @@ snapshots: '@chevrotain/types@11.1.2': {} + '@croct/json5-parser@0.2.2': + dependencies: + '@croct/json': 2.1.0 + + '@croct/json@2.1.0': {} + '@dnd-kit/accessibility@3.1.1(react@19.2.7)': dependencies: react: 19.2.7 @@ -7094,8 +7347,16 @@ snapshots: '@iconify/types': 2.0.0 mlly: 1.8.2 - '@inquirer/ansi@2.0.7': - optional: true + '@inquirer/ansi@2.0.7': {} + + '@inquirer/checkbox@5.2.1(@types/node@25.9.5)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@25.9.5) + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@25.9.5) + optionalDependencies: + '@types/node': 25.9.5 '@inquirer/confirm@6.1.1(@types/node@25.9.5)': dependencies: @@ -7103,7 +7364,6 @@ snapshots: '@inquirer/type': 4.0.7(@types/node@25.9.5) optionalDependencies: '@types/node': 25.9.5 - optional: true '@inquirer/core@11.2.1(@types/node@25.9.5)': dependencies: @@ -7116,15 +7376,95 @@ snapshots: signal-exit: 4.1.0 optionalDependencies: '@types/node': 25.9.5 - optional: true - '@inquirer/figures@2.0.7': - optional: true + '@inquirer/editor@5.2.2(@types/node@25.9.5)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@25.9.5) + '@inquirer/external-editor': 3.0.3(@types/node@25.9.5) + '@inquirer/type': 4.0.7(@types/node@25.9.5) + optionalDependencies: + '@types/node': 25.9.5 + + '@inquirer/expand@5.1.1(@types/node@25.9.5)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@25.9.5) + '@inquirer/type': 4.0.7(@types/node@25.9.5) + optionalDependencies: + '@types/node': 25.9.5 + + '@inquirer/external-editor@3.0.3(@types/node@25.9.5)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 25.9.5 + + '@inquirer/figures@2.0.7': {} + + '@inquirer/input@5.1.2(@types/node@25.9.5)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@25.9.5) + '@inquirer/type': 4.0.7(@types/node@25.9.5) + optionalDependencies: + '@types/node': 25.9.5 + + '@inquirer/number@4.1.1(@types/node@25.9.5)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@25.9.5) + '@inquirer/type': 4.0.7(@types/node@25.9.5) + optionalDependencies: + '@types/node': 25.9.5 + + '@inquirer/password@5.1.1(@types/node@25.9.5)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@25.9.5) + '@inquirer/type': 4.0.7(@types/node@25.9.5) + optionalDependencies: + '@types/node': 25.9.5 + + '@inquirer/prompts@8.5.2(@types/node@25.9.5)': + dependencies: + '@inquirer/checkbox': 5.2.1(@types/node@25.9.5) + '@inquirer/confirm': 6.1.1(@types/node@25.9.5) + '@inquirer/editor': 5.2.2(@types/node@25.9.5) + '@inquirer/expand': 5.1.1(@types/node@25.9.5) + '@inquirer/input': 5.1.2(@types/node@25.9.5) + '@inquirer/number': 4.1.1(@types/node@25.9.5) + '@inquirer/password': 5.1.1(@types/node@25.9.5) + '@inquirer/rawlist': 5.3.1(@types/node@25.9.5) + '@inquirer/search': 4.2.1(@types/node@25.9.5) + '@inquirer/select': 5.2.1(@types/node@25.9.5) + optionalDependencies: + '@types/node': 25.9.5 + + '@inquirer/rawlist@5.3.1(@types/node@25.9.5)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@25.9.5) + '@inquirer/type': 4.0.7(@types/node@25.9.5) + optionalDependencies: + '@types/node': 25.9.5 + + '@inquirer/search@4.2.1(@types/node@25.9.5)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@25.9.5) + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@25.9.5) + optionalDependencies: + '@types/node': 25.9.5 + + '@inquirer/select@5.2.1(@types/node@25.9.5)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@25.9.5) + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@25.9.5) + optionalDependencies: + '@types/node': 25.9.5 '@inquirer/type@4.0.7(@types/node@25.9.5)': optionalDependencies: '@types/node': 25.9.5 - optional: true '@isaacs/fs-minipass@4.0.1': dependencies: @@ -8446,6 +8786,66 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@swc/core-darwin-arm64@1.15.46': + optional: true + + '@swc/core-darwin-x64@1.15.46': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.46': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.46': + optional: true + + '@swc/core-linux-arm64-musl@1.15.46': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.46': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.46': + optional: true + + '@swc/core-linux-x64-gnu@1.15.46': + optional: true + + '@swc/core-linux-x64-musl@1.15.46': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.46': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.46': + optional: true + + '@swc/core-win32-x64-msvc@1.15.46': + optional: true + + '@swc/core@1.15.46': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.27 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.46 + '@swc/core-darwin-x64': 1.15.46 + '@swc/core-linux-arm-gnueabihf': 1.15.46 + '@swc/core-linux-arm64-gnu': 1.15.46 + '@swc/core-linux-arm64-musl': 1.15.46 + '@swc/core-linux-ppc64-gnu': 1.15.46 + '@swc/core-linux-s390x-gnu': 1.15.46 + '@swc/core-linux-x64-gnu': 1.15.46 + '@swc/core-linux-x64-musl': 1.15.46 + '@swc/core-win32-arm64-msvc': 1.15.46 + '@swc/core-win32-ia32-msvc': 1.15.46 + '@swc/core-win32-x64-msvc': 1.15.46 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.27': + dependencies: + '@swc/counter': 0.1.3 + '@szmarczak/http-timer@4.0.6': dependencies: defer-to-connect: 2.0.1 @@ -9487,6 +9887,12 @@ snapshots: character-reference-invalid@2.0.1: {} + chardet@2.2.0: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + chownr@3.0.0: {} chromium-pickle-js@0.2.0: {} @@ -9512,8 +9918,7 @@ snapshots: slice-ansi: 8.0.0 string-width: 8.2.1 - cli-width@4.1.0: - optional: true + cli-width@4.1.0: {} cliui@6.0.0: dependencies: @@ -10002,7 +10407,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-vite@5.0.0(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4)): + electron-vite@5.0.0(@swc/core@1.15.46)(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4)): dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.7) @@ -10011,6 +10416,8 @@ snapshots: magic-string: 0.30.21 picocolors: 1.1.1 vite: rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4) + optionalDependencies: + '@swc/core': 1.15.46 transitivePeerDependencies: - supports-color @@ -10252,20 +10659,17 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-string-truncated-width@3.0.3: - optional: true + fast-string-truncated-width@3.0.3: {} fast-string-width@3.0.2: dependencies: fast-string-truncated-width: 3.0.3 - optional: true fast-uri@3.1.4: {} fast-wrap-ansi@0.2.2: dependencies: fast-string-width: 3.0.2 - optional: true fastq@1.20.1: dependencies: @@ -10417,6 +10821,12 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -10672,6 +11082,41 @@ snapshots: husky@9.1.7: {} + i18next-cli@1.65.0(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(typescript@7.0.2): + dependencies: + '@croct/json5-parser': 0.2.2 + '@swc/core': 1.15.46 + chokidar: 5.0.0 + commander: 14.0.3 + execa: 9.6.1 + glob: 13.0.6 + i18next: 26.3.1(typescript@7.0.2) + i18next-resources-for-ts: 2.1.0 + inquirer: 14.0.2(@types/node@25.9.5) + jiti: 2.7.0 + jsonc-parser: 3.3.1 + magic-string: 0.30.21 + minimatch: 10.2.5 + ora: 9.4.0 + react: 19.2.7 + react-i18next: 17.0.8(i18next@26.3.1(typescript@7.0.2))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@7.0.2) + yaml: 2.9.0 + transitivePeerDependencies: + - '@swc/helpers' + - '@types/node' + - react-dom + - react-native + - typescript + + i18next-resources-for-ts@2.1.0: + dependencies: + '@babel/runtime': 7.29.7 + '@swc/core': 1.15.46 + chokidar: 5.0.0 + yaml: 2.9.0 + transitivePeerDependencies: + - '@swc/helpers' + i18next@26.3.1(typescript@7.0.2): optionalDependencies: typescript: 7.0.2 @@ -10704,6 +11149,17 @@ snapshots: inline-style-parser@0.2.7: {} + inquirer@14.0.2(@types/node@25.9.5): + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@25.9.5) + '@inquirer/prompts': 8.5.2(@types/node@25.9.5) + '@inquirer/type': 4.0.7(@types/node@25.9.5) + mute-stream: 3.0.0 + run-async: 4.0.6 + optionalDependencies: + '@types/node': 25.9.5 + inspect-webkit@0.0.5(typescript@7.0.2): dependencies: typescript: 7.0.2 @@ -11517,8 +11973,7 @@ snapshots: - '@types/node' optional: true - mute-stream@3.0.0: - optional: true + mute-stream@3.0.0: {} nan@2.26.2: optional: true @@ -11794,6 +12249,11 @@ snapshots: path-key@4.0.0: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.1 + minipass: 7.1.3 + path-to-regexp@6.3.0: optional: true @@ -12177,6 +12637,8 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readdirp@5.0.0: {} + recast@0.23.11: dependencies: ast-types: 0.16.1 @@ -12396,6 +12858,8 @@ snapshots: run-applescript@7.1.0: {} + run-async@4.0.6: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -13082,6 +13546,8 @@ snapshots: yaml@2.8.4: {} + yaml@2.9.0: {} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 diff --git a/src/main/i18n/main-i18n-lazy-locale.test.ts b/src/main/i18n/main-i18n-lazy-locale.test.ts index 30d11f549..30d6cb714 100644 --- a/src/main/i18n/main-i18n-lazy-locale.test.ts +++ b/src/main/i18n/main-i18n-lazy-locale.test.ts @@ -58,6 +58,11 @@ describe('main-i18n lazy locale loading', () => { expect(translateMain('menu.file', 'File')).not.toBe('File') }) + it('uses caller English when a target catalog omits a key', async () => { + await setMainUiLanguage(UI_LANGUAGE_SPANISH) + expect(translateMain('missing.main.feature', 'English fallback')).toBe('English fallback') + }) + it('returns to English from a lazily-loaded locale', async () => { await setMainUiLanguage(UI_LANGUAGE_SPANISH) expect(translateMain('menu.file', 'File')).toBe('Archivo') diff --git a/src/renderer/src/components/native-chat/NativeChatToolRun.tsx b/src/renderer/src/components/native-chat/NativeChatToolRun.tsx index 63ba7b167..3bb978bef 100644 --- a/src/renderer/src/components/native-chat/NativeChatToolRun.tsx +++ b/src/renderer/src/components/native-chat/NativeChatToolRun.tsx @@ -129,11 +129,12 @@ export function NativeChatToolRun({ const callCount = countToolCalls(blocks) || blocks.length const summary = summarizeToolRun(blocks) - const fallbackLabel = translate( - callCount === 1 ? 'components.native-chat.tool.countOne' : 'components.native-chat.tool.countN', - callCount === 1 ? '1 tool call' : `${callCount} tool calls`, - { count: callCount } - ) + const fallbackLabel = + callCount === 1 + ? translate('components.native-chat.tool.countOne', '1 tool call') + : translate('components.native-chat.tool.countN', '{{value0}} tool calls', { + value0: callCount + }) return ( // Extra top margin sets the tool run apart from the assistant prose above it diff --git a/src/renderer/src/components/right-sidebar/use-hosted-review-actions.ts b/src/renderer/src/components/right-sidebar/use-hosted-review-actions.ts index 9558eb9a3..331ada85d 100644 --- a/src/renderer/src/components/right-sidebar/use-hosted-review-actions.ts +++ b/src/renderer/src/components/right-sidebar/use-hosted-review-actions.ts @@ -181,7 +181,7 @@ export function useHostedReviewActions({ toast.success( isClosing ? translate( - 'auto.components.right.sidebar.HostedReviewActions.fa3ee9a515', + 'auto.components.right.sidebar.HostedReviewActions.closedToast', '{{value0}} closed', { value0: shortLabel } ) diff --git a/src/renderer/src/components/settings/shortcuts-search.ts b/src/renderer/src/components/settings/shortcuts-search.ts index 21cc4f98d..223b1421e 100644 --- a/src/renderer/src/components/settings/shortcuts-search.ts +++ b/src/renderer/src/components/settings/shortcuts-search.ts @@ -38,7 +38,7 @@ export const getShortcutsPaneSearchEntries = createLocalizedCatalog(() => [ ...KEYBINDING_DEFINITIONS.map((item) => ({ title: item.title, description: translate( - 'auto.components.settings.shortcuts.search.ca6a0c2df7', + 'auto.components.settings.shortcuts.search.groupShortcut', '{{value0}} shortcut', { value0: item.group } ), diff --git a/src/renderer/src/components/sidebar/AddRepoCloneStep.tsx b/src/renderer/src/components/sidebar/AddRepoCloneStep.tsx index e347fae9d..b6eebbeb1 100644 --- a/src/renderer/src/components/sidebar/AddRepoCloneStep.tsx +++ b/src/renderer/src/components/sidebar/AddRepoCloneStep.tsx @@ -137,12 +137,17 @@ export function CloneStep({ value={cloneDestination} onChange={(e) => onDestChange(e.target.value)} onKeyDown={handleKeyDown} - placeholder={translate( + placeholder={ isRemoteClone - ? 'auto.components.sidebar.AddRepoSteps.remoteCloneParentPlaceholder' - : 'auto.components.sidebar.AddRepoSteps.2ce3f6edf8', - isRemoteClone ? '/home/user/projects' : '/path/to/destination' - )} + ? translate( + 'auto.components.sidebar.AddRepoSteps.remoteCloneParentPlaceholder', + '/home/user/projects' + ) + : translate( + 'auto.components.sidebar.AddRepoSteps.2ce3f6edf8', + '/path/to/destination' + ) + } className="h-8 text-xs flex-1" disabled={isCloning} /> diff --git a/src/renderer/src/components/stats/UsageBreakdownSection.tsx b/src/renderer/src/components/stats/UsageBreakdownSection.tsx index 318144813..84186694c 100644 --- a/src/renderer/src/components/stats/UsageBreakdownSection.tsx +++ b/src/renderer/src/components/stats/UsageBreakdownSection.tsx @@ -26,12 +26,10 @@ export function UsageBreakdownSection({ rows, eventsOrTurns }: UsageBreakdownSectionProps): React.JSX.Element { - const eventsOrTurnsKey = + const eventsOrTurnsLabel = eventsOrTurns === 'turns' - ? 'auto.components.stats.UsageBreakdownSection.32176e1d44' - : 'auto.components.stats.UsageBreakdownSection.79a69522a5' - const eventsOrTurnsLabel = eventsOrTurns === 'turns' ? 'turns' : 'events' - const sessionsKey = 'auto.components.stats.UsageBreakdownSection.02a046792e' + ? translate('auto.components.stats.UsageBreakdownSection.32176e1d44', 'turns') + : translate('auto.components.stats.UsageBreakdownSection.79a69522a5', 'events') return (
@@ -50,8 +48,9 @@ export function UsageBreakdownSection({ {formatTokens(row.tokens)}
- {row.sessions} {translate(sessionsKey, 'sessions •')} {row.eventsOrTurns}{' '} - {translate(eventsOrTurnsKey, eventsOrTurnsLabel)} + {row.sessions}{' '} + {translate('auto.components.stats.UsageBreakdownSection.02a046792e', 'sessions •')}{' '} + {row.eventsOrTurns} {eventsOrTurnsLabel} {row.hasInferredPricing ? ` ${translate('auto.components.stats.UsageBreakdownSection.247c93ca92', '• inferred pricing')}` : ''} diff --git a/src/renderer/src/i18n/lazy-locale.test.ts b/src/renderer/src/i18n/lazy-locale.test.ts index 87c6e9559..8bc737936 100644 --- a/src/renderer/src/i18n/lazy-locale.test.ts +++ b/src/renderer/src/i18n/lazy-locale.test.ts @@ -37,6 +37,13 @@ describe('renderer i18n lazy locale loading', () => { expect(i18n.t('menu.file', { defaultValue: 'File' })).not.toBe('File') }) + it('uses the inline English default when a target catalog omits a key', async () => { + await setRendererUiLanguage(UI_LANGUAGE_SPANISH) + expect(i18n.t('missing.renderer.feature', { defaultValue: 'English fallback' })).toBe( + 'English fallback' + ) + }) + it('returns to English from a lazily-loaded locale', async () => { await setRendererUiLanguage(UI_LANGUAGE_SPANISH) expect(i18n.t('menu.file', { defaultValue: 'File' })).toBe('Archivo') diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 3de51346c..d1647b358 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -3714,7 +3714,10 @@ }, "UsageBreakdownSection": { "7765a4c3e1": "n/a", - "247c93ca92": "• inferred pricing" + "247c93ca92": "• inferred pricing", + "32176e1d44": "turns", + "79a69522a5": "events", + "02a046792e": "sessions •" }, "UsageSessionsTable": { "1afc25eb06": "Turns", @@ -4042,7 +4045,8 @@ "32a7256d85": "Clone", "69f5b5380d": "Cloning...", "cloneOnHostDescription": "Enter the Git URL and choose where to clone it on {{value0}}.", - "cloneParentFolder": "Parent folder" + "cloneParentFolder": "Parent folder", + "remoteCloneParentPlaceholder": "/home/user/projects" }, "AutoRenameFailedDialog": { "aed1623b1e": "Close", @@ -8650,6 +8654,7 @@ "shortcuts": { "search": { "ca6a0c2df7": "shortcut", + "groupShortcut": "{{value0}} shortcut", "4811a8264a": "terminal first", "afda131738": "orca first", "0ecfc47434": "conflict", @@ -10001,6 +10006,7 @@ "2bfaf4379c": "More {{value0}} actions", "377269db6f": "{{value0}} reopened", "fa3ee9a515": "closed", + "closedToast": "{{value0}} closed", "78f5ff294c": "This will reopen the {{value0}}.", "a3d572a4de": "This will close the {{value0}}.", "e4aca40024": "Delete Workspace", @@ -14165,7 +14171,9 @@ }, "tool": { "running": "Running…", - "result": "Result" + "result": "Result", + "countOne": "1 tool call", + "countN": "{{value0}} tool calls" }, "status": { "responding": "Agent is responding"