diff --git a/.github/workflows/locize-i18n-sync.yml b/.github/workflows/locize-i18n-sync.yml index c820b52797..5fb3fcdc7e 100644 --- a/.github/workflows/locize-i18n-sync.yml +++ b/.github/workflows/locize-i18n-sync.yml @@ -2,11 +2,12 @@ name: Sync Locize Translations & Create Translation PR on: push: - branches: [main] + branches: [dev] paths: - 'client/src/locales/en/**' repository_dispatch: types: [locize/versionPublished] + workflow_dispatch: concurrency: group: locize-i18n-sync @@ -51,7 +52,7 @@ jobs: create-pull-request: name: Create Translation PR on Version Published - if: ${{ github.event_name == 'repository_dispatch' }} + if: ${{ github.event_name == 'repository_dispatch' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest needs: sync-translations permissions: @@ -61,6 +62,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v5 with: + ref: dev persist-credentials: false # Keep a baseline so generated changes can be checked before opening a PR. @@ -75,6 +77,12 @@ jobs: path: "client/src/locales" version: latest + - name: Preserve Repository Translations Missing from locize + run: | + node scripts/merge-locize-download.mjs \ + --base-dir "$RUNNER_TEMP/locize-locale-baseline" \ + --current-dir client/src/locales + - name: Validate Downloaded Translations run: | node scripts/validate-locize-download.mjs \ @@ -91,7 +99,7 @@ jobs: add-paths: | client/src/locales/** commit-message: "🌍 i18n: Update translation.json with latest translations" - base: main + base: dev branch: i18n/locize-translation-update title: "🌍 i18n: Update translation.json with latest translations" body: | diff --git a/scripts/merge-locize-download.mjs b/scripts/merge-locize-download.mjs new file mode 100644 index 0000000000..b798f915aa --- /dev/null +++ b/scripts/merge-locize-download.mjs @@ -0,0 +1,89 @@ +#!/usr/bin/env node + +import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +const args = new Map(); +for (let index = 2; index < process.argv.length; index += 1) { + if (!process.argv[index].startsWith('--')) continue; + args.set(process.argv[index].slice(2), process.argv[index + 1]); + index += 1; +} + +const baseDir = args.get('base-dir'); +const currentDir = args.get('current-dir'); +if (!baseDir || !currentDir) { + console.error('Usage: merge-locize-download.mjs --base-dir --current-dir '); + process.exit(2); +} + +function isObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function leafCount(value) { + if (!isObject(value)) return 1; + return Object.values(value).reduce((count, child) => count + leafCount(child), 0); +} + +function mergeValues(base, current) { + if (!isObject(base) || !isObject(current)) return { value: current, restored: 0 }; + + let restored = 0; + const value = {}; + for (const [key, baseValue] of Object.entries(base)) { + if (!Object.hasOwn(current, key)) { + value[key] = baseValue; + restored += leafCount(baseValue); + continue; + } + const merged = mergeValues(baseValue, current[key]); + value[key] = merged.value; + restored += merged.restored; + } + + for (const [key, currentValue] of Object.entries(current)) { + if (!Object.hasOwn(base, key)) value[key] = currentValue; + } + + return { value, restored }; +} + +async function jsonFiles(directory, relative = '') { + const entries = await readdir(path.join(directory, relative), { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const entryRelative = path.join(relative, entry.name); + if (entry.isDirectory()) files.push(...(await jsonFiles(directory, entryRelative))); + else if (entry.isFile() && entry.name.endsWith('.json')) files.push(entryRelative); + } + return files; +} + +async function readJson(filePath) { + return JSON.parse(await readFile(filePath, 'utf8')); +} + +let restored = 0; +let changedFiles = 0; +for (const relative of await jsonFiles(baseDir)) { + const basePath = path.join(baseDir, relative); + const currentPath = path.join(currentDir, relative); + const base = await readJson(basePath); + let current; + try { + current = await readJson(currentPath); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + current = {}; + } + + const merged = mergeValues(base, current); + if (merged.restored === 0) continue; + await mkdir(path.dirname(currentPath), { recursive: true }); + await writeFile(currentPath, `${JSON.stringify(merged.value, null, 2)}\n`); + restored += merged.restored; + changedFiles += 1; +} + +console.log(`Preserved ${restored} repository translation value(s) across ${changedFiles} file(s).`);