-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
508 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export default { | ||
version: "0.6.2", | ||
locale: { | ||
source: "en", | ||
targets: ["es"], | ||
}, | ||
files: { | ||
po: { | ||
include: ["locales/[locale].po"], | ||
}, | ||
}, | ||
llm: { | ||
provider: "openai", | ||
model: "gpt-4-turbo", | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# This is a PO file containing English strings that serve as the source for translations. | ||
# Each entry consists of a msgid (string identifier) and msgstr (translated string). | ||
msgid "welcome_message" | ||
msgstr "Welcome to our application!" | ||
|
||
msgid "error_message" | ||
msgstr "An error occurred: {error_code}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# This is a PO file containing English strings that serve as the source for translations. | ||
# Each entry consists of a msgid (string identifier) and msgstr (translated string). | ||
msgid "welcome_message" | ||
msgstr "Bienvenido a nuestra aplicación!" | ||
|
||
msgid "error_message" | ||
msgstr "Ocurrió un error: {error_code}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export default { | ||
version: "0.6.2", | ||
locale: { | ||
source: "en", | ||
targets: ["es"], | ||
}, | ||
files: { | ||
yaml: { | ||
include: ["locales/[locale].yml"], | ||
}, | ||
}, | ||
llm: { | ||
provider: "openai", | ||
model: "gpt-4-turbo", | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# General messages | ||
welcome: Welcome to our application! | ||
goodbye: Thanks for using our app. See you next time! | ||
|
||
# Navigation | ||
nav: | ||
home: Home | ||
about: About | ||
contact: Contact Us | ||
settings: Settings | ||
|
||
# User messages | ||
user: | ||
greeting: "Hello, {name}!" | ||
profile: | ||
title: Your Profile | ||
edit: Edit Profile | ||
save: Save Changes | ||
cancel: Cancel | ||
|
||
# Error messages | ||
errors: | ||
not_found: Page not found | ||
server_error: An error occurred on the server | ||
validation: | ||
required: This field is required | ||
email: Please enter a valid email address | ||
password: Password must be at least 8 characters | ||
|
||
# Form labels | ||
form: | ||
email: Email Address | ||
password: Password | ||
submit: Submit | ||
reset: Reset Form | ||
|
||
# Success messages | ||
success: | ||
saved: Changes saved successfully | ||
uploaded: File uploaded successfully | ||
deleted: Item deleted successfully | ||
|
||
# Time-related | ||
time: | ||
today: Today | ||
yesterday: Yesterday | ||
tomorrow: Tomorrow | ||
days_ago: "{count} days ago" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
welcome: ¡Bienvenido a nuestra aplicación! | ||
goodbye: Gracias por usar nuestra app. ¡Hasta la próxima! | ||
nav: | ||
home: Inicio | ||
about: Acerca de | ||
contact: Contáctenos | ||
settings: Configuración | ||
user: | ||
greeting: Hola, {name}! | ||
profile: | ||
title: Tu Perfil | ||
edit: Editar Perfil | ||
save: Guardar Cambios | ||
cancel: Cancelar | ||
errors: | ||
not_found: Página no encontrada | ||
server_error: Ocurrió un error en el servidor | ||
validation: | ||
required: Este campo es obligatorio | ||
email: Por favor, introduce una dirección de correo válida | ||
password: La contraseña debe tener al menos 8 caracteres | ||
form: | ||
email: Dirección de Correo Electrónico | ||
password: Contraseña | ||
submit: Enviar | ||
reset: Restablecer Formulario | ||
success: | ||
saved: Cambios guardados exitosamente | ||
uploaded: Archivo subido exitosamente | ||
deleted: Elemento eliminado exitosamente | ||
time: | ||
today: Hoy | ||
yesterday: Ayer | ||
tomorrow: Mañana | ||
days_ago: Hace {count} días |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,26 @@ | ||
import fs from "node:fs"; | ||
import path from "node:path"; | ||
import dotenv from "dotenv"; | ||
dotenv.config(); | ||
|
||
// Try to find root directory by looking for package.json with workspaces | ||
function findMonorepoRoot(dir: string): string { | ||
const pkgPath = path.join(dir, "package.json"); | ||
|
||
if (fs.existsSync(pkgPath)) { | ||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); | ||
if (pkg.workspaces) { | ||
return dir; | ||
} | ||
} | ||
|
||
const parentDir = path.dirname(dir); | ||
if (parentDir === dir) { | ||
return process.cwd(); // Reached root, fallback to cwd | ||
} | ||
|
||
return findMonorepoRoot(parentDir); | ||
} | ||
|
||
const rootDir = findMonorepoRoot(process.cwd()); | ||
dotenv.config({ path: path.join(rootDir, ".env") }); | ||
dotenv.config(); // Fallback to current directory |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import { generateObject } from "ai"; | ||
import dedent from "dedent"; | ||
import { z } from "zod"; | ||
import { baseRequirements, createBasePrompt } from "../prompt.js"; | ||
import type { PromptOptions, Translator } from "../types.js"; | ||
|
||
interface PoEntry { | ||
key: string; | ||
value: string; | ||
comments: string[]; | ||
} | ||
|
||
function parsePoFile(content: string) { | ||
const entries: PoEntry[] = []; | ||
const lines = content.split("\n"); | ||
let currentComments: string[] = []; | ||
let currentKey = ""; | ||
let currentValue = ""; | ||
|
||
for (const line of lines) { | ||
const trimmed = line.trim(); | ||
|
||
// Collect comments | ||
if (trimmed.startsWith("#")) { | ||
currentComments.push(line); | ||
continue; | ||
} | ||
|
||
// On empty line, reset comments if we haven't started an entry | ||
if (!trimmed && !currentKey) { | ||
currentComments = []; | ||
continue; | ||
} | ||
|
||
if (trimmed.startsWith("msgid")) { | ||
if (currentKey) { | ||
entries.push({ | ||
key: currentKey, | ||
value: currentValue, | ||
comments: currentComments, | ||
}); | ||
} | ||
currentKey = trimmed.match(/msgid "(.*)"/)?.[1] ?? ""; | ||
currentValue = ""; | ||
} else if (trimmed.startsWith("msgstr")) { | ||
currentValue = trimmed.match(/msgstr "(.*)"/)?.[1] ?? ""; | ||
} else if (!trimmed && currentKey) { | ||
entries.push({ | ||
key: currentKey, | ||
value: currentValue, | ||
comments: currentComments, | ||
}); | ||
currentKey = ""; | ||
currentValue = ""; | ||
currentComments = []; | ||
} | ||
} | ||
|
||
if (currentKey) { | ||
entries.push({ | ||
key: currentKey, | ||
value: currentValue, | ||
comments: currentComments, | ||
}); | ||
} | ||
|
||
return entries; | ||
} | ||
|
||
function stringifyPoFile(entries: PoEntry[]) { | ||
return entries | ||
.map(({ key, value, comments }) => { | ||
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); | ||
} |
Oops, something went wrong.