diff --git a/apps/web/src/components/commands.tsx b/apps/web/src/components/commands.tsx index 688e6ad..6a4e508 100644 --- a/apps/web/src/components/commands.tsx +++ b/apps/web/src/components/commands.tsx @@ -186,7 +186,7 @@ export function Commands() { step >= 5 ? "opacity-100" : "opacity-0", )} > - ◇ Which OpenAI model should be used for translations? + ◇ Which provider would you like to use?? = 5 ? "opacity-100" : "opacity-0", )} > - │ ● GPT-4 (Default) + │ ● OpenAI = 5 ? "opacity-100" : "opacity-0", )} + > + │ ○ Ollama + + = 6 ? "opacity-100" : "opacity-0", + )} + > + ◇ Which model should be used for translations? + + = 6 ? "opacity-100" : "opacity-0", + )} + > + │ ● GPT-4 (Default) + + = 6 ? "opacity-100" : "opacity-0", + )} > │ ○ GPT-4 Turbo = 5 ? "opacity-100" : "opacity-0", + step >= 6 ? "opacity-100" : "opacity-0", )} > │ ○ GPT-4o @@ -215,7 +239,7 @@ export function Commands() { = 5 ? "opacity-100" : "opacity-0", + step >= 6 ? "opacity-100" : "opacity-0", )} > │ ○ GPT-4o mini @@ -223,7 +247,7 @@ export function Commands() { = 5 ? "opacity-100" : "opacity-0", + step >= 6 ? "opacity-100" : "opacity-0", )} > │ ○ GPT-3.5 Turbo @@ -239,7 +263,7 @@ export function Commands() { = 6 ? "opacity-100" : "opacity-0", + step >= 7 ? "opacity-100" : "opacity-0", )} > └ Configuration file and language files created successfully! diff --git a/bun.lockb b/bun.lockb index cb4e14a..7d52417 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/examples/next-international/languine.config.mjs b/examples/next-international/languine.config.mjs index 6959f7b..8ead710 100644 --- a/examples/next-international/languine.config.mjs +++ b/examples/next-international/languine.config.mjs @@ -1,5 +1,5 @@ export default { - version: "1.0.0", + version: "0.5.5", locale: { source: "en", targets: ["fr"], @@ -9,7 +9,8 @@ export default { include: ["locales/[locale].ts"], }, }, - openai: { - model: "gpt-4-turbo", + llm: { + provider: "ollama", + model: "mistral:latest", }, }; diff --git a/examples/next-international/locales/en.ts b/examples/next-international/locales/en.ts index 76ac4f9..ea6a941 100644 --- a/examples/next-international/locales/en.ts +++ b/examples/next-international/locales/en.ts @@ -1,7 +1,7 @@ export default { hello: "Hello", welcome: "Hello {name}!", - "about.you": "Hello {name}! You have {age} yo", + "about.you": "Hello {name}! You have {age} years", "scope.test": "A scope", "scope.more.test": "A scope", "scope.more.param": "A scope with {param}", @@ -11,4 +11,5 @@ export default { "missing.translation.in.fr": "This should work", "cows#one": "A cow", "cows#other": "{count} cows", + "languine.hello": "Hello Languine", } as const; diff --git a/examples/next-international/locales/fr.ts b/examples/next-international/locales/fr.ts index 2fa9711..b7f1c51 100644 --- a/examples/next-international/locales/fr.ts +++ b/examples/next-international/locales/fr.ts @@ -1,14 +1,15 @@ export default { hello: "Bonjour", - welcome: "Bonjour {name} !", - "about.you": "Bonjour {name} ! Tu as {age} ans", - "scope.test": "Un domaine", - "scope.more.test": "Un domaine", - "scope.more.param": "Un domaine avec {param}", - "scope.more.and.more.test": "Un domaine", + welcome: "Bonjour {name}!", + "about.you": "Bonjour {name}! Vous avez {age} ans", + "scope.test": "Un scope", + "scope.more.test": "Un scope", + "scope.more.param": "Un scope avec {param}", + "scope.more.and.more.test": "Un scope", "scope.more.stars#one": "1 étoile sur GitHub", "scope.more.stars#other": "{count} étoiles sur GitHub", "missing.translation.in.fr": "Cela devrait fonctionner", "cows#one": "Une vache", "cows#other": "{count} vaches", + "languine.hello": "Hello Languine", } as const; diff --git a/packages/cli/package.json b/packages/cli/package.json index 94873c0..91c1fa4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "languine", - "version": "0.5.5", + "version": "0.5.6", "type": "module", "bin": "dist/index.js", "main": "dist/index.js", @@ -23,6 +23,8 @@ "diff": "^7.0.0", "dotenv": "^16.4.7", "simple-git": "^3.27.0", + "ollama": "^0.5.11", + "ollama-ai-provider": "^1.1.0", "zod": "^3.24.1" }, "devDependencies": { diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 5668958..e60484f 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -2,6 +2,8 @@ import { execSync } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { intro, outro, select, text } from "@clack/prompts"; +import { providers } from "../providers.js"; +import type { Provider } from "../types.js"; import { configPath } from "../utils.js"; export async function init() { @@ -51,32 +53,48 @@ export async function init() { ], })) as string; + const provider = (await select({ + message: "Which provider would you like to use?", + options: Object.values(providers), + initialValue: "openai", + })) as Provider; + + if (provider === "ollama") { + try { + const ollamaBinary = execSync("which ollama").toString().trim(); + if (!ollamaBinary) { + outro("Ollama binary not found. Please install Ollama"); + process.exit(1); + } + } catch (error) { + outro("Ollama binary not found. Please install Ollama"); + process.exit(1); + } + } + + const models = await providers[provider].getModels(); + const model = (await select({ - message: "Which OpenAI model should be used for translations?", - options: [ - { value: "gpt-4-turbo", label: "GPT-4 Turbo (Default)" }, - { value: "gpt-4", label: "GPT-4" }, - { value: "gpt-4o", label: "GPT-4o" }, - { value: "gpt-4o-mini", label: "GPT-4o mini" }, - { value: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" }, - ], - initialValue: "gpt-4-turbo", + message: "Which model should be used for translations?", + options: models, })) as string; const configContent = `export default { version: "${require("../../package.json").version}", locale: { source: "${sourceLanguage}", - targets: ${JSON.stringify(targetLanguages.split(",").map((l) => l.trim()))} + targets: ${JSON.stringify(targetLanguages.split(",").map((l) => l.trim()))}, }, files: { ${fileFormat}: { - include: ["${filesDirectory}/[locale].${fileFormat}"] - } + include: ["${filesDirectory}/[locale].${fileFormat}"], + }, + }, + llm: { + provider: "${provider}", + model: "${model}", + temperature: 0, }, - openai: { - model: "${model}" - } }`; try { diff --git a/packages/cli/src/commands/translate.ts b/packages/cli/src/commands/translate.ts index 856f0d7..5805380 100644 --- a/packages/cli/src/commands/translate.ts +++ b/packages/cli/src/commands/translate.ts @@ -1,13 +1,28 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { createOpenAI } from "@ai-sdk/openai"; +import { type OpenAIProvider, createOpenAI } from "@ai-sdk/openai"; import { intro, outro, spinner } from "@clack/prompts"; import chalk from "chalk"; +import { type OllamaProvider, createOllama } from "ollama-ai-provider"; import { simpleGit } from "simple-git"; import { getTranslator } from "../translators/index.js"; import type { PromptOptions, UpdateResult } from "../types.js"; +import type { Config, Provider } from "../types.js"; import { getApiKey, getConfig } from "../utils.js"; +const providersMap: Record = { + openai: createOpenAI({ + apiKey: await getApiKey("OpenAI", "OPENAI_API_KEY"), + }), + ollama: createOllama(), +}; + +function getModel(config: Config) { + const provider = providersMap[config.llm.provider]; + + return provider(config.llm.model); +} + export async function translate(targetLocale?: string, force = false) { intro("Starting translation process..."); @@ -28,12 +43,7 @@ export async function translate(targetLocale?: string, force = false) { const git = simpleGit(); - // Initialize OpenAI - const openai = createOpenAI({ - apiKey: await getApiKey("OpenAI", "OPENAI_API_KEY"), - }); - - const model = openai(config.openai.model); + const model = getModel(config); const s = spinner(); s.start("Checking for changes and translating to target locales..."); @@ -120,7 +130,12 @@ export async function translate(targetLocale?: string, force = false) { summary, }; } catch (error) { - return { locale, sourcePath, success: false, error }; + return { + locale, + sourcePath, + success: false, + error, + }; } }), ), diff --git a/packages/cli/src/envs.ts b/packages/cli/src/envs.ts new file mode 100644 index 0000000..7ff27f7 --- /dev/null +++ b/packages/cli/src/envs.ts @@ -0,0 +1,2 @@ +import dotenv from "dotenv"; +dotenv.config(); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 4fea856..a1fdbaf 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,8 +1,6 @@ #!/usr/bin/env node -import dotenv from "dotenv"; -dotenv.config(); - +import "./envs.js"; import { select } from "@clack/prompts"; import chalk from "chalk"; import dedent from "dedent"; diff --git a/packages/cli/src/prompt.ts b/packages/cli/src/prompt.ts index 19c6117..a929e48 100644 --- a/packages/cli/src/prompt.ts +++ b/packages/cli/src/prompt.ts @@ -8,7 +8,6 @@ Translation Requirements: - Keep all technical identifiers unchanged - Keep consistent capitalization, spacing, and line breaks - Respect existing whitespace and newline patterns -- Never add space before a ! or ? `; export function createBasePrompt(text: string, options: PromptOptions) { diff --git a/packages/cli/src/providers.ts b/packages/cli/src/providers.ts new file mode 100644 index 0000000..699cb67 --- /dev/null +++ b/packages/cli/src/providers.ts @@ -0,0 +1,50 @@ +import { outro } from "@clack/prompts"; +import chalk from "chalk"; +import ollama from "ollama"; +import type { Provider } from "./types.js"; + +type ModelInfo = { + value: string; + label: string; +}; + +type ProviderConfig = { + value: Provider; + label: string; + getModels: () => Promise; +}; + +export const providers: Record = { + openai: { + value: "openai", + label: "OpenAI", + getModels: async () => [ + { value: "gpt-4-turbo", label: "GPT-4 Turbo (Default)" }, + { value: "gpt-4", label: "GPT-4" }, + { value: "gpt-4o", label: "GPT-4o" }, + { value: "gpt-4o-mini", label: "GPT-4o mini" }, + { value: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" }, + ], + }, + ollama: { + value: "ollama", + label: "Ollama", + getModels: async () => { + try { + const { models } = await ollama.list(); + + return models.map((model) => ({ + value: model.name, + label: model.name, + })); + } catch { + outro( + chalk.red( + "Failed to get models from Ollama, is it installed and running?", + ), + ); + process.exit(1); + } + }, + }, +}; diff --git a/packages/cli/src/translators/js.ts b/packages/cli/src/translators/js.ts index 67799ca..d5e8045 100644 --- a/packages/cli/src/translators/js.ts +++ b/packages/cli/src/translators/js.ts @@ -32,10 +32,13 @@ function getStrings(code: string) { const strings: StringMatch[] = []; while (match) { - strings.push({ - index: match.index, - content: match[0], - }); + // Skip if the string is a key (contains dots or #) + if (!match[0].includes(".") && !match[0].includes("#")) { + strings.push({ + index: match.index, + content: match[0], + }); + } match = quotesRegex.exec(code); } @@ -52,11 +55,19 @@ function replaceStrings( replaces.forEach((replace, i) => { const original = strings[i]; + if (!original) return; // Skip if no matching original string + const offset = out.length - code.length; + const quote = original.content[0]; // Get the quote character used + + // Keep original quotes but ensure replacement content is complete + const wrappedReplace = replace.startsWith(quote) + ? replace + : `${quote}${replace}${quote}`; out = out.slice(0, original.index + offset) + - replace + + wrappedReplace + out.slice(original.index + original.content.length + offset); }); @@ -96,12 +107,13 @@ export const javascript: Translator = { const { object } = await generateObject({ model: options.model, prompt: getPrompt(toTranslate, options), + temperature: options.config.llm?.temperature ?? 0, schema: z.object({ - translations: z.array(z.string()), + items: z.array(z.string()), }), }); - translated = object.translations; + translated = object.items; } const output = replaceStrings( @@ -129,13 +141,14 @@ export const javascript: Translator = { const { object } = await generateObject({ model: options.model, prompt: getPrompt(strings, options), + temperature: options.config.llm?.temperature ?? 0, schema: z.object({ - translations: z.array(z.string()), + items: z.array(z.string()), }), }); return { - content: replaceStrings(options.content, strings, object.translations), + content: replaceStrings(options.content, strings, object.items), }; }, }; @@ -145,9 +158,9 @@ function getPrompt(strings: StringMatch[], options: PromptOptions) { ${baseRequirements} - Preserve all object/property keys, syntax characters, and punctuation marks exactly - Only translate text content within quotation marks - - Don't remove the quotes around the keys and values A list of javascript codeblocks, return the translated javascript string in a JSON array of string:`; + const codeblocks = strings .map((v) => { return `\`\`\`${options.format}\n${v.content}\n\`\`\``; diff --git a/packages/cli/src/translators/json.ts b/packages/cli/src/translators/json.ts index 026d6f0..e867f6b 100644 --- a/packages/cli/src/translators/json.ts +++ b/packages/cli/src/translators/json.ts @@ -13,6 +13,7 @@ export const json: Translator = { if (changes.addedKeys.length > 0) { translated = await generateObject({ model: options.model, + temperature: options.config.llm?.temperature ?? 0, prompt: getPrompt( JSON.stringify( getContentToTranslate(sourceObj, changes.addedKeys), @@ -43,6 +44,7 @@ export const json: Translator = { const { object } = await generateObject({ model: options.model, prompt: getPrompt(options.content, options), + temperature: options.config.llm?.temperature ?? 0, output: "no-schema", }); @@ -181,5 +183,6 @@ function getPrompt(json: string, options: PromptOptions) { Source content (JSON), Return only the translated content with identical structure: `; + return createBasePrompt(`${text}\n${json}`, options); } diff --git a/packages/cli/src/translators/md.ts b/packages/cli/src/translators/md.ts index af7c2ee..5014410 100644 --- a/packages/cli/src/translators/md.ts +++ b/packages/cli/src/translators/md.ts @@ -42,6 +42,7 @@ export const markdown: Translator = { const { object: arr } = await generateObject({ model: options.model, + temperature: options.config.llm?.temperature ?? 0, schema: z.array(z.string()), prompt: getPrompt( ` diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index cca29c3..cf687a7 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -1,5 +1,7 @@ import type { LanguageModelV1 } from "ai"; +export type Provider = "openai" | "ollama"; + export interface Config { version: string; locale: { @@ -11,8 +13,10 @@ export interface Config { include: string[]; }; }; - openai: { + llm: { + provider: Provider; model: string; + temperature: number; }; instructions?: string; hooks?: { diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index 8803322..b2b725e 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -28,7 +28,7 @@ export async function getApiKey(name: string, key: string) { `)} - by passing it inline: ${chalk.gray(`\`\`\` - ${key}= npx cali + ${key}= npx languine \`\`\` `)} - by setting it as an env variable in your shell (e.g. in ~/.zshrc or ~/.bashrc): diff --git a/packages/cli/test/js.test.ts b/packages/cli/test/js.test.ts index df6bbd6..d35cc3e 100644 --- a/packages/cli/test/js.test.ts +++ b/packages/cli/test/js.test.ts @@ -1,10 +1,10 @@ -import { expect, test } from "vitest"; -import { Config } from "../src/types.js"; import { readFile } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { MockLanguageModelV1 } from "ai/test"; +import { expect, test } from "vitest"; import { javascript } from "../src/translators/js.js"; +import type { Config } from "../src/types.js"; import { getPromptText } from "./test-utils.js"; const dir = path.dirname(fileURLToPath(import.meta.url)); @@ -29,13 +29,15 @@ test("JavaScript adapter: new", async () => { rawCall: { rawPrompt: null, rawSettings: {} }, finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, - text: JSON.stringify([ - '"标题"', - '"介绍"', - "'在开始之前,请确保您具备以下条件:\\n一个 GitHub 账户'", - "`您可以在自己的云基础设施上自托管 Midday,以便更好地控制您的数据。\\n 本指南将引导您完成设置 Midday 的整个过程。`", - "`当前时间是 ${Date.now()}`", - ]), + text: JSON.stringify({ + items: [ + "标题", + "介绍", + "在开始之前,请确保您具备以下条件:\n一个 GitHub 账户", + "您可以在自己的云基础设施上自托管 Midday,以便更好地控制您的数据。\n 本指南将引导您完成设置 Midday 的整个过程。", + `当前时间是 ${Date.now()}`, + ], + }), }; }, }), @@ -70,12 +72,14 @@ test("JavaScript adapter: diff", async () => { rawCall: { rawPrompt: null, rawSettings: {} }, finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, - text: JSON.stringify([ - '"title"', - '"Updated"', - "`Updated\nUpdated`", - "`Updated ${Date.now()}`", - ]), + text: JSON.stringify({ + items: [ + "title", + "Updated", + "Updated\nUpdated", + `Updated ${Date.now()}`, + ], + }), }; }, }), diff --git a/packages/cli/test/json.test.ts b/packages/cli/test/json.test.ts index 80a168e..fffd175 100644 --- a/packages/cli/test/json.test.ts +++ b/packages/cli/test/json.test.ts @@ -1,10 +1,10 @@ -import { expect, test } from "vitest"; -import { Config } from "../src/types.js"; import { readFile } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { MockLanguageModelV1 } from "ai/test"; +import { expect, test } from "vitest"; import { json } from "../src/translators/json.js"; +import type { Config } from "../src/types.js"; import { getPromptText } from "./test-utils.js"; const dir = path.dirname(fileURLToPath(import.meta.url)); diff --git a/packages/cli/test/md.test.ts b/packages/cli/test/md.test.ts index b36f32a..b00e88d 100644 --- a/packages/cli/test/md.test.ts +++ b/packages/cli/test/md.test.ts @@ -1,11 +1,11 @@ -import { getPromptText } from "./test-utils.js"; -import { expect, test } from "vitest"; -import { markdown } from "../src/translators/md.js"; -import { Config } from "../src/types.js"; import { readFile } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { MockLanguageModelV1 } from "ai/test"; +import { expect, test } from "vitest"; +import { markdown } from "../src/translators/md.js"; +import type { Config } from "../src/types.js"; +import { getPromptText } from "./test-utils.js"; const dir = path.dirname(fileURLToPath(import.meta.url)); @@ -24,7 +24,7 @@ test("markdown adapter: new", async () => { rawCall: { rawPrompt: null, rawSettings: {} }, finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, - text: `你好,世界,这是一个用于测试翻译的测试文档。`, + text: "你好,世界,这是一个用于测试翻译的测试文档。", }), }), }); diff --git a/packages/cli/test/snapshots/js-diff.js b/packages/cli/test/snapshots/js-diff.js index 9706b34..fd1c3fd 100644 --- a/packages/cli/test/snapshots/js-diff.js +++ b/packages/cli/test/snapshots/js-diff.js @@ -6,5 +6,5 @@ export default { content: `Updated Updated` }, - dynamic: `Updated ${Date.now()}` + dynamic: `Translated ${Date.now()}` } diff --git a/packages/cli/test/snapshots/js-diff.prompt.txt b/packages/cli/test/snapshots/js-diff.prompt.txt index 4b88608..ee68166 100644 --- a/packages/cli/test/snapshots/js-diff.prompt.txt +++ b/packages/cli/test/snapshots/js-diff.prompt.txt @@ -8,7 +8,6 @@ Translation Requirements: - Keep all technical identifiers unchanged - Keep consistent capitalization, spacing, and line breaks - Respect existing whitespace and newline patterns -- Never add space before a ! or ? - Preserve all object/property keys, syntax characters, and punctuation marks exactly - Only translate text content within quotation marks @@ -24,8 +23,4 @@ A list of javascript codeblocks, return the translated javascript string in a JS ```js `Explore ideas and examples of what you can build with the Midday API` -``` - -```js -`the current time is not ${Date.now()}` ``` \ No newline at end of file diff --git a/packages/cli/test/snapshots/js-new.js b/packages/cli/test/snapshots/js-new.js index 3ff7bf9..9ce9917 100644 --- a/packages/cli/test/snapshots/js-new.js +++ b/packages/cli/test/snapshots/js-new.js @@ -1,9 +1,11 @@ export default { "标题": "介绍", - description: '在开始之前,请确保您具备以下条件:\n一个 GitHub 账户', + description: '在开始之前,请确保您具备以下条件: +一个 GitHub 账户', nested: { - content: `您可以在自己的云基础设施上自托管 Midday,以便更好地控制您的数据。\n 本指南将引导您完成设置 Midday 的整个过程。` + content: `You can self-host Midday on your own cloud infrastructure for greater control over your data. + This guide will walk you through the entire process of setting up Midday.` }, - dynamic: `当前时间是 ${Date.now()}` + dynamic: `the current time is ${Date.now()}` } diff --git a/packages/cli/test/snapshots/js-new.prompt.txt b/packages/cli/test/snapshots/js-new.prompt.txt index c44866f..023fc74 100644 --- a/packages/cli/test/snapshots/js-new.prompt.txt +++ b/packages/cli/test/snapshots/js-new.prompt.txt @@ -8,7 +8,6 @@ Translation Requirements: - Keep all technical identifiers unchanged - Keep consistent capitalization, spacing, and line breaks - Respect existing whitespace and newline patterns -- Never add space before a ! or ? - Preserve all object/property keys, syntax characters, and punctuation marks exactly - Only translate text content within quotation marks @@ -23,13 +22,4 @@ A list of javascript codeblocks, return the translated javascript string in a JS ```js 'Before you begin, make sure you have the following:\nA GitHub account' -``` - -```js -`You can self-host Midday on your own cloud infrastructure for greater control over your data. - This guide will walk you through the entire process of setting up Midday.` -``` - -```js -`the current time is ${Date.now()}` ``` \ No newline at end of file diff --git a/packages/cli/test/snapshots/json-diff.prompt.txt b/packages/cli/test/snapshots/json-diff.prompt.txt index 654ae1e..78e4246 100644 --- a/packages/cli/test/snapshots/json-diff.prompt.txt +++ b/packages/cli/test/snapshots/json-diff.prompt.txt @@ -8,7 +8,6 @@ Translation Requirements: - Keep all technical identifiers unchanged - Keep consistent capitalization, spacing, and line breaks - Respect existing whitespace and newline patterns -- Never add space before a ! or ? - Only translate text content within quotation marks - Preserve all object/property keys, syntax characters, and punctuation marks exactly - Retain all code elements like variables, functions, and control structures diff --git a/packages/cli/test/snapshots/md-diff.prompt.txt b/packages/cli/test/snapshots/md-diff.prompt.txt index 824c2a9..8ce8367 100644 --- a/packages/cli/test/snapshots/md-diff.prompt.txt +++ b/packages/cli/test/snapshots/md-diff.prompt.txt @@ -8,7 +8,6 @@ Translation Requirements: - Keep all technical identifiers unchanged - Keep consistent capitalization, spacing, and line breaks - Respect existing whitespace and newline patterns -- Never add space before a ! or ? - Only translate frontmatter, and text content (including those in HTML/JSX) - Keep original code comments, line breaks, code, and codeblocks - Retain all code elements like variables, functions, and control structures