PronounsPage/helpers/merge/translationsHelper.ts
Valentyne Stigloher 10180aa6a3 (refactor) use #shared alias instead of ~~/shared
the #shared alias used by Nuxt cannot be easily disabled and to prevent breackage with jiti, we make use of it
2025-08-17 18:56:02 +02:00

103 lines
4.5 KiB
TypeScript

import fs from 'node:fs/promises';
import { deepListKeys } from '#shared/helpers.ts';
import { listMissingTranslations } from '#shared/missingTranslations.ts';
import { DictNode, parse } from '~/helpers/merge/sumlAst.ts';
import type { Node } from '~/helpers/merge/sumlAst.ts';
import type { Config } from '~~/locale/config.ts';
import type { Translations } from '~~/locale/translations.ts';
import { loadSuml } from '~~/server/loader.ts';
import { rootDir } from '~~/server/paths.ts';
export const loadDocument = async (name: string) => {
return parse(await fs.readFile(`${rootDir}/${name}`, 'utf-8'));
};
export const saveDocument = async (name: string, document: Node) => {
await fs.writeFile(`${rootDir}/${name}`, `${document.toString()}\n`);
};
export const mergeTranslationProposals = async (locale: string, proposalsFile: string) => {
const baseTranslationsDocument = await loadDocument('locale/_base/translations.suml');
const localeTranslationsDocument = await loadDocument(`locale/${locale}/translations.suml`);
const proposalsDocument = await loadDocument(proposalsFile);
if (!(proposalsDocument instanceof DictNode) ||
!(baseTranslationsDocument instanceof DictNode) ||
!(localeTranslationsDocument instanceof DictNode)) {
throw new Error('input nodes must be DictNode');
}
merge(proposalsDocument, baseTranslationsDocument, localeTranslationsDocument);
await saveDocument(`locale/${locale}/translations.suml`, localeTranslationsDocument);
};
export const merge = (source: DictNode, base: DictNode, target: DictNode) => {
const baseKeys = base.items.map((entry) => entry.key);
for (const sourceEntry of source.items) {
const baseEntry = base.items.find((baseEntry) => baseEntry.key === sourceEntry.key);
const targetEntry = target.items.find((targetEntry) => targetEntry.key === sourceEntry.key);
if (targetEntry === undefined) {
let insertIndex = 0;
if (baseEntry === undefined) {
insertIndex = target.items.length;
} else {
const baseIndex = baseKeys.indexOf(baseEntry.key);
const previousKeys = baseKeys.slice(0, baseIndex).toReversed();
for (const previousKey of previousKeys) {
const targetIndex = target.items.findIndex((targetEntry) => targetEntry.key === previousKey);
if (targetIndex !== -1) {
insertIndex = targetIndex + 1;
break;
}
}
}
target.items.splice(insertIndex, 0, sourceEntry);
} else if (targetEntry.value instanceof DictNode) {
if (!(sourceEntry.value instanceof DictNode)) {
throw new Error('source entry must be an DictNode when merging');
}
const childBase = baseEntry?.value instanceof DictNode ? baseEntry.value : new DictNode([], []);
merge(sourceEntry.value, childBase, targetEntry.value);
} else {
targetEntry.value = sourceEntry.value;
}
}
};
export const createTranslationFiles = async (locale: string) => {
const baseTranslations = await loadSuml<Translations>('locale/_base/translations.suml');
const config = await loadSuml<Config>('locale/_base/config.suml');
const configDocument = await loadDocument('locale/_base/config.suml');
const translationsDocument = await loadDocument('locale/_base/translations.suml');
const requiredTranslationKeys = listMissingTranslations({}, baseTranslations, config);
for (const key of deepListKeys(baseTranslations)) {
if (!requiredTranslationKeys.includes(key)) {
remove(translationsDocument, key.split('.'));
}
}
await fs.mkdir(`${rootDir}/locale/${locale}`, { recursive: true });
await saveDocument(`locale/${locale}/config.suml`, configDocument);
await saveDocument(`locale/${locale}/translations.suml`, translationsDocument);
};
export const remove = (node: Node, path: string[]) => {
if (!(node instanceof DictNode)) {
throw new Error('node must be DictNode');
}
if (path.length === 1) {
node.items = node.items.filter((item) => item.key !== path[0]);
return;
}
const childNode = node.items.find((item) => item.key === path[0])?.value;
if (!childNode) {
return;
}
remove(childNode, path.slice(1));
node.items = node.items.filter((item) => !(item.value instanceof DictNode) || item.value.items.length > 0);
};