Created
July 21, 2023 08:05
-
-
Save mche/dae0cbfe7097f42c41ff09024960d036 to your computer and use it in GitHub Desktop.
Экспорт в xlsx po-файлов переводов в указанной папке. Также обязательно наличие актуального template.pot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-trailing-spaces */ | |
/* eslint-disable no-magic-numbers */ | |
/* eslint-disable line-comment-position */ | |
/* tslint:disable */ | |
console.info(` | |
_ _ | |
___ __ __ _ __ ___ _ __ | |_ _ __ ___ (_) ___ | |
/ _ \\ \\ \\/ / | '_ \\ / _ \\ | '__| | __| _____ | '_ \\ / _ \\ | | / __| | |
| __/ > < | |_) | | (_) | | | | |_ |_____| | |_) | | (_) | _ | | \\__ \\ | |
\\___| /_/\\_\\ | .__/ \\___/ |_| \\__| | .__/ \\___/ (_) _/ | |___/ | |
|_| |_| |__/ | |
`); | |
const { getHashDigest } = require('loader-utils'); | |
const { glob } = require('glob'); | |
const fs = require('fs'); | |
const gt = require('gettext-parser'); | |
const Excel = require('exceljs'); | |
const path = require('path'); | |
const {promises: {readFile}} = require("fs"); | |
const argv = require('minimist')(process.argv.slice(2)); | |
if (!argv['po-dir']) { | |
console.error('Не указан путь к папке po-файлов: --po-dir="./app/i18n/gettext"'); | |
console.info(` | |
Под версию nodejs 18. | |
1. Подливает новые переводы из шаблона .pot во все найденные po-файлы. | |
2. Экспортирует в указанную папку po-файлы в Ексель. | |
3. Если в переводах используется формат с контекстом (реакт), то НЕсгенерированные контексты в экспорте помечаются отдельной колонкой галочкой ✅. | |
node export-po.js <опции> | |
Опции (с примерами): | |
--po-dir="./app/i18n/gettext" // Путь к папке с po-файлами (обязательно) | |
--template-pot="./app/i18n/gettext/template.pot" // Путь к pot-шаблону (по умолчанию ищется в папке {{--po-dir}}/template.pot) | |
--export-dir="/app/i18n/export" // Путь к папке экспорта (по умолчанию в --po-dir) | |
`); | |
return; | |
} | |
if (!argv['template-pot']) { | |
// console.warn('Не указан путь к шаблону template.pot: --template-pot="./app/i18n/gettext/template.pot"'); | |
argv['template-pot'] = `${argv['po-dir']}/template.pot`; | |
if (!fs.existsSync(argv['template-pot'])) { | |
console.error(`Нет шаблона ${argv['template-pot']}. Или укажите другой путь --template-pot="./app/i18n/gettext/template.pot"`); | |
return; | |
} | |
} | |
if (!argv['export-dir']) { | |
argv['export-dir'] ||= argv['po-dir']; | |
} | |
if (!fs.existsSync(argv['export-dir'])) { | |
console.error(`Нет папки --export-dir="${argv['export-dir']}"`); | |
return; | |
} | |
const reDeletedTranslations = /(?:^#\S+ .+?\r?\n)+\r?\n/gm; | |
// const poColumns = ['msgid', 'msgctxt', 'comments', 'msgstr']; | |
const p = fs.readFileSync(argv['template-pot'], 'utf-8', 'r'); | |
const pot = gt.po.parse(p); | |
/* | |
Ключи в po[t].translations: пустая строка - переводы без контекста, строки контекстов(реакт) или ид константы | |
Нужно по шаблону pot находить переводы в po. | |
Если перевод не найден в соотв. контексте, то попытаться найти везде, и если найдено, | |
но поставить флаг fuzzy | |
*/ | |
function processPo(fileContent, file) { | |
// иногда удаленные переводы почему-то мешают gettext-parser (нет идеала) | |
// eslint-disable-next-line no-param-reassign | |
fileContent = fileContent.replace(reDeletedTranslations, '').trim() + '\n'; | |
const po = gt.po.parse(fileContent); | |
// разобрать структуру po для быстрого поиска | |
// каждой константе массив вариантов контекстов | |
const oldTranslations = {}; | |
Object.keys(po.translations).forEach(key1 => { | |
Object.keys(po.translations[key1]).forEach(key2 => { | |
const tr = po.translations[key1][key2]; | |
if (!tr.msgid) { | |
return; | |
} | |
oldTranslations[tr.msgid] ||= []; | |
oldTranslations[tr.msgid].push(tr); | |
}); | |
}); | |
const workbook = new Excel.Workbook(); | |
const wbSheet = workbook.addWorksheet(po.headers.Language.toLocaleUpperCase()); | |
// чет проблемы с динамическим объявлением колонок | |
wbSheet.columns = [ | |
{ header: 'EN', key: 'msgid', width: 100 }, | |
{ header: po.headers.Language.toLocaleUpperCase(), key: 'msgstr', width: 100 }, | |
{ header: 'Context', key: 'msgctxt', width: 10, outlineLevel: 1 }, | |
// { header: 'Comment', key: 'comment', width: 10 } | |
]; | |
// слияние шаблона с переводом | |
Object.keys(pot.translations).forEach(key1 => { | |
Object.keys(pot.translations[key1]).forEach(key2 => { | |
const item = pot.translations[key1][key2]; | |
const oldItems = oldTranslations[item.msgid]; | |
if (!item.msgid) { | |
return; | |
} | |
if (oldItems) { | |
// уже переведено, но возможно разные контексты | |
const tr = oldItems.find(old => item.msgctxt === old.msgctxt); | |
if (tr) { | |
item.msgstr = tr.msgstr; | |
item.comments = tr.comments || {}; | |
delete item.comments?.extracted; | |
} else { | |
// взять первый перевод и в комментах прописать контексты | |
item.msgstr = oldItems[0].msgstr; | |
item.comments = oldItems[0].comments || {}; | |
item.comments.extracted = `[${oldItems.map(o => o.msgctxt || 'без контекста').join('], [')}]`; | |
// если несколько контекстов, то фуззи? | |
item.comments.flag ||= oldItems.length === 1 ? undefined : 'fuzzy'; | |
} | |
} | |
const row = { | |
msgid: item.msgid, | |
// пробелом? | |
msgstr: item.comments?.flag === 'fuzzy' ? undefined : item.msgstr?.join(' ') | |
}; | |
delete item.comments?.reference; | |
// проверить автогенерацию контекста | |
if (item.msgctxt && item.msgid) { | |
const dig = getHashDigest(item.msgid, 'sha512', 'base64', 6); | |
if (item.msgctxt === dig) { | |
delete item.comments.extracted; | |
} else { | |
// item.comments ||= {}; | |
// item.comments.extracted = `✅\n${item.comments.extracted || ''}`; | |
// row.comment = '✅'; | |
row.msgctxt = item.msgctxt; | |
} | |
} | |
wbSheet.addRow(row); | |
}); | |
}); | |
// заменить перевод с шаблоном | |
po.translations = pot.translations; | |
if (argv['export-dir'] !== argv['po-dir']) { | |
fs.writeFileSync(file + '-new.po', gt.po.compile(po), { encoding: 'utf-8' }); | |
} | |
// | |
const xlsxFile = `${argv['export-dir']}/${path.basename(file)}.xlsx`; | |
workbook.xlsx.writeFile(xlsxFile); | |
console.info(' ✅ ' + file); | |
} | |
glob(`${argv['po-dir']}/*.po`).then(poFiles => | |
Promise.all(poFiles.map(f => readFile(f, 'utf-8').then(fileContent => processPo(fileContent, f)))) | |
.then(() => { | |
console.info(` | |
____ | |
/ __ \\ _ | |
____ ___ _ __ ___ ___ / /_/ / ___ | | | |
| __| / _ \\ | '_ \` _ \\ / _ \\ | ___ \\ / _ \\ | | | |
| | | (_) || | | | | || (_) || |_/ /| (_) | |_| | |
|_| \\___/ |_| |_| |_| \\___/ \\____/ \\___/ (_) | |
`); | |
}).catch(error => { | |
console.error(error.message); | |
process.exit(1); | |
}) | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment