File cockpit-po-plugin.js of Package cockpit-docker

import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';

import gettext_parser from "gettext-parser";
import pkg from 'glob';
const { glob } = pkg;
import Jed from "jed";

const config = {};

const DEFAULT_WRAPPER = 'cockpit.locale(PO_DATA);';

function get_po_files() {
    try {
        const linguas_file = path.resolve(config.srcdir, "po/LINGUAS");
        const linguas = fs.readFileSync(linguas_file, 'utf8').match(/\S+/g);
        return linguas.map(lang => path.resolve(config.srcdir, 'po', lang + '.po'));
    } catch (error) {
        if (error.code !== 'ENOENT') {
            throw error;
        }

        /* No LINGUAS file?  Fall back to globbing.
         * Note: we won't detect .po files being added in this case.
         */
        return glob.sync(path.resolve(config.srcdir, 'po/*.po'));
    }
}

function get_plural_expr(statement) {
    try {
        /* Check that the plural forms isn't being sneaky since we build a function here */
        Jed.PF.parse(statement);
    } catch (ex) {
        console.error("bad plural forms: " + ex.message);
        process.exit(1);
    }

    const expr = statement.replace(/nplurals=[1-9]; plural=([^;]*);?$/, '(n) => $1');
    if (expr === statement) {
        console.error("bad plural forms: " + statement);
        process.exit(1);
    }

    return expr;
}

function buildFile(po_file, subdir, filename, filter) {
    return new Promise((resolve, reject) => {
        // Read the PO file, remove fuzzy/disabled lines to avoid tripping up the validator
        const po_data = fs.readFileSync(po_file, 'utf8')
                .split('\n')
                .filter(line => !line.startsWith('#~'))
                .join('\n');
        const parsed = gettext_parser.po.parse(po_data, { defaultCharset: 'utf8', validation: true });
        delete parsed.translations[""][""]; // second header copy

        const rtl_langs = ["ar", "fa", "he", "ur"];
        const dir = rtl_langs.includes(parsed.headers.Language) ? "rtl" : "ltr";

        // cockpit.js only looks at "plural-forms" and "language"
        const chunks = [
            '{\n',
            ' "": {\n',
            `  "plural-forms": ${get_plural_expr(parsed.headers['Plural-Forms'])},\n`,
            `  "language": "${parsed.headers.Language}",\n`,
            `  "language-direction": "${dir}"\n`,
            ' }'
        ];
        for (const [msgctxt, context] of Object.entries(parsed.translations)) {
            const context_prefix = msgctxt ? msgctxt + '\u0004' : ''; /* for cockpit.ngettext */

            for (const [msgid, translation] of Object.entries(context)) {
                /* Only include msgids which appear in this source directory */
                const references = translation.comments.reference.split(/\s/);
                if (!references.some(str => str.startsWith(`pkg/${subdir}`) || str.startsWith(config.src_directory) || str.startsWith(`pkg/lib`)))
                    continue;

                if (translation.comments.flag?.match(/\bfuzzy\b/))
                    continue;

                if (!references.some(filter))
                    continue;

                const key = JSON.stringify(context_prefix + msgid);
                // cockpit.js always ignores the first item
                chunks.push(`,\n ${key}: [\n  null`);
                for (const str of translation.msgstr) {
                    chunks.push(',\n  ' + JSON.stringify(str));
                }
                chunks.push('\n ]');
            }
        }
        chunks.push('\n}');

        const wrapper = config.wrapper?.(subdir) || DEFAULT_WRAPPER;
        const output = wrapper.replace('PO_DATA', chunks.join('')) + '\n';

        const out_path = path.join(subdir ? (subdir + '/') : '', filename);
        fs.writeFileSync(path.resolve(config.outdir, out_path), output);
        return resolve();
    });
}

function init(options) {
    config.srcdir = process.env.SRCDIR || './';
    config.subdirs = options.subdirs || [''];
    config.src_directory = options.src_directory || 'src';
    config.wrapper = options.wrapper;
    config.outdir = options.outdir || './dist';
}

function run() {
    const promises = [];
    for (const subdir of config.subdirs) {
        for (const po_file of get_po_files()) {
            const lang = path.basename(po_file).slice(0, -3);
            promises.push(Promise.all([
                // Separate translations for the manifest.json file and normal pages
                buildFile(po_file, subdir, `po.${lang}.js`, str => !str.includes('manifest.json')),
                buildFile(po_file, subdir, `po.manifest.${lang}.js`, str => str.includes('manifest.json'))
            ]));
        }
    }
    return Promise.all(promises);
}

export const cockpitPoEsbuildPlugin = options => ({
    name: 'cockpitPoEsbuildPlugin',
    setup(build) {
        init({ ...options, outdir: build.initialOptions.outdir });
        build.onEnd(async result => { result.errors.length === 0 && await run() });
    },
});
openSUSE Build Service is sponsored by