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('locale/_base/translations.suml'); const config = await loadSuml('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); };