Skip to content

Commit 40b657e

Browse files
committed
Stale translation warning
1 parent b4c7528 commit 40b657e

File tree

4 files changed

+342
-6
lines changed

4 files changed

+342
-6
lines changed

.github/workflows/deploy-web.yml

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,17 @@ jobs:
2929
with:
3030
node-version: '18'
3131

32+
- name: Check translation label
33+
id: translation-label
34+
env:
35+
GITHUB_TOKEN: ${{ github.token }}
36+
run: |
37+
LABEL_RESULT=$(node .github/workflows/scripts/check-translation-label.js --repo "${{ github.repository }}" --token "$GITHUB_TOKEN" --sha "${{ github.sha }}" --label "docs:translation-impact")
38+
echo "$LABEL_RESULT" >> "$GITHUB_OUTPUT"
39+
3240
- name: Get changed English documentation files
3341
id: changed-files
42+
if: steps.translation-label.outputs.label_present == 'true'
3443
run: |
3544
set -euo pipefail
3645
BEFORE="${{ github.event.before }}"
@@ -49,17 +58,27 @@ jobs:
4958
if [ -z "$CHANGED_FILES" ]; then
5059
echo "has_changes=false" >> "$GITHUB_OUTPUT"
5160
else
52-
printf '%s\n' "$CHANGED_FILES" > changed_english_docs.txt
53-
echo "has_changes=true" >> "$GITHUB_OUTPUT"
61+
if [ -n "$DIFF_BASE" ]; then
62+
printf '%s\n' "$CHANGED_FILES" > changed_english_docs_raw.txt
63+
node .github/workflows/scripts/filter-small-doc-changes.js "$DIFF_BASE" "${{ github.sha }}" changed_english_docs_raw.txt changed_english_docs.txt
64+
if [ -s changed_english_docs.txt ]; then
65+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
66+
else
67+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
68+
fi
69+
else
70+
printf '%s\n' "$CHANGED_FILES" > changed_english_docs.txt
71+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
72+
fi
5473
fi
5574
5675
- name: Mark translations as outdated
57-
if: steps.changed-files.outputs.has_changes == 'true'
76+
if: steps.translation-label.outputs.label_present == 'true' && steps.changed-files.outputs.has_changes == 'true'
5877
run: |
5978
node .github/workflows/scripts/mark-translations-outdated.js
6079
6180
- name: Check if translations were modified
62-
if: steps.changed-files.outputs.has_changes == 'true'
81+
if: steps.translation-label.outputs.label_present == 'true' && steps.changed-files.outputs.has_changes == 'true'
6382
id: check-changes
6483
run: |
6584
if [ -n "$(git status --porcelain)" ]; then
@@ -69,15 +88,15 @@ jobs:
6988
fi
7089
7190
- name: Commit changes
72-
if: steps.check-changes.outputs.translations_modified == 'true'
91+
if: steps.translation-label.outputs.label_present == 'true' && steps.check-changes.outputs.translations_modified == 'true'
7392
run: |
7493
git config --local user.email "[email protected]"
7594
git config --local user.name "omp-bot"
7695
git add frontend/i18n/
7796
git commit -m "Mark translations as potentially outdated (post-merge)"
7897
7998
- name: Push changes
80-
if: steps.check-changes.outputs.translations_modified == 'true'
99+
if: steps.translation-label.outputs.label_present == 'true' && steps.check-changes.outputs.translations_modified == 'true'
81100
run: |
82101
git push origin HEAD:${{ github.ref }}
83102
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env node
2+
3+
const https = require('https');
4+
5+
const args = process.argv.slice(2);
6+
const options = {
7+
repo: process.env.GITHUB_REPOSITORY || '',
8+
token: process.env.GITHUB_TOKEN || '',
9+
sha: process.env.GITHUB_SHA || '',
10+
label: '',
11+
pr: '',
12+
};
13+
14+
for (let i = 0; i < args.length; i += 2) {
15+
const key = args[i];
16+
const value = args[i + 1] || '';
17+
switch (key) {
18+
case '--repo':
19+
options.repo = value;
20+
break;
21+
case '--token':
22+
options.token = value;
23+
break;
24+
case '--sha':
25+
options.sha = value;
26+
break;
27+
case '--label':
28+
options.label = value;
29+
break;
30+
case '--pr':
31+
options.pr = value;
32+
break;
33+
default:
34+
i -= 1;
35+
}
36+
}
37+
38+
if (!options.repo || !options.token || !options.label) {
39+
console.error('Missing required arguments: repo, token, and label');
40+
process.exit(1);
41+
}
42+
43+
const githubRequest = (path, accept) =>
44+
new Promise((resolve, reject) => {
45+
const req = https.request(
46+
{
47+
hostname: 'api.github.com',
48+
path,
49+
method: 'GET',
50+
headers: {
51+
Authorization: `Bearer ${options.token}`,
52+
'User-Agent': 'translations-workflow',
53+
Accept: accept || 'application/vnd.github+json',
54+
},
55+
},
56+
res => {
57+
let body = '';
58+
res.on('data', chunk => {
59+
body += chunk;
60+
});
61+
res.on('end', () => {
62+
if (res.statusCode >= 200 && res.statusCode < 300) {
63+
try {
64+
resolve(JSON.parse(body));
65+
} catch (err) {
66+
reject(err);
67+
}
68+
} else {
69+
reject(new Error(`GitHub API responded with ${res.statusCode}: ${body}`));
70+
}
71+
});
72+
}
73+
);
74+
req.on('error', reject);
75+
req.end();
76+
});
77+
78+
const detectPrNumber = async () => {
79+
if (options.pr) {
80+
return options.pr;
81+
}
82+
if (!options.sha) {
83+
return '';
84+
}
85+
86+
try {
87+
const pulls = await githubRequest(
88+
`/repos/${options.repo}/commits/${options.sha}/pulls`,
89+
'application/vnd.github.groot-preview+json'
90+
);
91+
if (Array.isArray(pulls) && pulls.length > 0) {
92+
return String(pulls[0].number);
93+
}
94+
} catch (error) {
95+
console.error(`Failed to determine PR number: ${error.message}`);
96+
}
97+
return '';
98+
};
99+
100+
const main = async () => {
101+
try {
102+
const prNumber = await detectPrNumber();
103+
if (!prNumber) {
104+
process.stdout.write('label_present=false');
105+
return;
106+
}
107+
108+
const issue = await githubRequest(`/repos/${options.repo}/issues/${prNumber}`);
109+
const labels = Array.isArray(issue.labels) ? issue.labels.map(label => label.name) : [];
110+
const hasLabel = labels.includes(options.label);
111+
process.stdout.write(`label_present=${hasLabel ? 'true' : 'false'}`);
112+
} catch (error) {
113+
console.error(`Failed to load labels: ${error.message}`);
114+
process.stdout.write('label_present=false');
115+
}
116+
};
117+
118+
main();
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const { execSync } = require('child_process');
5+
6+
const MEANINGFUL_CHAR_THRESHOLD = 10; // minimum characters to count the change as worthy
7+
8+
const [, , baseRef, headRef, inputPath, outputPath] = process.argv;
9+
10+
if (!headRef || !inputPath || !outputPath) {
11+
console.error('Usage: node filter-small-doc-changes.js <baseRef> <headRef> <inputList> <outputList>');
12+
process.exit(1);
13+
}
14+
15+
const fileList = fs
16+
.readFileSync(inputPath, 'utf8')
17+
.split('\n')
18+
.map(line => line.trim())
19+
.filter(Boolean);
20+
21+
const keptFiles = [];
22+
23+
const getFileLines = (ref, file) => {
24+
if (!ref) {
25+
return null;
26+
}
27+
28+
try {
29+
const content = execSync(`git show ${ref}:${file}`, { encoding: 'utf8' });
30+
return content.replace(/\r/g, '').split('\n');
31+
} catch {
32+
return null;
33+
}
34+
};
35+
36+
const frontmatterEndLine = lines => {
37+
if (!lines || lines[0] !== '---') {
38+
return 0;
39+
}
40+
41+
for (let i = 1; i < lines.length; i++) {
42+
if (lines[i] === '---') {
43+
return i + 1;
44+
}
45+
}
46+
return 0;
47+
};
48+
49+
const isTrivialLine = (lineText, lineNumber, frontmatterLimit) => {
50+
if (lineNumber > 0 && frontmatterLimit > 0 && lineNumber <= frontmatterLimit) {
51+
return true;
52+
}
53+
return lineText.trim().length === 0;
54+
};
55+
56+
const levenshtein = (a, b) => {
57+
if (a === b) {
58+
return 0;
59+
}
60+
const lenA = a.length;
61+
const lenB = b.length;
62+
if (lenA === 0) {
63+
return lenB;
64+
}
65+
if (lenB === 0) {
66+
return lenA;
67+
}
68+
69+
const matrix = Array.from({ length: lenA + 1 }, () => new Array(lenB + 1).fill(0));
70+
71+
for (let i = 0; i <= lenA; i++) {
72+
matrix[i][0] = i;
73+
}
74+
for (let j = 0; j <= lenB; j++) {
75+
matrix[0][j] = j;
76+
}
77+
78+
for (let i = 1; i <= lenA; i++) {
79+
for (let j = 1; j <= lenB; j++) {
80+
if (a[i - 1] === b[j - 1]) {
81+
matrix[i][j] = matrix[i - 1][j - 1];
82+
} else {
83+
matrix[i][j] = Math.min(
84+
matrix[i - 1][j] + 1,
85+
matrix[i][j - 1] + 1,
86+
matrix[i - 1][j - 1] + 1
87+
);
88+
}
89+
}
90+
}
91+
92+
return matrix[lenA][lenB];
93+
};
94+
95+
const hasMeaningfulDiff = (file, diffOutput) => {
96+
const baseLines = getFileLines(baseRef, file);
97+
const headLines = getFileLines(headRef, file);
98+
99+
if (!diffOutput.trim()) {
100+
return false;
101+
}
102+
103+
if (!baseLines) {
104+
return true;
105+
}
106+
107+
const baseFrontmatterLimit = frontmatterEndLine(baseLines);
108+
const headFrontmatterLimit = frontmatterEndLine(headLines);
109+
110+
let currentOldLine = 0;
111+
let currentNewLine = 0;
112+
const diffLines = diffOutput.split('\n');
113+
const pendingRemoved = [];
114+
let totalChangedChars = 0;
115+
116+
for (const diffLine of diffLines) {
117+
if (diffLine.startsWith('@@')) {
118+
const match = diffLine.match(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
119+
if (match) {
120+
currentOldLine = Number(match[1]);
121+
currentNewLine = Number(match[3]);
122+
}
123+
continue;
124+
}
125+
126+
if (diffLine.startsWith('---') || diffLine.startsWith('+++')) {
127+
continue;
128+
}
129+
130+
if (diffLine.startsWith('-')) {
131+
const text = baseLines[currentOldLine - 1] ?? '';
132+
if (!isTrivialLine(text, currentOldLine, baseFrontmatterLimit)) {
133+
pendingRemoved.push(text);
134+
}
135+
currentOldLine++;
136+
continue;
137+
}
138+
139+
if (diffLine.startsWith('+')) {
140+
const text = headLines?.[currentNewLine - 1] ?? '';
141+
if (!isTrivialLine(text, currentNewLine, headFrontmatterLimit)) {
142+
if (pendingRemoved.length > 0) {
143+
const removed = pendingRemoved.shift();
144+
totalChangedChars += levenshtein(removed, text);
145+
} else {
146+
totalChangedChars += text.trim().length;
147+
}
148+
if (totalChangedChars >= MEANINGFUL_CHAR_THRESHOLD) {
149+
return true;
150+
}
151+
}
152+
currentNewLine++;
153+
continue;
154+
}
155+
}
156+
157+
while (pendingRemoved.length > 0) {
158+
totalChangedChars += pendingRemoved.shift().trim().length;
159+
if (totalChangedChars >= MEANINGFUL_CHAR_THRESHOLD) {
160+
return true;
161+
}
162+
}
163+
164+
return totalChangedChars >= MEANINGFUL_CHAR_THRESHOLD;
165+
};
166+
167+
for (const file of fileList) {
168+
if (!baseRef) {
169+
keptFiles.push(file);
170+
continue;
171+
}
172+
173+
let diffOutput = '';
174+
try {
175+
diffOutput = execSync(`git diff ${baseRef} ${headRef} --unified=0 -- ${file}`, {
176+
encoding: 'utf8',
177+
stdio: ['ignore', 'pipe', 'ignore'],
178+
});
179+
} catch (error) {
180+
diffOutput = error.stdout?.toString() ?? '';
181+
if (!diffOutput && !error.stdout) {
182+
keptFiles.push(file);
183+
continue;
184+
}
185+
}
186+
187+
if (hasMeaningfulDiff(file, diffOutput)) {
188+
keptFiles.push(file);
189+
}
190+
}
191+
192+
fs.writeFileSync(outputPath, keptFiles.join('\n'), 'utf8');

.github/workflows/scripts/mark-translations-outdated.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ A versão em inglês deste documento foi atualizada recentemente. Esta traduçã
1717
Ajude-nos a manter nossas traduções atualizadas! Se você é fluente neste idioma, considere revisar a [versão em inglês](ENGLISH_DOC_LINK) e atualizar esta tradução.
1818
:::
1919
20+
`,
21+
'ru': `:::warning Этот перевод может быть устаревшим.
22+
Английская версия этой статьи была недавно обновлена. Данный перевод может всё ещё не отражать эти изменения.
23+
24+
Помогите нам поддерживать актуальность переводов! Если вы свободно владеете английским языком, пожалуйста, рассмотрите возможность проверки [английской версии](ENGLISH_DOC_LINK) и обновления этого перевода.
25+
:::
26+
2027
`,
2128
};
2229

0 commit comments

Comments
 (0)