fix: crowdin sync and pretranslate for plugin locales, run plugin tests on release

This commit is contained in:
LukeGus committed 2026-09-30 23:27:34 -05:00
1 parent 1c4b057fd8
commit f8edb5f299
5 files changed
+72 -30

No files matched your search

+2 -2
View File
@@ -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
+7 -3
View File
@@ -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
+4 -1
View File
@@ -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
+34 -24
View File
@@ -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/<id>/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,
};
+25
View File
@@ -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);
});
});