From 5e583767d21a043eda17558f2fb2f55f7f606b6e Mon Sep 17 00:00:00 2001 From: Pontus Abrahamsson Date: Wed, 25 Dec 2024 23:09:04 +0100 Subject: [PATCH] Add support for yaml and po --- bun.lockb | Bin 165432 -> 165464 bytes examples/po/languine.config.mjs | 16 ++ examples/po/locales/en.po | 7 + examples/po/locales/es.po | 7 + examples/yaml/languine.config.mjs | 16 ++ examples/yaml/locales/en.yml | 48 +++++ examples/yaml/locales/es.yml | 35 ++++ packages/cli/package.json | 1 + packages/cli/src/commands/init.ts | 9 + packages/cli/src/envs.ts | 26 ++- packages/cli/src/translators/index.ts | 11 ++ packages/cli/src/translators/po.ts | 175 ++++++++++++++++++ .../cli/src/translators/xcode-xcstrings.ts | 2 - packages/cli/src/translators/yaml.ts | 158 ++++++++++++++++ 14 files changed, 508 insertions(+), 3 deletions(-) create mode 100644 examples/po/languine.config.mjs create mode 100644 examples/po/locales/en.po create mode 100644 examples/po/locales/es.po create mode 100644 examples/yaml/languine.config.mjs create mode 100644 examples/yaml/locales/en.yml create mode 100644 examples/yaml/locales/es.yml create mode 100644 packages/cli/src/translators/po.ts create mode 100644 packages/cli/src/translators/yaml.ts diff --git a/bun.lockb b/bun.lockb index 81b9ee25f58502a9cdbe62031cc3e05f738a60b1..7dee3d88196a921ebef263d1500aed902a8729e3 100755 GIT binary patch delta 17001 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 delta 16733 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 z { + const commentLines = + comments.length > 0 ? `${comments.join("\n")}\n` : ""; + return `${commentLines}msgid "${key}"\nmsgstr "${value}"`; + }) + .join("\n\n"); +} + +export const po: Translator = { + async onUpdate(options) { + const sourceEntries = parsePoFile(options.content); + const previousEntries = parsePoFile(options.previousContent); + const previousTranslationEntries = parsePoFile(options.previousTranslation); + + const sourceMap = new Map(sourceEntries.map((entry) => [entry.key, entry])); + const previousMap = new Map( + previousEntries.map((entry) => [entry.key, entry]), + ); + const prevTransMap = new Map( + previousTranslationEntries.map((entry) => [entry.key, entry]), + ); + + const addedKeys = sourceEntries + .filter(({ key, value }) => { + const prev = previousMap.get(key); + return !prev || prev.value !== value; + }) + .map((entry) => entry.key); + + if (addedKeys.length === 0) { + return { + summary: "No new keys to translate", + content: options.previousTranslation, + }; + } + + const toTranslate = Object.fromEntries( + addedKeys.map((key) => [key, sourceMap.get(key)!.value]), + ); + + const { object } = await generateObject({ + model: options.model, + temperature: options.config.llm?.temperature ?? 0, + prompt: getPrompt(JSON.stringify(toTranslate, null, 2), options), + schema: z.object({ + items: z.array(z.string().describe("Translated string value")), + }), + }); + + // Update translations while preserving order and comments + const updatedEntries = sourceEntries.map(({ key, comments }) => { + const translationIndex = addedKeys.indexOf(key); + const value = + translationIndex !== -1 + ? object.items[translationIndex] + : (prevTransMap.get(key)?.value ?? ""); + + return { key, value, comments }; + }); + + return { + summary: `Translated ${addedKeys.length} new keys`, + content: stringifyPoFile(updatedEntries), + }; + }, + + async onNew(options) { + const sourceEntries = parsePoFile(options.content); + const sourceStrings = Object.fromEntries( + sourceEntries.map((entry) => [entry.key, entry.value]), + ); + + const { object } = await generateObject({ + model: options.model, + prompt: getPrompt(JSON.stringify(sourceStrings, null, 2), options), + temperature: options.config.llm?.temperature ?? 0, + schema: z.object({ + items: z.array(z.string().describe("Translated string value")), + }), + }); + + const translatedEntries = sourceEntries.map((entry, index) => ({ + ...entry, + value: object.items[index], + })); + + return { + content: stringifyPoFile(translatedEntries), + }; + }, +}; + +function getPrompt(base: string, options: PromptOptions) { + const text = dedent` + ${baseRequirements} + - Preserve all msgid values exactly as they appear + - Only translate the msgstr values, not the msgid values + - Return translations as a JSON array of strings in the same order as input + - Maintain all format specifiers like %s, %d, etc. in the exact same order + - Preserve any HTML tags or special formatting in the strings + `; + + return createBasePrompt(`${text}\n${base}`, options); +} diff --git a/packages/cli/src/translators/xcode-xcstrings.ts b/packages/cli/src/translators/xcode-xcstrings.ts index e505e24..69d2785 100644 --- a/packages/cli/src/translators/xcode-xcstrings.ts +++ b/packages/cli/src/translators/xcode-xcstrings.ts @@ -181,8 +181,6 @@ export const xcodeXCStrings: Translator = { options.config.locale.source, ); - console.log("katt"); - const sourceKeys = Object.keys(sourceStrings); const { object } = await generateObject({ diff --git a/packages/cli/src/translators/yaml.ts b/packages/cli/src/translators/yaml.ts new file mode 100644 index 0000000..4badfcb --- /dev/null +++ b/packages/cli/src/translators/yaml.ts @@ -0,0 +1,158 @@ +import { generateObject } from "ai"; +import dedent from "dedent"; +import YAML from "yaml"; +import { z } from "zod"; +import { baseRequirements, createBasePrompt } from "../prompt.js"; +import type { PromptOptions, Translator } from "../types.js"; + +interface YamlEntry { + key: string; + value: string; +} + +function parseYamlFile(content: string) { + const doc = YAML.parse(content); + const entries: YamlEntry[] = []; + + function traverse(obj: Record, prefix = "") { + if (typeof obj === "object" && obj !== null) { + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key; + if (typeof value === "object" && value !== null) { + traverse(value as Record, fullKey); + } else { + entries.push({ + key: fullKey, + value: String(value), + }); + } + } + } + } + + traverse(doc); + return entries; +} + +function stringifyYamlFile(entries: YamlEntry[]) { + const doc = new YAML.Document(); + const obj: Record = {}; + + for (const { key, value } of entries) { + const parts = key.split("."); + let current = obj; + + // Build nested structure + for (let i = 0; i < parts.length - 1; i++) { + if (!current[parts[i]]) { + current[parts[i]] = {}; + } + current = current[parts[i]] as Record; + } + + const lastKey = parts[parts.length - 1]; + current[lastKey] = value; + } + + doc.contents = doc.createNode(obj); + return doc.toString(); +} + +export const yaml: Translator = { + async onUpdate(options) { + const sourceEntries = parseYamlFile(options.content); + const previousEntries = parseYamlFile(options.previousContent); + const previousTranslationEntries = parseYamlFile( + options.previousTranslation, + ); + + const sourceMap = new Map(sourceEntries.map((entry) => [entry.key, entry])); + const previousMap = new Map( + previousEntries.map((entry) => [entry.key, entry]), + ); + const prevTransMap = new Map( + previousTranslationEntries.map((entry) => [entry.key, entry]), + ); + + const addedKeys = sourceEntries + .filter(({ key, value }) => { + const prev = previousMap.get(key); + return !prev || prev.value !== value; + }) + .map((entry) => entry.key); + + if (addedKeys.length === 0) { + return { + summary: "No new keys to translate", + content: options.previousTranslation, + }; + } + + const toTranslate = Object.fromEntries( + addedKeys.map((key) => [key, sourceMap.get(key)!.value]), + ); + + const { object } = await generateObject({ + model: options.model, + temperature: options.config.llm?.temperature ?? 0, + prompt: getPrompt(JSON.stringify(toTranslate, null, 2), options), + schema: z.object({ + items: z.array(z.string().describe("Translated string value")), + }), + }); + + // Update translations while preserving order + const updatedEntries = sourceEntries.map(({ key }) => { + const translationIndex = addedKeys.indexOf(key); + const value = + translationIndex !== -1 + ? object.items[translationIndex] + : (prevTransMap.get(key)?.value ?? ""); + + return { key, value }; + }); + + return { + summary: `Translated ${addedKeys.length} new keys`, + content: stringifyYamlFile(updatedEntries), + }; + }, + + async onNew(options) { + const sourceEntries = parseYamlFile(options.content); + const sourceStrings = Object.fromEntries( + sourceEntries.map((entry) => [entry.key, entry.value]), + ); + + const { object } = await generateObject({ + model: options.model, + prompt: getPrompt(JSON.stringify(sourceStrings, null, 2), options), + temperature: options.config.llm?.temperature ?? 0, + schema: z.object({ + items: z.array(z.string().describe("Translated string value")), + }), + }); + + const translatedEntries = sourceEntries.map((entry, index) => ({ + ...entry, + value: object.items[index], + })); + + return { + content: stringifyYamlFile(translatedEntries), + }; + }, +}; + +function getPrompt(base: string, options: PromptOptions) { + const text = dedent` + ${baseRequirements} + - Preserve all YAML keys exactly as they appear + - Only translate the values, not the keys + - Return translations as a JSON array of strings in the same order as input + - Maintain all format specifiers like {name}, {count}, etc. in the exact same order + - Preserve any HTML tags or special formatting in the strings + `; + + return createBasePrompt(`${text}\n${base}`, options); +}