Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions .github/workflows/i18n-translation-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
name: i18n translation drift

on:
pull_request:
paths:
- "docs/**/*.md"
- "docs/.vuepress/config.js"
- "scripts/i18n-drift-check.mjs"
push:
branches:
- master
paths:
- "docs/**/*.md"
- "docs/.vuepress/config.js"
- "scripts/i18n-drift-check.mjs"

permissions:
contents: read
pull-requests: write
issues: write

jobs:
report-pr:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Generate i18n drift report
run: |
node scripts/i18n-drift-check.mjs \
--base "${{ github.event.pull_request.base.sha }}" \
--head "${{ github.event.pull_request.head.sha }}" \
--json-out i18n-report.json \
--markdown-out i18n-report.md

- name: Upsert pull request comment
uses: actions/github-script@v7
with:
script: |
const fs = require("fs");
const body = fs.readFileSync("i18n-report.md", "utf8");
const marker = "<!-- i18n-translation-drift -->";
const finalBody = `${marker}\n${body}`;

const { owner, repo } = context.repo;
const issue_number = context.issue.number;

const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
});

const existing = comments.find((comment) =>
comment.user?.type === "Bot" && comment.body?.includes(marker)
);

if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body: finalBody,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: finalBody,
});
}

- name: Upload i18n report artifact
uses: actions/upload-artifact@v4
with:
name: i18n-report-pr
path: |
i18n-report.json
i18n-report.md

create-issue-on-master:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Generate i18n drift report
run: |
node scripts/i18n-drift-check.mjs \
--base "${{ github.event.before }}" \
--head "${{ github.sha }}" \
--json-out i18n-report.json \
--markdown-out i18n-report.md

- name: Create issue when drift exists
uses: actions/github-script@v7
with:
script: |
const fs = require("fs");
const report = JSON.parse(fs.readFileSync("i18n-report.json", "utf8"));
const md = fs.readFileSync("i18n-report.md", "utf8");

if (!report.hasDrift) {
core.info("No translation drift detected; no issue created.");
return;
}

const { owner, repo } = context.repo;

const baseLabels = [
{ name: "i18n", color: "e4e669" },
{ name: "translation-needed", color: "e4e669" },
];
const localeLabels = report.locales.map((locale) => ({
name: `locale:${locale}`,
color: "0075ca",
}));
const allLabels = [...baseLabels, ...localeLabels];

for (const label of allLabels) {
try {
await github.rest.issues.createLabel({ owner, repo, ...label });
} catch {
// label already exists
}
}

// One running issue, not one per push: the same marker lookup the PR job uses for
// its comment. Later pushes with drift add their report to it as a comment.
const marker = "<!-- i18n-translation-drift-issue -->";
const labels = allLabels.map((l) => l.name);
const commitUrl = `${context.serverUrl}/${owner}/${repo}/commit/${context.sha}`;
const section = [`**Changes merged in [${context.sha.slice(0, 7)}](${commitUrl})**`, "", md].join("\n");

const openIssues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: "open",
labels: "translation-needed",
per_page: 100,
});
const existing = openIssues.find((issue) =>
!issue.pull_request && issue.user?.type === "Bot" && issue.body?.includes(marker)
);

if (existing) {
await github.rest.issues.addLabels({ owner, repo, issue_number: existing.number, labels });
await github.rest.issues.createComment({
owner,
repo,
issue_number: existing.number,
body: section,
});
core.info(`Added the report to the open issue #${existing.number}.`);
return;
}

const body = [
marker,
"This issue tracks translation updates needed after source documentation changes were merged to `master`.",
"Later merges that leave translations behind add their report here as a comment. Close it once the translations have caught up.",
"",
section,
"",
"Please coordinate locale updates with maintainers/reviewers.",
].join("\n");

await github.rest.issues.create({
owner,
repo,
title: "i18n: translation updates required",
body,
labels,
});

- name: Upload i18n report artifact
uses: actions/upload-artifact@v4
with:
name: i18n-report-master
path: |
i18n-report.json
i18n-report.md
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ basement_dist
devspace*
.devspace/**.*

# i18n drift report (generated artifact, do not commit)
i18n-report.json
i18n-report.md

# Add ignores
*IGNORE*
*ignore*
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
"scripts": {
"dev": "NODE_OPTIONS=--openssl-legacy-provider npx npx vuepress dev docs",
"build": "NODE_OPTIONS='--max-old-space-size=8192' npx npx vuepress build docs",
"i18n:check": "node scripts/i18n-drift-check.mjs --base origin/master --working-tree --markdown-out i18n-report.md --json-out i18n-report.json",
"i18n:audit": "node scripts/i18n-drift-check.mjs --audit",
"i18n:baseline": "node scripts/i18n-drift-check.mjs --audit --update-baseline",
"i18n:stamp": "node scripts/i18n-drift-check.mjs --stamp",
"i18n:explain": "node scripts/i18n-drift-check.mjs --explain",
"i18n:test": "node --test scripts/i18n-drift-check.test.mjs",
"build:plantuml:all": "./scripts/_build_plantuml.sh",
"build:plantuml:diff": "MODE=STAGED_GIT ./scripts/_build_plantuml.sh",
"lint": "npx markdownlint './docs/**/*.md' --ignore node_modules --config markdownlint.yaml",
Expand Down
Loading
Loading