Fix Colombian Spanish Transifex mapping and duplicate target uploads

Map es-CO to supported es_CO and prefer explicit local mappings when a legacy alias shares the remote code. Report skipped aliases rather than overwriting one target twice. Offline force-upload tests pass; no remote upload was run. Nine local alias differences still need reconciliation.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu committed 2026-09-13 18:27:46 +03:00
1 parent 46a8b785f8
commit 27a566a0d5
5 files changed
+33 -7

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
[main]
host = https://www.transifex.com
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja-JP, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
lang_map = te_IN: te-IN, es_AR: es-AR, es_CO: es-CO, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja-JP, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
[o:wekan:p:wekan:r:application]
file_filter = imports/i18n/data/<lang>.i18n.json
+8 -1
View File
@@ -6,7 +6,7 @@ Run the explicit upload operation from any directory:
releases/translations/push-all-translations.sh
```
It uploads the English source and every other local language file, including
It uploads the English source and every distinct remote target language, including
regional English variants. Local translation values replace the corresponding
remote values, irrespective of file or remote modification times. Source strings
edited remotely are also replaced by the local source. This operation publishes
@@ -47,3 +47,10 @@ The upload implementation follows the [Transifex API v3 documentation](https://t
It bypasses the CLI's timestamp skip rather than using an undocumented force
attribute. The [CLI documentation](https://developers.transifex.com/docs/cli)
describes `--force` as disabling that timestamp skip.
Colombian Spanish maps `es-CO.i18n.json` to Transifex `es_CO`; `es-CO`
is not a supported Transifex code. The legacy `es_CO.i18n.json` file is an alias
of that remote target. The explicit mapping selects `es-CO.i18n.json`, and the
script reports the alias instead of uploading the same target twice. Both local
files remain available; their nine differing values still require reconciliation.
The offline dry run reports 244 distinct targets and the English source.
@@ -130,6 +130,11 @@ async function main() {
const config = readConfig();
if (!config.sourceFile) throw new Error('Missing source_file in .tx/config');
const languages = localLanguages(config);
for (const language of languages) {
for (const alias of language.aliases || []) {
console.log(`[tx] alias ${alias}.i18n.json -> ${language.code}; using mapped ${language.file}.i18n.json`);
}
}
const source = JSON.parse(fs.readFileSync(config.sourceFile, 'utf8'));
if (args.includes('--dry-run')) {
const failures = [];
@@ -61,12 +61,21 @@ export function readConfig() {
// Every language WeKan ships, as the code Transifex knows it by. English is the
// SOURCE language, so it is not a target and is left out.
export function localLanguages({ localToTx, sourceLanguage = 'en' }) {
return readdirSync(DATA_DIR)
const targets = new Map();
const files = readdirSync(DATA_DIR)
.filter(f => f.endsWith('.i18n.json'))
.map(f => f.replace('.i18n.json', ''))
.filter(name => name !== sourceLanguage)
.map(name => ({ file: name, code: localToTx.get(name) || name }))
.sort((a, b) => a.code.localeCompare(b.code));
.filter(name => name !== sourceLanguage);
// Explicit filename mappings win over a second file named after the same
// remote code. One remote language cannot retain two different uploads.
files.sort((a, b) => Number(localToTx.has(b)) - Number(localToTx.has(a)) || a.localeCompare(b));
for (const file of files) {
const code = localToTx.get(file) || file;
const target = targets.get(code);
if (target) (target.aliases ||= []).push(file);
else targets.set(code, { file, code });
}
return [...targets.values()].sort((a, b) => a.code.localeCompare(b.code));
}
// ── the token ───────────────────────────────────────────────────────────────
+6 -1
View File
@@ -12,6 +12,11 @@ const root = path.resolve(__dirname, '..');
assert.ok(localLanguages(config).some(row => row.file === 've-PP' && row.code === 'vep'));
assert.ok(localLanguages(config).some(row => row.file === 'en-GB' && row.code === 'en_GB'));
assert.ok(!localLanguages(config).some(row => row.file === 'en'));
const colombian = localLanguages(config).filter(row => row.code === 'es_CO');
assert.equal(colombian.length, 1, 'upload each remote language only once');
assert.equal(colombian[0].file, 'es-CO', 'explicit mapping chooses canonical local file');
assert.deepEqual(colombian[0].aliases, ['es_CO']);
assert.ok(!localLanguages(config).some(row => row.code === 'es-CO'), 'never register an unsupported hyphenated code');
const content = JSON.stringify({ title: 'Title __card__', empty: '' });
const source = JSON.parse(content);
assert.doesNotThrow(() => validateTranslation(content, source), 'intentional empty source strings are valid');
@@ -66,6 +71,6 @@ const root = path.resolve(__dirname, '..');
assert.equal(network, 0, 'pagination cannot send credentials to another origin');
const dry = cp.spawnSync('sh', [path.join(root, 'releases/translations/push-all-translations.sh'), '--dry-run'], { cwd: path.join(root, '.tools/tmp'), env: { ...process.env, NODE_BIN: process.execPath }, encoding: 'utf8' });
assert.equal(dry.status, 0, dry.stderr);
assert.match(dry.stdout, /245 targets and source en; no network requests/);
assert.match(dry.stdout, /244 targets and source en; no network requests/);
console.log('transifexForcePush: full uploads, polling, mapped languages, additive registration, failure continuation and summary, token origin boundary and offline wrapper passed');
})().catch(error => { console.error(error); process.exitCode = 1; });