From f8edb5f2995f59c2e68fb4e375e2c0439b532765 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Wed, 30 Sep 2026 23:27:34 -0500 Subject: [PATCH] fix: crowdin sync and pretranslate for plugin locales, run plugin tests on release --- .github/workflows/crowdin-sync.yml | 4 +- .github/workflows/release.yml | 10 +++-- crowdin.yml | 5 ++- scripts/crowdin-pretranslate.cjs | 58 ++++++++++++++++------------ scripts/crowdin-pretranslate.test.ts | 25 ++++++++++++ 5 files changed, 72 insertions(+), 30 deletions(-) create mode 100644 scripts/crowdin-pretranslate.test.ts diff --git a/.github/workflows/crowdin-sync.yml b/.github/workflows/crowdin-sync.yml index 520bd5dec..71a141524 100644 --- a/.github/workflows/crowdin-sync.yml +++ b/.github/workflows/crowdin-sync.yml @@ -51,7 +51,7 @@ jobs: env: CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_KEY }} - - name: Machine pre-translate untranslated strings + - name: Pre-translate untranslated strings env: CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }} run: node scripts/crowdin-pretranslate.cjs @@ -74,7 +74,7 @@ jobs: git config user.name "LukeGus" git config user.email "bugattiguy527@gmail.com" - git add src/ui/locales/translated + git add src/ui/locales/translated plugins/*/locales/translated if git diff --cached --quiet; then echo "No translation changes to commit." exit 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a099ee464..9778be0ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -102,8 +102,9 @@ jobs: - name: Lint and format run: | - npm run lint:fix || true + npm run lint:fix npm run format + npm run lint # The plugin boot tests load the built bundles. - name: Build plugins @@ -112,6 +113,9 @@ jobs: - name: Run unit tests run: npm run test + - name: Run plugin tests + run: npm run test:plugins + - name: Sync version run: node scripts/sync-version.cjs --version "${{ needs.prep.outputs.version }}" @@ -184,7 +188,7 @@ jobs: env: CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_KEY }} - - name: Machine pre-translate untranslated strings + - name: Pre-translate untranslated strings env: CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }} run: node scripts/crowdin-pretranslate.cjs @@ -207,7 +211,7 @@ jobs: git config user.name "LukeGus" git config user.email "bugattiguy527@gmail.com" - git add src/ui/locales/translated + git add src/ui/locales/translated plugins/*/locales/translated if git diff --cached --quiet; then echo "No translation changes to commit." exit 0 diff --git a/crowdin.yml b/crowdin.yml index 2dbe8904b..8a88aa9aa 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,8 +1,11 @@ project_id: "858252" api_token: "env:CROWDIN_API_TOKEN" +preserve_hierarchy: true files: + # Core has always lived at /en.json in Crowdin; moving it would orphan its translations. - source: /src/ui/locales/en.json + dest: /en.json translation: /src/ui/locales/translated/%locale_with_underscore%.json - source: /plugins/*/locales/en.json - translation: /plugins/**/locales/translated/%locale_with_underscore%.json + translation: /%original_path%/translated/%locale_with_underscore%.json diff --git a/scripts/crowdin-pretranslate.cjs b/scripts/crowdin-pretranslate.cjs index 32ffcb204..104fc5ed8 100644 --- a/scripts/crowdin-pretranslate.cjs +++ b/scripts/crowdin-pretranslate.cjs @@ -52,14 +52,20 @@ async function resolveProjectId() { return { id: match.id, targetLanguageIds: match.targetLanguageIds || [] }; } -async function resolveFileId(projectId) { - const files = await paged(`/projects/${projectId}/files`); - const match = files.find( - (f) => f.name === SOURCE_FILE || f.path === `/${SOURCE_FILE}`, +// Core is /en.json and each plugin is /plugins//locales/en.json. +function isSourceFile(file) { + return ( + file.path === `/${SOURCE_FILE}` || + /^\/plugins\/[^/]+\/locales\/en\.json$/.test(file.path || "") ); - if (!match) - throw new Error(`source file "${SOURCE_FILE}" not found in project`); - return match.id; +} + +async function resolveFileIds(projectId) { + const files = await paged(`/projects/${projectId}/files`); + const ids = files.filter(isSourceFile).map((f) => f.id); + if (ids.length === 0) + throw new Error(`no "${SOURCE_FILE}" source files found in project`); + return ids; } async function pollPreTranslation(projectId, preTranslationId) { @@ -84,31 +90,35 @@ async function main() { throw new Error("project has no target languages configured"); } - const fileId = await resolveFileId(projectId); + const fileIds = await resolveFileIds(projectId); console.log( - `Pre-translating project ${projectId}, file ${fileId}, ${targetLanguageIds.length} languages via MT engine ${ENGINE_ID}`, + `Pre-translating project ${projectId}, ${fileIds.length} files, ${targetLanguageIds.length} languages`, ); - const { data } = await request( - "POST", - `/projects/${projectId}/pre-translations`, - { - languageIds: targetLanguageIds, - fileIds: [fileId], - method: "mt", - engineId: ENGINE_ID, - scope: "untranslated", - }, - ); - - const result = await pollPreTranslation(projectId, data.identifier); - console.log(`Pre-translation finished (${result.progress}%)`); + // Translation memory first so strings that moved between files keep their + // existing translations, then machine translation for whatever is left. + const passes = [ + { method: "tm", translateUntranslatedOnly: true }, + { method: "mt", engineId: ENGINE_ID, scope: "untranslated" }, + ]; + for (const pass of passes) { + const { data } = await request( + "POST", + `/projects/${projectId}/pre-translations`, + { languageIds: targetLanguageIds, fileIds, ...pass }, + ); + const result = await pollPreTranslation(projectId, data.identifier); + console.log( + `${pass.method} pre-translation finished (${result.progress}%)`, + ); + } } module.exports = { resolveProjectId, - resolveFileId, + resolveFileIds, + isSourceFile, pollPreTranslation, }; diff --git a/scripts/crowdin-pretranslate.test.ts b/scripts/crowdin-pretranslate.test.ts new file mode 100644 index 000000000..a0bac51cc --- /dev/null +++ b/scripts/crowdin-pretranslate.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const { isSourceFile } = require("./crowdin-pretranslate.cjs") as { + isSourceFile: (file: { path?: string }) => boolean; +}; + +describe("isSourceFile", () => { + it("matches the core and plugin source files", () => { + expect(isSourceFile({ path: "/en.json" })).toBe(true); + expect(isSourceFile({ path: "/plugins/acme-ssl/locales/en.json" })).toBe( + true, + ); + }); + + it("skips anything else", () => { + expect(isSourceFile({ path: "/src/ui/locales/en.json" })).toBe(false); + expect(isSourceFile({ path: "/plugins/a/b/locales/en.json" })).toBe(false); + expect(isSourceFile({ path: "/plugins/acme-ssl/locales/de.json" })).toBe( + false, + ); + expect(isSourceFile({})).toBe(false); + }); +});