Skip to content

Commit

Permalink
Add automated translation checking
Browse files Browse the repository at this point in the history
  • Loading branch information
brentvollebregt committed Feb 25, 2025
1 parent 0aa7851 commit 7b1a8f2
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE/translation.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ _Insert a brief description about what your change is / does._
**Translation checklist**

- [ ] I have ran the application to make sure my code runs
- [ ] I have used the correct [ISO 639-1 code](https://www.alchemysoftware.com/livedocs/ezscript/Topics/Catalyst/Language.htm) for my translations (e.g. `en`)
- [ ] I have used the correct [ISO 639-1 code](https://localizely.com/iso-639-1-list/) for my translations (e.g. `en`)
- [ ] I have formatted all changes as described in the [contribution style guide](https://github.com/brentvollebregt/auto-py-to-exe/blob/master/CONTRIBUTING.md#style-guide)
- [ ] I have added the language to `supportedLanguages` in alphabetical order
- [ ] I have added the language to the table in [README.md](https://github.com/brentvollebregt/auto-py-to-exe/blob/master/README.md#translations) in alphabetical order
Expand Down
159 changes: 159 additions & 0 deletions .github/workflow_utils/check-translations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
const I18N_LOCATION = '../../auto_py_to_exe/web/js/i18n.js';
const README_LOCATION = '../../README.md';

const fs = require('fs');
const { supportedLanguages, translationMap } = require(I18N_LOCATION);
const readmeContent = fs.readFileSync(README_LOCATION, 'utf8');

// Build up a report to return
let report = 'Basic checks:';

// Validate if languages are sorted by name

let supportedLanguagesSortedByNameError = null;
for (let i = 1; i < supportedLanguages.length; i++) {
if (supportedLanguages[i - 1].name.localeCompare(supportedLanguages[i].name) > 0) {
supportedLanguagesSortedByNameError = `Sorting error: "${supportedLanguages[i - 1].name}" should be after "${
supportedLanguages[i].name
}"`;
}
}

if (supportedLanguagesSortedByNameError === null) {
report += '\n- ✔️ i18n.js translationMap is sorted correctly';
} else {
report += '\n- ❌ i18n.js translationMap is sorted correctly';
report += `\n\t- ${supportedLanguagesSortedByNameError}`;
}

// Validate the README translation table is sorted by name

function extractTranslationTable(content) {
const lines = content.split('\n');

let tableStart = false;
let tableLines = [];
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim() === '## Translations') {
tableStart = true;
i++; // Skip the next line (empty)
continue;
}

if (tableStart) {
if (lines[i].trim().startsWith('|')) {
tableLines.push(lines[i].trim());
} else {
break; // Stop when there are no more rows
}
}
}

// Skip the first two rows (header and ---) and then parse the data
const tableRows = [];
for (let i = 2; i < tableLines.length; i++) {
const [_, language, translator, translated] = tableLines[i].split('|').map((x) => x.trim());
tableRows.push({
language,
translator,
translated,
});
}

return tableRows;
}

const readmeTranslationsTableRows = extractTranslationTable(readmeContent);

let readmeTranslationsTableRowsSortedByLanguageError = null;
for (let i = 1; i < readmeTranslationsTableRows.length; i++) {
if (readmeTranslationsTableRows[i - 1].language.localeCompare(readmeTranslationsTableRows[i].language) > 0) {
readmeTranslationsTableRowsSortedByLanguageError = `Sorting error: "${
readmeTranslationsTableRows[i - 1].language
}" should be after "${readmeTranslationsTableRows[i].language}"`;
}
}

if (readmeTranslationsTableRowsSortedByLanguageError === null) {
report += '\n- ✔️ README.md translation table is sorted correctly';
} else {
report += '\n- ❌ README.md translation table is sorted correctly';
report += `\n\t- ${readmeTranslationsTableRowsSortedByLanguageError}`;
}

// Identify missing translations in i18n.js

const countMissingTranslations = (map, languages) => {
const missingPaths = Object.fromEntries(languages.map((lang) => [lang, []]));

const traverse = (node, path = []) => {
if (typeof node !== 'object' || node === null) return;

if (Object.keys(node).some((key) => languages.includes(key))) {
languages.forEach((lang) => {
if (!(lang in node)) {
missingPaths[lang].push(path.join('.'));
}
});
} else {
Object.entries(node).forEach(([key, value]) => traverse(value, [...path, key]));
}
};

traverse(map);
return missingPaths;
};

const allMissingTranslationPaths = countMissingTranslations(
translationMap,
supportedLanguages.map((l) => l.code)
);

// Match the i18n.js translations with the README translations table

const translationMergeErrors = [];

const mergedTranslations = supportedLanguages.map((l) => {
const missingTranslationPaths = allMissingTranslationPaths[l.code];
const readmeRow = readmeTranslationsTableRows.find((r) => r.language === l.name);

if (missingTranslationPaths === undefined) {
translationMergeErrors.push(`Unable to find missing translation count for code "${l.code}"`);
}
if (readmeRow === undefined) {
translationMergeErrors.push(`Unable to find README row for language "${l.name}"`);
}

return {
...l,
missingTranslationPaths,
readmeRow,
};
});

if (translationMergeErrors.length > 0) {
report += '\n\nTranslation Errors:';
translationMergeErrors.forEach((e) => {
report += `\n- ${e}`;
});
} else {
report += '\n\nTranslations:';
report += '\n\n| Name | Code | i18n.js Missing Count | Translator | Translated |';
report += '\n| ---- | ---- | --------------------- | ---------- | ---------- |';
mergedTranslations.forEach((t) => {
const missingWithWarning =
t.missingTranslationPaths.length === 0
? t.missingTranslationPaths.length
: `${t.missingTranslationPaths.length} ⚠️`;
report += `\n| ${t.name} | ${t.code} | ${missingWithWarning} | ${t.readmeRow.translator} | ${t.readmeRow.translated} |`;
});
}

report += '\n\nWarnings:';
mergedTranslations.forEach((t) => {
t.missingTranslationPaths.forEach((path) => {
report += `\n- ⚠️ ${t.name} (${t.code}) is missing a translation for ${path}`;
});
});

console.log(report);
11 changes: 11 additions & 0 deletions .github/workflows/check-formatting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,14 @@ jobs:
prettier_version: '2.8.8'
dry: true
prettier_options: '--check .'

translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4

- name: Run check-translations.js
run: node check-translations.js >> $GITHUB_OUTPUT
working-directory: ./.github/workflow_utils
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,10 @@ If you believe you've found an issue with this tool, please follow the ["Reporti
| Persian (فارسی) | [DrunkLeen](https://github.com/drunkleen), [Ar.dst](https://github.com/Ar-dst) | UI and [README](./README-Persian.md) |
| Polish (Polski) | [Akuczaku](https://github.com/Akuczaku) | UI |
| Russian (Русский) | Oleg | UI |
| Serbian | [rina](https://github.com/sweatshirts) | UI |
| Serbian (Srpski) | [rina](https://github.com/sweatshirts) | UI |
| Slovak (Slovenčina) | [mostypc123](https://github.com/mostypc123) | UI |
| Spanish (Español) | [enriiquee](https://github.com/enriiquee) | UI |
| Spanish Latam (Español Latam) | [Matyrela](https://github.com/Matyrela) | UI |
| Spanish Latin America (Español Latam) | [Matyrela](https://github.com/Matyrela) | UI |
| Thai (ภาษาไทย) | [teerut26](https://github.com/teerut26) | UI (partial) |
| Turkish (Türkçe) | [mcagriaksoy](https://github.com/mcagriaksoy) | UI and [README](./README-Turkish.md) |
| Ukrainian (Українська) | [AndrejGorodnij](https://github.com/AndrejGorodnij) | UI |
Expand Down
7 changes: 6 additions & 1 deletion auto_py_to_exe/web/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -2416,7 +2416,7 @@ const supportedLanguages = [
code: 'he',
},
{
name: 'Hindi (हिंदी)',
name: 'Hindi (हिन्दी)',
code: 'hi',
},
{
Expand Down Expand Up @@ -2486,3 +2486,8 @@ const supportedLanguages = [
];

let currentLanguage = _checkLanguageIsSupportedOrDefault(_getLanguage()); // Keeps track of the current language

module.exports = {
translationMap,
supportedLanguages,
};

0 comments on commit 7b1a8f2

Please sign in to comment.