diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 33b69b41a63..e6441b4f857 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -23,4 +23,15 @@ export default defineConfig([ }, }, }, + { + // Node tooling: not in root tsconfig (strict app); use dedicated tsconfig for type-aware lint + files: ['i18n-scripts/**/*.ts'], + languageOptions: { + parserOptions: { + projectService: false, + tsconfigRootDir: __dirname, + project: ['./i18n-scripts/tsconfig.json'], + }, + }, + }, ]) diff --git a/frontend/i18n-scripts/translation-backporting.ts b/frontend/i18n-scripts/translation-backporting.ts new file mode 100755 index 00000000000..dfcee3bce8a --- /dev/null +++ b/frontend/i18n-scripts/translation-backporting.ts @@ -0,0 +1,855 @@ +/* Copyright Contributors to the Open Cluster Management project */ +import chalk from 'chalk' +import fs from 'node:fs' +import path from 'node:path' +import { execFileSync } from 'node:child_process' +import inquirer from 'inquirer' +import { simpleGit, type SimpleGit } from 'simple-git' + +/** Resolved from repo `frontend/` (see `npm run i18n-backporting`). */ +const LOCALES_DIR = path.resolve(process.cwd(), 'public', 'locales') + +const UPSTREAM_NAME = 'upstream' + +/** Max release refs shown in the interactive picker (list is newest-first). */ +const INTERACTIVE_RELEASE_PICK_LIMIT = 8 + +/** Language code → `translation.json` key/value pairs from the given release ref. */ +type TranslationMap = Record> + +/** Per-language, per-lookup-key: string values keyed by git ref (e.g. `upstream/release-2.16`); optional numeric `similarity` for EN diffs. */ +type LangRefPairMap = Record>> + +const SIMILARITY_KEY = 'similarity' as const + +/** `similarity` line: green when score is strictly greater than this. */ +const SIMILARITY_COLOR_GREEN_ABOVE = 0.8 +/** `similarity` line: yellow when score is strictly greater than this and not green; else red. */ +const SIMILARITY_COLOR_YELLOW_ABOVE = 0.5 + +const DISPLAY_LINE_MAX = 100 + +/** One output indent level for `printBackportMapFormatted` (2 characters). Key = 1×, ref lines = 2×. */ +const INDENT = ' ' + +/** Max line length when clipping backport output for ja / ko / zh*. */ +const BACKPORT_CLIP_MAX_CJK = 80 + +/** Max line length when clipping backport output for other locales. */ +const BACKPORT_CLIP_MAX_OTHER = 132 + +const PROMPT_LINES = [ + 'This utility will backport translations from the main branch to the selected release branch.', + 'The new branch will be named "backport-translations-to--"', + 'Note: You can also use a command line argument to specify the release branch.', + ' Example: npm run i18n-backporting release-2.17\n', +].join('\n') + +/** Shown once before interactive English backports when any diff is below the auto-accept similarity threshold. */ +const EN_LOW_SIMILARITY_EXPLAINER = (releaseRef: string): string => + [ + `English string changes have been detected. Answer yes or no to each question`, + `whether to backport each of these new English strings to the target release ${releaseRef}`, + ].join('\n') + +void run().catch((err: unknown) => { + console.error(err instanceof Error ? err.message : String(err)) + process.exit(1) +}) + +async function run(): Promise { + console.log(chalk.bgBlue.white.bold('\n Translation Backporting v1.0.0 \n')) + console.log(chalk.cyan(PROMPT_LINES)) + const git = simpleGit(process.cwd()) + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // ███████╗███████╗████████╗██╗ ██╗██████╗ + // ██╔════╝██╔════╝╚══██╔══╝██║ ██║██╔══██╗ + // ███████╗█████╗ ██║ ██║ ██║██████╔╝ + // ╚════██║██╔══╝ ██║ ██║ ██║██╔═══╝ + // ███████║███████╗ ██║ ╚██████╔╝██║ + // ╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + // --- FETCH FROM GIT --- + try { + console.log(chalk.dim('Fetching from upstream...')) + await git.fetch([UPSTREAM_NAME]) + } catch (e: unknown) { + console.log(e instanceof Error ? e.message : String(e)) + return + } + + // --- GET RELEASES --- + const rawList = await git.raw(['branch', '-r', '--list', `${UPSTREAM_NAME}/release-*`]) + let releaseList = sortReleaseBranches( + rawList + .split('\n') + .map((b) => b.trim()) + .filter(Boolean) + ).reverse() + + // --- CHECKING TRANSLATIONS --- + if (releaseList.length === 0) { + console.log(chalk.yellow('No release branches found.')) + return + } + + /** Newest `upstream/release-*` whose tip has no commits outside `upstream/main` (`rev-list main..release` === 0). */ + let mainRef: string | undefined + for (const rel of releaseList) { + const out = (await git.raw(['rev-list', '--count', `${UPSTREAM_NAME}/main..${rel}`])).trim() + const ahead = Number.parseInt(out, 10) + if (!Number.isFinite(ahead)) { + throw new Error(`Unexpected rev-list --count for ${UPSTREAM_NAME}/main..${rel}: ${JSON.stringify(out)}`) + } + if (ahead > 0) { + break + } + mainRef = rel + } + if (mainRef) { + console.log(chalk.magenta.bold(`${mainRef} is the main branch`)) + const idx = releaseList.indexOf(mainRef) + releaseList = releaseList.slice(idx + 1) + if (releaseList.length === 0) { + console.log(chalk.yellow('No older release branches left after excluding the main release and newer lines.')) + return + } + } else { + console.log( + chalk.yellow( + `No release in releaseList is at or behind ${UPSTREAM_NAME}/main (every tip has commits not in main).` + ) + ) + } + + // --- GET RELEASE FROM CLI ARGUMENT --- + const cliReleaseArg = takeReleaseCliArg() + let releaseRef: string | undefined + if (cliReleaseArg) { + releaseRef = resolveReleaseSpecifier(cliReleaseArg, releaseList) + if (!releaseRef) { + console.log( + chalk.yellow(`Release argument does not match any remote release branch: ${cliReleaseArg}. Pick from the list.`) + ) + } + } + let usedInteractivePick = false + if (!releaseRef) { + usedInteractivePick = true + releaseRef = await pickReleaseBranch(releaseList) + if (!releaseRef) { + return + } + } + if (usedInteractivePick && !(await confirmReleaseBranch(releaseRef))) { + console.log(chalk.yellow('Aborted.')) + return + } + console.log(chalk.magenta.bold(`${releaseRef} is the backport branch.`)) + + // --- GET LOCALE LANGUAGES --- + const localeLangs = fs + .readdirSync(LOCALES_DIR, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + + const backportEnMap: LangRefPairMap = {} + const backportMap: LangRefPairMap = {} + + const selectedIdx = releaseList.indexOf(releaseRef) + if (selectedIdx === -1) { + throw new Error(`Selected release not in release list: ${releaseRef}`) + } + releaseList = releaseList.slice(0, selectedIdx + 1) + + const orderedReleases = [...releaseList].reverse() + if (mainRef) { + orderedReleases.push(mainRef) + } + + let payloadMap: TranslationMap | undefined + let payloadChanged = false + let printedLowSimilarityEnExplainer = false + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // ███████╗██╗███╗ ██╗██████╗ ██████╗ █████╗ ██████╗██╗ ██╗██████╗ ██████╗ ██████╗ ████████╗███████╗ + // ██╔════╝██║████╗ ██║██╔══██╗ ██╔══██╗██╔══██╗██╔════╝██║ ██╔╝██╔══██╗██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝ + // █████╗ ██║██╔██╗ ██║██║ ██║ ██████╔╝███████║██║ █████╔╝ ██████╔╝██║ ██║██████╔╝ ██║ ███████╗ + // ██╔══╝ ██║██║╚██╗██║██║ ██║ ██╔══██╗██╔══██║██║ ██╔═██╗ ██╔═══╝ ██║ ██║██╔══██╗ ██║ ╚════██║ + // ██║ ██║██║ ╚████║██████╔╝ ██████╔╝██║ ██║╚██████╗██║ ██╗██║ ╚██████╔╝██║ ██║ ██║ ███████║ + // ╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ + // + // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + // --- BACKPORTING --- + for (let i = 0; i < orderedReleases.length - 1; i++) { + if (i === 0) { + console.log(chalk.yellow('\nSTEP 1: Searching for changes to backport\n')) + } + const currentRef = orderedReleases[i] + const nextRef = orderedReleases[i + 1] + console.log(chalk.yellow(`Backporting from ${nextRef} into ${releaseRef}...`)) + const currentTranslationMap = await retrieveTranslations(git, currentRef, localeLangs) + const nextTranslationMap = await retrieveTranslations(git, nextRef, localeLangs) + if (currentRef === releaseRef) { + payloadMap = cloneTranslationMap(currentTranslationMap) + } + const nextMap = nextTranslationMap['en'] + const currentEn = currentTranslationMap['en'] + if (!nextMap || !currentEn) { + continue + } + + if ( + !printedLowSimilarityEnExplainer && + process.stdin.isTTY && + enDiffHasAnyBelowGreenSimilarity(currentEn, nextMap) + ) { + console.log(`\n\n${chalk.magentaBright(EN_LOW_SIMILARITY_EXPLAINER(releaseRef))}`) + printedLowSimilarityEnExplainer = true + } + + for (const key of Object.keys(nextMap)) { + if (!Object.prototype.hasOwnProperty.call(currentEn, key)) { + continue + } + const nextEnVal = nextMap[key] + const currentEnVal = currentEn[key] + if (currentEnVal !== nextEnVal) { + if (!backportEnMap['en']) { + backportEnMap['en'] = {} + } + const similarity = getStringSimilarity(currentEnVal, nextEnVal) + backportEnMap['en'][key] = { + [releaseRef]: currentEnVal, + [nextRef]: nextEnVal, + [SIMILARITY_KEY]: similarity, + } + + let acceptNewerEn = similarity >= SIMILARITY_COLOR_GREEN_ABOVE + if (!acceptNewerEn) { + if (!process.stdin.isTTY) { + console.log(chalk.dim(`English differs for "${key}"; no TTY — skipping newer EN backport.`)) + continue + } + const choice = await promptBackportNewerEnToRelease({ + key, + releaseRef, + nextRef, + currentEnVal, + nextEnVal, + similarity, + }) + if (choice === 'q') { + process.exit(0) + } + if (choice === 'n') { + continue + } + acceptNewerEn = true + } + + if (acceptNewerEn && payloadMap && currentRef === releaseRef) { + if (!payloadMap['en']) { + payloadMap['en'] = {} + } + if (payloadMap['en'][key] !== nextEnVal) { + payloadChanged = true + payloadMap['en'][key] = nextEnVal + } + } + } + + for (const lang of localeLangs) { + if (lang === 'en' || (isJapaneseChineseKoreanLocale(lang) && key.endsWith('_plural'))) { + continue + } + const curLangMap = currentTranslationMap[lang] + const nxtLangMap = nextTranslationMap[lang] + let lookupKey = key + let curLoc = curLangMap?.[lookupKey] + let nxtLoc = nxtLangMap?.[lookupKey] + if (!curLoc && isJapaneseChineseKoreanLocale(lang)) { + lookupKey = `${key}_0` + curLoc = curLangMap?.[lookupKey] + nxtLoc = nxtLangMap?.[lookupKey] + } + if (curLoc && nxtLoc && curLoc !== nxtLoc) { + if (!backportMap[lang]) { + backportMap[lang] = {} + } + backportMap[lang][lookupKey] = { [releaseRef]: curLoc ?? '', [nextRef]: nxtLoc ?? '' } + if (payloadMap && currentRef === releaseRef) { + if (!payloadMap[lang]) { + payloadMap[lang] = {} + } + const nextVal = nxtLoc ?? '' + if (payloadMap[lang][lookupKey] !== nextVal) { + payloadChanged = true + payloadMap[lang][lookupKey] = nextVal + } + } + } + } + } + } + + // --- PRINT BACKPORT SUMMARY / OPTIONAL DETAIL --- + printBackportKeyCountsTable(backportMap, backportEnMap, releaseRef) + const detailChoice = await promptBackportDetailsChoice() + if (detailChoice === 'q') { + process.exit(0) + } + if (detailChoice === 'y') { + printBackportMapFormatted(backportEnMap, releaseRef) + printBackportMapFormatted(backportMap, releaseRef) + } + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // ██████╗ ██████╗ ███╗ ███╗███╗ ███╗██╗████████╗ █████╗ ███╗ ██╗██████╗ ██████╗ ██╗ ██╗███████╗██╗ ██╗ + // ██╔════╝██╔═══██╗████╗ ████║████╗ ████║██║╚══██╔══╝ ██╔══██╗████╗ ██║██╔══██╗ ██╔══██╗██║ ██║██╔════╝██║ ██║ + // ██║ ██║ ██║██╔████╔██║██╔████╔██║██║ ██║ ███████║██╔██╗ ██║██║ ██║ ██████╔╝██║ ██║███████╗███████║ + // ██║ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║██║ ██║ ██╔══██║██║╚██╗██║██║ ██║ ██╔═══╝ ██║ ██║╚════██║██╔══██║ + // ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║██║ ██║ ██║ ██║██║ ╚████║██████╔╝ ██║ ╚██████╔╝███████║██║ ██║ + // ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ + // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + if (!payloadChanged) { + console.log('No changes to backport...') + process.exit(0) + } + + const gitRoot = (await git.revparse(['--show-toplevel'])).trim() + const worktreeRelPath = 'frontend/i18n-scripts/temp' + const worktreeAbsPath = path.join(gitRoot, ...worktreeRelPath.split('/')) + const branchName = backportTranslationsBranchName(releaseRef) + + const runCmd = (file: string, args: readonly string[], cwd: string): void => { + execFileSync(file, args, { cwd, stdio: 'inherit' }) + } + + // STEP 2: create a new worktree + console.log(chalk.yellow('\nSTEP 2: Create a new worktree.\n')) + // git worktree add frontend/i18n-scripts/temp (remove first if this path is already in use) + const worktreeListOut = execFileSync('git', ['worktree', 'list'], { cwd: gitRoot, encoding: 'utf8' }) + const resolvedWorktreePath = path.resolve(worktreeAbsPath) + const worktreeAlreadyExists = worktreeListOut.split('\n').some((line) => { + const trimmed = line.trim() + if (!trimmed) return false + const listedPath = trimmed.split(/\s+/)[0] + return listedPath ? path.resolve(listedPath) === resolvedWorktreePath : false + }) + if (worktreeAlreadyExists) { + console.log(chalk.dim('Worktree already exists; removing it before add.')) + // git worktree remove --force frontend/i18n-scripts/temp + runCmd('git', ['worktree', 'remove', '--force', worktreeRelPath], gitRoot) + } else if (fs.existsSync(worktreeAbsPath)) { + // Leftover path on disk (not registered) would make `git worktree add` fail + fs.rmSync(worktreeAbsPath, { recursive: true, force: true }) + } + runCmd('git', ['worktree', 'add', worktreeRelPath], gitRoot) + + // STEP 3: change cwd to the new worktree and show pwd + console.log(chalk.yellow('\nSTEP 3: Change cwd to the new worktree.\n')) + // pushd frontend/i18n-scripts/temp + // pwd + const originalCwd = process.cwd() + process.chdir(worktreeAbsPath) + console.log(process.cwd()) + + try { + // STEP 4: create a new branch + console.log(chalk.yellow('\nSTEP 4: Create a new branch.\n')) + // git checkout -q -b ${branchName} --no-track upstream/${releaseRef} + runCmd('git', ['checkout', '-q', '-b', branchName, '--no-track', releaseRef], process.cwd()) + + // STEP 5: save payload to LOCALES_DIR in worktree + console.log(chalk.yellow('\nSTEP 5: Save backported locale files.\n')) + // Save payload to LOCALES_DIR in worktree. + const worktreeLocalesDir = path.join(process.cwd(), 'frontend', 'public', 'locales') + if (payloadMap) { + console.log(chalk.yellow('\nSaving backported changes...\n')) + for (const lang of Object.keys(payloadMap)) { + const entry = payloadMap[lang] + const outDir = path.join(worktreeLocalesDir, lang) + fs.mkdirSync(outDir, { recursive: true }) + const outPath = path.join(outDir, 'translation.json') + fs.writeFileSync(outPath, `${JSON.stringify(entry, null, 2)}\n`, 'utf8') + } + console.log(chalk.dim(`Saved ${Object.keys(payloadMap).length} locale file(s) under ${worktreeLocalesDir}.`)) + } + + // STEP 6: stage changes and list staged files + console.log(chalk.yellow('\nSTEP 6: Stage changes.\n')) + // git add frontend/public/locales/* + // git diff --staged --name-only + runCmd('git', ['add', path.join('frontend', 'public', 'locales')], process.cwd()) + runCmd('git', ['diff', '--staged', '--name-only'], process.cwd()) + + // STEP 7: commit staged changes + console.log(chalk.yellow('\nSTEP 7: Commit staged changes.\n')) + // git commit --signoff --no-verify -m "chore(i18n): backport translations to ${releaseRef}" + runCmd( + 'git', + ['commit', '--signoff', '--no-verify', '-m', `chore(i18n): backport translations to ${releaseRef}`], + process.cwd() + ) + + // STEP 8: push changes + console.log(chalk.yellow('\nSTEP 8: Push branch.\n')) + // git push --set-upstream origin ${branchName} + runCmd('git', ['push', '--set-upstream', 'origin', branchName], process.cwd()) + + // STEP 9: if gh exists, create a PR + console.log(chalk.yellow('\nSTEP 9: Create PR (if gh is available).\n')) + // gh pr create --base release-2.15 --title "chore(i18n): backport translations to ${releaseRef}" --body-file .github/pull_request_template.md + let hasGh = false + try { + execFileSync('gh', ['--version'], { stdio: 'ignore' }) + hasGh = true + } catch { + hasGh = false + } + if (hasGh) { + const base = releaseRef.startsWith(`${UPSTREAM_NAME}/`) + ? releaseRef.slice(UPSTREAM_NAME.length + 1) + : releaseRef.replace(/\//g, '-') + runCmd( + 'gh', + [ + 'pr', + 'create', + '--base', + base, + '--title', + `chore(i18n): backport translations to ${releaseRef}`, + '--body-file', + path.join('.github', 'pull_request_template.md'), + ], + process.cwd() + ) + } else { + console.log(chalk.dim('gh not found; skipping PR creation step.')) + } + } finally { + // STEP 10: remove the worktree + console.log(chalk.yellow('\nSTEP 10: Remove the worktree.\n')) + // git worktree remove frontend/i18n-scripts/temp + runCmd('git', ['worktree', 'remove', worktreeRelPath], gitRoot) + + // STEP 11: return to the original cwd and show pwd + console.log(chalk.yellow('\nSTEP 11: Return to original cwd.\n')) + // popd + // pwd + process.chdir(originalCwd) + console.log(process.cwd()) + } + + process.exit(0) +} + +/** Per-language summary: full name; **Backports** is locale diff count, or English EN-diff count on the English row only. */ +function printBackportKeyCountsTable( + backportMap: LangRefPairMap, + backportEnMap: LangRefPairMap, + releaseRef: string +): void { + console.log() + console.log() + console.log(chalk.magenta.bold(`\nNumber of strings to be`)) + console.log(chalk.magenta.bold(`backported to ${releaseRef}\n`)) + const langs = new Set([...Object.keys(backportMap), ...Object.keys(backportEnMap)]) + const rowsWithCount = [...langs].map((code) => { + const nMap = backportMap[code] ? Object.keys(backportMap[code]).length : 0 + const nEn = backportEnMap[code] ? Object.keys(backportEnMap[code]).length : 0 + const count = code === 'en' ? nEn : nMap + const language = localeCodeToFullLanguageName(code) + return { language, count } + }) + rowsWithCount.sort((a, b) => b.count - a.count || a.language.localeCompare(b.language)) + const rows = rowsWithCount.map(({ language, count }) => ({ + language, + backports: String(count), + })) + const col0Header = 'Language' + const col1Header = 'Backports' + const w0 = Math.max(col0Header.length, ...rows.map((r) => r.language.length), 1) + const w1 = Math.max(col1Header.length, ...rows.map((r) => r.backports.length), 1) + console.log(`${col0Header.padEnd(w0)} ${col1Header}`) + console.log(`${''.padEnd(w0, '-')} ${''.padEnd(w1, '-')}`) + for (const r of rows) { + console.log(`${r.language.padEnd(w0)} ${chalk.magenta(r.backports.padEnd(w1))}`) + } +} + +type BackportDetailsChoice = 'y' | 'n' | 'q' + +/** Ask whether to print full backport diff; non-TTY returns `n`. */ +async function promptBackportDetailsChoice(): Promise { + if (!process.stdin.isTTY) { + return 'n' + } + const { choice } = await inquirer.prompt<{ choice: string }>([ + { + type: 'input', + name: 'choice', + message: 'Show details? (y/n/q)', + validate: (input: string) => { + const c = input.trim().toLowerCase() + if (c.length !== 1) { + return 'Enter a single character: y, n, or q' + } + if (c === 'y' || c === 'n' || c === 'q') { + return true + } + return 'Enter y, n, or q' + }, + }, + ]) + return choice.trim().toLowerCase() as BackportDetailsChoice +} + +// --- PRINT BACKPORT MAP --- +/** Prints each locale section: blue banner for the language name; keys and ref/value lines in dim white (releaseRef first). */ +function printBackportMapFormatted(backportMap: LangRefPairMap, releaseRef: string): void { + for (const lang of Object.keys(backportMap).sort()) { + const lineMax = isJapaneseChineseKoreanLocale(lang) ? BACKPORT_CLIP_MAX_CJK : BACKPORT_CLIP_MAX_OTHER + const byKey = backportMap[lang] + if (!byKey) { + continue + } + const totalKeys = Object.keys(byKey).length + const fullName = localeCodeToFullLanguageName(lang) + console.log(chalk.bgBlue.white.bold(`\n ${fullName} (${totalKeys}) \n`)) + const keysSorted = Object.keys(byKey).sort() + for (let ki = 0; ki < keysSorted.length; ki++) { + const lookupKey = keysSorted[ki] + console.log(chalk.white(clipDisplayLine(`${INDENT}${formatLookupKeyLineIndex(ki + 1)}${lookupKey}`, lineMax))) + const pair = byKey[lookupKey]! + const refOrder: string[] = [] + if (Object.prototype.hasOwnProperty.call(pair, releaseRef)) { + refOrder.push(releaseRef) + } + for (const ref of Object.keys(pair).sort()) { + if (ref !== releaseRef && ref !== SIMILARITY_KEY) { + refOrder.push(ref) + } + } + for (const ref of refOrder) { + const raw = pair[ref] + if (typeof raw !== 'string') { + continue + } + const valThis = raw.replace(/\s+/g, ' ').trim() + const linePrefix = `${INDENT}${INDENT}${ref}: ` + console.log(chalk.dim.white(clipDisplayLine(linePrefix + valThis, lineMax))) + } + const sim = pair[SIMILARITY_KEY] + if (typeof sim === 'number') { + const simLine = `${INDENT}${INDENT}${SIMILARITY_KEY}: ${sim.toFixed(4)}` + const simColor = + sim > SIMILARITY_COLOR_GREEN_ABOVE + ? chalk.green + : sim > SIMILARITY_COLOR_YELLOW_ABOVE + ? chalk.yellow + : chalk.red + console.log(simColor(simLine)) + } + if (ki < keysSorted.length - 1) { + console.log() + } + } + } +} + +/** 1-based key index prefix, e.g. `【57】 `. */ +function formatLookupKeyLineIndex(oneBased: number): string { + return `【${oneBased}】 ` +} + +function clipDisplayLine(s: string, maxLen: number = DISPLAY_LINE_MAX): string { + if (s.length <= maxLen) { + return s + } + return `${s.slice(0, maxLen - 1)}…` +} + +function localeCodeToFullLanguageName(code: string): string { + const tag = code.replace(/_/g, '-') + try { + const name = new Intl.DisplayNames(['en'], { type: 'language' }).of(tag) + return name && name !== tag ? name : code + } catch { + return code + } +} + +function cloneTranslationMap(map: TranslationMap): TranslationMap { + const out: TranslationMap = {} + for (const [lang, entries] of Object.entries(map)) { + out[lang] = { ...entries } + } + return out +} + +/** True if some shared EN key differs across refs and similarity is below the auto-accept threshold. */ +function enDiffHasAnyBelowGreenSimilarity(currentEn: Record, nextMap: Record): boolean { + for (const key of Object.keys(nextMap)) { + if (!Object.prototype.hasOwnProperty.call(currentEn, key)) { + continue + } + const nextEnVal = nextMap[key] + const currentEnVal = currentEn[key] + if (currentEnVal === nextEnVal) { + continue + } + if (getStringSimilarity(currentEnVal, nextEnVal) < SIMILARITY_COLOR_GREEN_ABOVE) { + return true + } + } + return false +} + +function isJapaneseChineseKoreanLocale(lang: string): boolean { + return lang === 'ja' || lang === 'ko' || lang.startsWith('zh') +} + +/** + * For each `localeLangs` entry, reads `LOCALES_DIR//translation.json` at `releaseRef` via `git show`. + */ +async function retrieveTranslations( + git: SimpleGit, + releaseRef: string, + localeLangs: string[] +): Promise { + const gitRoot = (await git.revparse(['--show-toplevel'])).trim() + const translationMap: TranslationMap = {} + + for (const lang of localeLangs) { + const translationPath = path.join(LOCALES_DIR, lang, 'translation.json') + const gitPath = path.relative(gitRoot, translationPath).split(path.sep).join('/') + if (gitPath.startsWith('..')) { + throw new Error(`Locale file ${translationPath} is outside git root ${gitRoot}`) + } + const spec = `${releaseRef}:${gitPath}` + try { + const raw = await git.show(spec) + translationMap[lang] = JSON.parse(raw) as Record + } catch (err: unknown) { + const detail = err instanceof Error ? err.message : String(err) + throw new Error(`Failed to read translations for locale "${lang}" (${spec}): ${detail}`, { cause: err }) + } + } + + return translationMap +} + +/** + * Parses `…/release-M.m…` and sorts ascending so e.g. release-2.1 … release-2.16 … release-2.17. + */ +function parseReleaseVersion(ref: string): [number, number] | null { + const m = ref.match(/release-(\d+)\.(\d+)/) + if (!m) return null + return [Number.parseInt(m[1], 10), Number.parseInt(m[2], 10)] +} + +/** Local branch name: `backport-translations-to--`; `` omits the `upstream/` prefix. */ +function backportTranslationsBranchName(releaseRef: string): string { + const releaseSegment = releaseRef.startsWith(`${UPSTREAM_NAME}/`) + ? releaseRef.slice(UPSTREAM_NAME.length + 1) + : releaseRef.replace(/\//g, '-') + const d = new Date() + const z = (n: number) => String(n).padStart(2, '0') + const dateTime = `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())}_${z(d.getHours())}${z(d.getMinutes())}${z(d.getSeconds())}` + return `backport-translations-to-${releaseSegment}-${dateTime}` +} + +/** First positional CLI argument (skips tokens starting with `-`, including `--`). */ +function takeReleaseCliArg(): string | undefined { + for (const a of process.argv.slice(2)) { + if (!a.startsWith('-')) { + return a.trim() + } + } + return undefined +} + +/** Maps user input to an entry in `releaseList` (exact ref or `release-M.m` → `upstream/release-M.m`). */ +function resolveReleaseSpecifier(spec: string, releaseList: string[]): string | undefined { + const trimmed = spec.trim() + if (!trimmed) { + return undefined + } + const exact = releaseList.find((r) => r === trimmed) + if (exact) { + return exact + } + const normalized = trimmed.includes('/') ? trimmed : `${UPSTREAM_NAME}/${trimmed}` + return releaseList.find((r) => r === normalized) +} + +function sortReleaseBranches(refs: string[]): string[] { + return [...refs].sort((a, b) => { + const va = parseReleaseVersion(a) + const vb = parseReleaseVersion(b) + if (va && vb) { + if (va[0] !== vb[0]) return va[0] - vb[0] + return va[1] - vb[1] + } + if (va && !vb) return -1 + if (!va && vb) return 1 + return a.localeCompare(b) + }) +} + +/** + * Interactive list pick, or non-interactive via `I18N_BACKPORT_RELEASE` + * (full ref `upstream/release-2.16` or short `release-2.16`). + */ +async function pickReleaseBranch(releaseList: string[]): Promise { + const fromEnv = process.env.I18N_BACKPORT_RELEASE?.trim() + if (fromEnv) { + const resolved = resolveReleaseSpecifier(fromEnv, releaseList) + if (resolved) { + console.log( + chalk.dim( + resolved === fromEnv + ? `Using I18N_BACKPORT_RELEASE=${fromEnv}` + : `Using I18N_BACKPORT_RELEASE=${fromEnv} → ${resolved}` + ) + ) + return resolved + } + console.log(chalk.red(`I18N_BACKPORT_RELEASE=${fromEnv} does not match any remote release branch.`)) + console.log(chalk.dim(releaseList.join('\n'))) + return undefined + } + + if (!process.stdin.isTTY) { + console.log( + chalk.yellow( + 'No TTY: set I18N_BACKPORT_RELEASE (e.g. upstream/release-2.16 or release-2.16) for a non-interactive run.' + ) + ) + return undefined + } + + const pickChoices = releaseList.slice(0, INTERACTIVE_RELEASE_PICK_LIMIT) + const { releaseRef } = await inquirer.prompt<{ releaseRef: string }>([ + { + type: 'list', + name: 'releaseRef', + message: 'Pick release branch:', + choices: pickChoices.map((ref) => ({ name: ref, value: ref })), + }, + ]) + return releaseRef +} + +type BackportNewerEnChoice = 'y' | 'n' | 'q' + +/** Ask whether to use the newer ref’s English string on the backport branch. */ +async function promptBackportNewerEnToRelease(args: { + key: string + releaseRef: string + nextRef: string + currentEnVal: string + nextEnVal: string + similarity: number +}): Promise { + const { key, releaseRef, nextRef, currentEnVal, nextEnVal, similarity } = args + const valueLineColor = + similarity > SIMILARITY_COLOR_GREEN_ABOVE + ? chalk.white + : similarity > SIMILARITY_COLOR_YELLOW_ABOVE + ? chalk.yellow + : chalk.red + console.log() + console.log(chalk.greenBright.bold(`For key: ${key}`)) + console.log(valueLineColor(clipDisplayLine(`${releaseRef}: ${currentEnVal}`, DISPLAY_LINE_MAX))) + console.log(valueLineColor(clipDisplayLine(`${nextRef}: ${nextEnVal}`, DISPLAY_LINE_MAX))) + const { choice } = await inquirer.prompt<{ choice: string }>([ + { + type: 'input', + name: 'choice', + message: 'Backport? (y/n/q)', + validate: (input: string) => { + const c = input.trim().toLowerCase() + if (c.length !== 1) { + return 'Enter a single character: y, n, or q' + } + if (c === 'y' || c === 'n' || c === 'q') { + return true + } + return 'Enter y, n, or q' + }, + }, + ]) + return choice.trim().toLowerCase() as BackportNewerEnChoice +} + +/** Yes/no for the chosen ref; non-TTY runs skip the prompt and proceed. */ +async function confirmReleaseBranch(releaseRef: string): Promise { + if (!process.stdin.isTTY) { + return true + } + const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([ + { + type: 'confirm', + name: 'confirmed', + message: `Proceed with ${releaseRef}?`, + default: true, + }, + ]) + return confirmed +} + +/** Dice-coefficient–style similarity from overlapping character n-grams (default bigrams). */ +export const getStringSimilarity = ( + str1: string, + str2: string, + substringLength: number = 2, + caseSensitive: boolean = false +): number => { + let a = str1 + let b = str2 + if (!caseSensitive) { + a = a.toLowerCase() + b = b.toLowerCase() + } + if (a.length < substringLength || b.length < substringLength) { + return 0 + } + + const map = new Map() + for (let i = 0; i < a.length - (substringLength - 1); i++) { + const substr1 = a.slice(i, i + substringLength) + map.set(substr1, (map.get(substr1) ?? 0) + 1) + } + + let match = 0 + for (let j = 0; j < b.length - (substringLength - 1); j++) { + const substr2 = b.slice(j, j + substringLength) + const count = map.get(substr2) ?? 0 + if (count > 0) { + map.set(substr2, count - 1) + match++ + } + } + + return (match * 2) / (a.length + b.length - (substringLength - 1) * 2) +} diff --git a/frontend/i18n-scripts/tsconfig.json b/frontend/i18n-scripts/tsconfig.json new file mode 100644 index 00000000000..55912899587 --- /dev/null +++ b/frontend/i18n-scripts/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": false, + "noImplicitAny": false, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "types": ["node"] + }, + "include": ["./**/*.ts"] +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b7224611577..655e45a6ef4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -132,6 +132,7 @@ "html-webpack-plugin": "^5.6.6", "i18next-parser": "6.6.0", "identity-obj-proxy": "3.0.0", + "inquirer": "^13.4.2", "jest": "^29.7.0", "jest-axe": "6.0.1", "jest-diff": "29.7.0", @@ -165,6 +166,7 @@ "reselect": "^4.1.8", "sass": "1.99.0", "sass-loader": "13.3.3", + "simple-git": "^3.28.0", "stacktrace-js": "2.0.2", "storybook": "8.0.9", "stream-browserify": "3.0.0", @@ -176,6 +178,7 @@ "ts-node": "10.9.2", "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "^4.2.0", + "tsx": "^4.19.4", "typescript": "^5.8.2", "v8-compile-cache": "2.4.0", "vm-browserify": "^1.1.2", @@ -3200,6 +3203,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", @@ -3216,6 +3236,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", @@ -3232,6 +3269,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", @@ -3613,6 +3667,380 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inquirer/ansi": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", + "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.4.tgz", + "integrity": "sha512-w6KF8ZYRvqHhROkOTHXYC3qIV/KYEu5o12oLqQySvch61vrYtRxNSHTONSdJqWiFJPlCUQAHT5OgOIyuTr+MHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.9", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz", + "integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.9.tgz", + "integrity": "sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/editor": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.1.1.tgz", + "integrity": "sha512-6y11LgmNpmn5D2aB5FgnCfBUBK8ZstwLCalyJmORcJZ/WrhOjm16mu6eSqIx8DnErxDqSLr+Jkp+GP8/Nwd5tA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/external-editor": "^3.0.0", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.13.tgz", + "integrity": "sha512-dF2zvrFo9LshkcB23/O1il13kBkBltWIXzut1evfbuBLXMiGIuC45c+ZQ0uukjCDsvI8OWqun4FRYMnzFCQa3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.0.tgz", + "integrity": "sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", + "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.12.tgz", + "integrity": "sha512-uiMFBl4LqFzJClh80Q3f9hbOFJ6kgkDWI4LjAeBuyO6EanVVMF69AgOvpi1qdqjDSjDN6578B6nky9ceEpI+1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.12.tgz", + "integrity": "sha512-/vrwhEf7Xsuh+YlHF4IjSy3g1cyrQuPaSiHIxCEbLu8qnfvrcvJyCkoktOOF+xV9gSb77/G0n3h04RbMDW2sIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.12.tgz", + "integrity": "sha512-CBh7YHju623lxJRcAOo498ZUwIuMy63bqW/vVq0tQAZVv+lkWlHkP9ealYE1utWSisEShY5VMdzIXRmyEODzcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.2.tgz", + "integrity": "sha512-XJmn/wY4AX56l1BRU+ZjDrFtg9+2uBEi4JvJQj82kwJDQKiPgSn4CEsbfGGygS4Gw6rkL4W18oATjfVfaqub2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.1.4", + "@inquirer/confirm": "^6.0.12", + "@inquirer/editor": "^5.1.1", + "@inquirer/expand": "^5.0.13", + "@inquirer/input": "^5.0.12", + "@inquirer/number": "^4.0.12", + "@inquirer/password": "^5.0.12", + "@inquirer/rawlist": "^5.2.8", + "@inquirer/search": "^4.1.8", + "@inquirer/select": "^5.1.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.8.tgz", + "integrity": "sha512-Su7FQvp5buZmCymN3PPoYv31ZQQX4ve2j02k7piGgKAWgE+AQRB5YoYVveGXcl3TZ9ldgRMSxj56YfDFmmaqLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.8.tgz", + "integrity": "sha512-fGiHKGD6DyPIYUWxoXnQTeXeyYqSOUrasDMABBmMHUalH/LxkuzY0xVRtimXAt1sUeeyYkVuKQx1bebMuN11Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.4.tgz", + "integrity": "sha512-2kWcGKPMLAXAWRp1AH1SLsQmX+j0QjeljyXMUji9WMZC8nRDO0b7qquIGr6143E7KMLt3VAIGNXzwa/6PXQs4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.9", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", + "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -5696,6 +6124,23 @@ "streamx": "^2.15.0" } }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true, + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -7626,6 +8071,23 @@ "node": ">=8" } }, + "node_modules/@simple-git/args-pathspec": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", + "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@simple-git/argv-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", + "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-git/args-pathspec": "^1.0.3" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.48", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", @@ -14546,6 +15008,13 @@ "node": ">=10" } }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -14972,6 +15441,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -19682,6 +20161,23 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -19698,6 +20194,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fastest-levenshtein": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", @@ -20814,6 +21320,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/get-value": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/get-value/-/get-value-3.0.1.tgz", @@ -22111,6 +22630,33 @@ "node": ">=10" } }, + "node_modules/inquirer": { + "version": "13.4.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-13.4.2.tgz", + "integrity": "sha512-ziXEKBO6nxsX9Z3XEh7LNiUvYN/o5PYuYK+27l69NpjSUOh6JXQsQAKEw2AnZq5xvHeb3ZwkpzOxvNOswIX1fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.9", + "@inquirer/prompts": "^8.4.2", + "@inquirer/type": "^4.0.5", + "mute-stream": "^3.0.0", + "run-async": "^4.0.6", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -28711,6 +29257,16 @@ "multicast-dns": "cli.js" } }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/nanoclone": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", @@ -32410,6 +32966,16 @@ "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", "license": "MIT" }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -32495,6 +33061,16 @@ "node": "6.* || >= 7.*" } }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -33279,6 +33855,24 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "devOptional": true }, + "node_modules/simple-git": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", + "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "@simple-git/args-pathspec": "^1.0.3", + "@simple-git/argv-parser": "^1.1.0", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, "node_modules/sirv": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", @@ -35219,6 +35813,459 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 41f62bf54b1..6cf46395001 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "i18n-generate": "rm -rf public/locales/upload && MODE=generate i18next --fail-on-warnings", "i18n-upload": "i18n-scripts/memsource-upload.sh", "i18n-download": "i18n-scripts/memsource-download.sh", + "i18n-backporting": "tsx i18n-scripts/translation-backporting.ts", "update": "npx npm-check-updates -x @testing-library/user-event,monaco-editor,eslint,react-router-dom --doctor --upgrade && npm audit fix && npm dedup", "serve:plugins": "concurrently npm:serve:plugin:* -c green,blue,cyan,magenta", "serve:plugin": "./scripts/checkPlugin.sh && cd plugins/${PLUGIN} && cp ../../package.json . && TS_NODE_PROJECT=../../webpack.tsconfig.json webpack serve -c webpack.plugin.ts --env plugin=$PLUGIN --env port=$PORT --mode development", @@ -161,6 +162,7 @@ "html-webpack-plugin": "^5.6.6", "i18next-parser": "6.6.0", "identity-obj-proxy": "3.0.0", + "inquirer": "^13.4.2", "jest": "^29.7.0", "jest-axe": "6.0.1", "jest-diff": "29.7.0", @@ -194,6 +196,7 @@ "reselect": "^4.1.8", "sass": "1.99.0", "sass-loader": "13.3.3", + "simple-git": "^3.28.0", "stacktrace-js": "2.0.2", "storybook": "8.0.9", "stream-browserify": "3.0.0", @@ -205,6 +208,7 @@ "ts-node": "10.9.2", "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "^4.2.0", + "tsx": "^4.19.4", "typescript": "^5.8.2", "v8-compile-cache": "2.4.0", "vm-browserify": "^1.1.2",