Skip to content

Instantly share code, notes, and snippets.

@mche
Created July 21, 2023 08:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mche/dae0cbfe7097f42c41ff09024960d036 to your computer and use it in GitHub Desktop.
Save mche/dae0cbfe7097f42c41ff09024960d036 to your computer and use it in GitHub Desktop.
Экспорт в xlsx po-файлов переводов в указанной папке. Также обязательно наличие актуального template.pot
/* 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