diff --git a/apps/dashboard/languine.json b/apps/dashboard/languine.json
new file mode 100644
index 0000000000..87a97720d8
--- /dev/null
+++ b/apps/dashboard/languine.json
@@ -0,0 +1,16 @@
+{
+ "version": "1.0.0",
+ "locale": {
+ "source": "en",
+ "targets": ["sv"]
+ },
+ "files": {
+ "ts": {
+ "include": ["src/locales/[locale].ts"]
+ }
+ },
+ "openai": {
+ "model": "gpt-4"
+ },
+ "instructions": "Make the tone more for a Fintech/Accounting tool"
+}
diff --git a/apps/dashboard/src/app/[locale]/(app)/(sidebar)/account/date-and-locale/page.tsx b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/account/date-and-locale/page.tsx
index 67cc28e25b..c0b790e7c6 100644
--- a/apps/dashboard/src/app/[locale]/(app)/(sidebar)/account/date-and-locale/page.tsx
+++ b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/account/date-and-locale/page.tsx
@@ -1,3 +1,4 @@
+import { ChangeLanguage } from "@/components/change-language";
import { ChangeTimezone } from "@/components/change-timezone";
import { DateFormatSettings } from "@/components/date-format-settings";
import { LocaleSettings } from "@/components/locale-settings";
@@ -19,6 +20,7 @@ export default async function Page() {
return (
+
diff --git a/apps/dashboard/src/components/change-language.tsx b/apps/dashboard/src/components/change-language.tsx
index a11609ffd8..cce2c0cf87 100644
--- a/apps/dashboard/src/components/change-language.tsx
+++ b/apps/dashboard/src/components/change-language.tsx
@@ -25,13 +25,13 @@ import {
import { useAction } from "next-safe-action/hooks";
export function ChangeLanguage() {
- const action = useAction(updateUserAction);
+ // const action = useAction(updateUserAction);
const changeLocale = useChangeLocale();
const locale = useCurrentLocale();
const t = useI18n();
- const handleOnChange = async (locale: string) => {
- await action.execute({ locale });
+ const handleOnChange = (locale: string) => {
+ // await action.execute({ locale });
changeLocale(locale);
};
diff --git a/apps/dashboard/src/locales/client.ts b/apps/dashboard/src/locales/client.ts
index 8db379e975..97a5f35345 100644
--- a/apps/dashboard/src/locales/client.ts
+++ b/apps/dashboard/src/locales/client.ts
@@ -3,7 +3,7 @@
import { createI18nClient } from "next-international/client";
// NOTE: Also update middleware.ts to support locale
-export const languages = ["en"];
+export const languages = ["en", "sv"];
export const {
useScopedI18n,
@@ -13,5 +13,5 @@ export const {
useI18n,
} = createI18nClient({
en: () => import("./en"),
- // sv: () => import("./sv"),
+ sv: () => import("./sv"),
});
diff --git a/apps/dashboard/src/locales/server.ts b/apps/dashboard/src/locales/server.ts
index 869cf6b843..1ea7ea27a6 100644
--- a/apps/dashboard/src/locales/server.ts
+++ b/apps/dashboard/src/locales/server.ts
@@ -2,5 +2,5 @@ import { createI18nServer } from "next-international/server";
export const { getI18n, getScopedI18n, getStaticParams } = createI18nServer({
en: () => import("./en"),
- // sv: () => import("./sv"),
+ sv: () => import("./sv"),
});
diff --git a/apps/dashboard/src/locales/sv.ts b/apps/dashboard/src/locales/sv.ts
index f2beaf1ebc..1fd30c776c 100644
--- a/apps/dashboard/src/locales/sv.ts
+++ b/apps/dashboard/src/locales/sv.ts
@@ -1,12 +1,12 @@
export default {
transaction_methods: {
- card_purchase: "Kortbetalning",
+ card_purchase: "Kortköp",
payment: "Betalning",
- card_atm: "Kort bankomat",
+ card_atm: "Kort ATM",
transfer: "Överföring",
- other: "Annan",
+ other: "Annat",
ach: "Ach",
- deposit: "Deposition",
+ deposit: "Insättning",
wire: "Wire",
fee: "Avgift",
interest: "Ränta",
@@ -16,42 +16,57 @@ export default {
description: "Ändra språket som används i användargränssnittet.",
placeholder: "Välj språk",
},
+ locale: {
+ title: "Lokal",
+ searchPlaceholder: "Sök lokal",
+ description:
+ "Ställer in regionen och språkinställningarna för valuta, datum och andra lokal-specifika format.",
+ placeholder: "Välj lokal",
+ },
languages: {
en: "Engelska",
sv: "Svenska",
},
timezone: {
- title: "Tidzon",
- description: "Aktuell tidzoninställning.",
- placeholder: "Välj tidzon",
- },
- inbox_filter: {
- all: "Alla",
- todo: "Att göra",
- done: "Slutförda",
+ title: "Tidszon",
+ searchPlaceholder: "Sök tidszon",
+ description:
+ "Definierar den standard tidszon som används för att visa tider i appen.",
+ placeholder: "Välj tidszon",
},
spending_period: {
last_30d: "Senaste 30 dagarna",
- this_month: "Den här månaden",
+ this_month: "Denna månad",
last_month: "Förra månaden",
- this_year: "Det här året",
+ this_year: "Detta år",
last_year: "Förra året",
},
transactions_period: {
- all: "All",
+ all: "Alla",
income: "Inkomst",
- outcome: "Utgifter",
+ expense: "Utgifter",
+ },
+ transaction_frequency: {
+ weekly: "Veckovis återkommande",
+ monthly: "Månadsvis återkommande",
+ annually: "Årligen återkommande",
+ },
+ inbox_filter: {
+ all: "Alla",
+ todo: "Att göra",
+ done: "Klar",
},
chart_type: {
profit: "Vinst",
- revenue: "Omsättning",
- burn_rate: "Brännhastighet",
+ revenue: "Intäkter",
+ expense: "Utgifter",
+ burn_rate: "Burn rate",
},
folders: {
all: "Alla",
- exports: "Exporteringar",
+ exports: "Export",
inbox: "Inkorg",
- imports: "Importer",
+ imports: "Import",
transactions: "Transaktioner",
invoices: "Fakturor",
},
@@ -64,10 +79,136 @@ export default {
member: "Medlem",
},
tracker_status: {
- in_progress: "Pågående",
- completed: "Färdig",
+ in_progress: "Pågår",
+ completed: "Slutförd",
+ },
+ notifications: {
+ inbox: "Få notifikationer om nya objekt i din inkorg.",
+ match: "Få notifikationer om matchningar.",
+ transaction: "Få notifikationer om en ny transaktion.",
+ transactions: "Få notifikationer om nya transaktioner.",
+ "invoice.paid": "Få notifikationer om betalda fakturor.",
+ "invoice.overdue": "Få notifikationer om försenade fakturor.",
+ "inbox.match": "Få notifikationer om nya matchningar i din inkorg.",
+ },
+ widgets: {
+ insights: "Assistent",
+ inbox: "Inkorg",
+ spending: "Utgifter",
+ transactions: "Transaktioner",
+ tracker: "Tracker",
+ },
+ bottom_bar: {
+ "transactions#one": "1 Transaktion",
+ "transactions#other": "{count} Transaktioner",
+ multi_currency: "Multi valuta",
+ description: "Inkluderar transaktioner från alla sidor av resultat",
+ },
+ account_type: {
+ depository: "Depå",
+ credit: "Kredit",
+ other_asset: "Annan tillgång",
+ loan: "Lån",
+ other_liability: "Annan skuld",
+ },
+ tags: {
+ bylaws: "Stadgar",
+ shareholder_agreements: "Aktieägaravtal",
+ board_meeting: "Styrelsemöte",
+ corporate_policies: "Företagspolicyer",
+ annual_reports: "Årsredovisningar",
+ budget_reports: "Budgetrapporter",
+ audit_reports: "Revisionsrapporter",
+ tax_returns: "Skattedeklarationer",
+ invoices_and_receipts: "Fakturor & Kvittot",
+ employee_handbook: "Anställdas handbok",
+ payroll_records: "Löneuppgifter",
+ performance_reviews: "Prestationsbedömningar",
+ employee_training_materials: "Utbildningsmaterial för anställda",
+ benefits_documentation: "Förmånsdokumentation",
+ termination_letters: "Uppsägningsbrev",
+ patents: "Patent",
+ trademarks: "Varumärken",
+ copyrights: "Upphovsrätt",
+ client_contracts: "Kundkontrakt",
+ financial_records: "Finansiella uppgifter",
+ compliance_reports: "Efterlevnadsrapporter",
+ regulatory_filings: "Regulatoriska inlämningar",
+ advertising_copy: "Reklamkopia",
+ press_releases: "Pressmeddelanden",
+ branding_guidelines: "Varumärkesriktlinjer",
+ market_research_reports: "Marknadsundersökningsrapporter",
+ campaign_performance_reports: "Kampanjprestationsrapporter",
+ customer_surveys: "Kundenkäter",
+ quality_control_reports: "Kvalitetskontrollrapporter",
+ inventory_reports: "Lager rapporter",
+ maintenance_logs: "Underhållsloggar",
+ production_schedules: "Produktionsscheman",
+ vendor_agreements: "Leverantörsavtal",
+ supplier_agreements: "Leverantörsavtal",
+ sales_contracts: "Försäljningskontrakt",
+ sales_reports: "Försäljningsrapporter",
+ client_proposals: "Kundförslag",
+ customer_order_forms: "Kundorderformulär",
+ sales_presentations: "Försäljningspresentationer",
+ data_security_plans: "Datasäkerhetsplaner",
+ system_architecture_diagrams: "Systemarkitekturdiagram",
+ incident_response_plans: "Incidentresponsplaner",
+ user_manuals: "Användarmanualer",
+ software_licenses: "Programvarulicenser",
+ data_backup_logs: "Data backup-loggar",
+ project_plans: "Projektplaner",
+ task_lists: "Uppgiftslistor",
+ risk_management_plans: "Riskhanteringsplaner",
+ project_status_reports: "Projektstatusrapporter",
+ meeting_agendas: "Mötesagendor",
+ lab_notebooks: "Lab anteckningsböcker",
+ experiment_results: "Experimentresultat",
+ product_design_documents: "Produktdesign dokument",
+ prototypes_and_models: "Prototyper & Modeller",
+ testing_reports: "Testrapporter",
+ newsletters: "Nyhetsbrev",
+ email_correspondence: "E-postkorrespondens",
+ support_tickets: "Supportärenden",
+ faqs_and_knowledge: "FAQs & Kunskap",
+ user_guides: "Användarguider",
+ warranty_information: "Garantiinformation",
+ swot_analysis: "SWOT-analys",
+ strategic_objectives: "Strategiska mål",
+ roadmaps: "Roadmaps",
+ competitive_analysis: "Konkurrensanalys",
+ safety_data_sheets: "Säkerhetsdatablad",
+ compliance_certificates: "Efterlevnadscertifikat",
+ incident_reports: "Incidentrapporter",
+ emergency_response_plans: "Nödresponsplaner",
+ certification_records: "Certifieringsregister",
+ training_schedules: "Utbildningsscheman",
+ e_learning_materials: "E-lärningsmaterial",
+ competency_assessment_forms: "Kompetensbedömningsformulär",
+ },
+ invoice_status: {
+ draft: "Utkast",
+ overdue: "Försenad",
+ paid: "Betald",
+ unpaid: "Obetald",
+ canceled: "Avbruten",
+ },
+ payment_status: {
+ none: "Okänd",
+ good: "Bra",
+ average: "Medel",
+ bad: "Dålig",
+ },
+ payment_status_description: {
+ none: "Ingen betalningshistorik ännu.",
+ good: "Betalar konsekvent i tid.",
+ average: "För det mesta i tid.",
+ bad: "Rum för förbättring.",
},
+ "invoice_count#zero": "Inga fakturor",
+ "invoice_count#one": "1 faktura",
+ "invoice_count#other": "{count} fakturor",
account_balance: {
- total_balance: "Total saldo",
+ total_balance: "Total balans",
},
} as const;
diff --git a/apps/dashboard/src/middleware.ts b/apps/dashboard/src/middleware.ts
index 33d31b0fc3..e895cdb864 100644
--- a/apps/dashboard/src/middleware.ts
+++ b/apps/dashboard/src/middleware.ts
@@ -4,7 +4,7 @@ import { createI18nMiddleware } from "next-international/middleware";
import { type NextRequest, NextResponse } from "next/server";
const I18nMiddleware = createI18nMiddleware({
- locales: ["en"],
+ locales: ["en", "sv"],
defaultLocale: "en",
urlMappingStrategy: "rewrite",
});
diff --git a/bun.lockb b/bun.lockb
index e4b67d0952..08cfb61982 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 568ac078a8..4faa824283 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,7 @@
{
"name": "midday",
"private": true,
- "workspaces": [
- "packages/*",
- "apps/*",
- "packages/email/*"
- ],
+ "workspaces": ["packages/*", "apps/*", "packages/email/*"],
"scripts": {
"build": "turbo build",
"clean": "git clean -xdf node_modules",
diff --git a/packages/i18n/.gitignore b/packages/i18n/.gitignore
new file mode 100644
index 0000000000..11ee758152
--- /dev/null
+++ b/packages/i18n/.gitignore
@@ -0,0 +1 @@
+.env.local
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
new file mode 100644
index 0000000000..5953500774
--- /dev/null
+++ b/packages/i18n/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "languine",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "bin": "dist/index.js",
+ "main": "dist/index.js",
+ "scripts": {
+ "clean": "rm -rf .turbo node_modules",
+ "lint": "biome check .",
+ "format": "biome format --write .",
+ "typecheck": "tsc --noEmit",
+ "test": "bun test src",
+ "build": "tsup src/index.ts --format esm --dts --clean",
+ "dev": "tsup src/index.ts --format esm --watch --clean",
+ "start": "node dist/index.js"
+ },
+ "dependencies": {
+ "@ai-sdk/openai": "^1.0.7",
+ "@clack/prompts": "^0.8.2",
+ "chalk": "^5.3.0",
+ "dedent": "^1.5.3",
+ "dotenv": "^16.4.7",
+ "gradient-string": "^3.0.0",
+ "zod": "^3.23.8"
+ },
+ "devDependencies": {
+ "tsup": "^8.0.2",
+ "typescript": "^5.3.3"
+ }
+}
diff --git a/packages/i18n/src/commands/init.ts b/packages/i18n/src/commands/init.ts
new file mode 100644
index 0000000000..27791acfb5
--- /dev/null
+++ b/packages/i18n/src/commands/init.ts
@@ -0,0 +1,189 @@
+import fs from "node:fs/promises";
+import path from "node:path";
+import { intro, outro, select, text } from "@clack/prompts";
+
+export async function init() {
+ intro("Let's set up your i18n configuration");
+
+ const sourceLanguage = await select({
+ message: "What is your source language?",
+ options: [
+ { value: "en", label: "English" },
+ { value: "es", label: "Spanish" },
+ { value: "fr", label: "French" },
+ { value: "de", label: "German" },
+ ],
+ });
+
+ const targetLanguages = await text({
+ message: "What languages do you want to translate to?",
+ placeholder: "es,fr,de,zh,ja,pt",
+ validate: (value) => {
+ if (!value) return "Please enter at least one language";
+
+ const codes = value.split(",").map((code) => code.trim().toLowerCase());
+ const validCodes = new Set([
+ "es",
+ "fr",
+ "de",
+ "it",
+ "pt",
+ "ru",
+ "zh",
+ "ja",
+ "ko",
+ "ar",
+ "hi",
+ "af",
+ "bg",
+ "bn",
+ "ca",
+ "cs",
+ "cy",
+ "da",
+ "el",
+ "en",
+ "et",
+ "fa",
+ "fi",
+ "ga",
+ "gu",
+ "he",
+ "hr",
+ "hu",
+ "id",
+ "kn",
+ "lt",
+ "lv",
+ "mk",
+ "ml",
+ "mr",
+ "ms",
+ "mt",
+ "nl",
+ "no",
+ "pa",
+ "pl",
+ "ro",
+ "sk",
+ "sl",
+ "sq",
+ "sr",
+ "sv",
+ "sw",
+ "ta",
+ "te",
+ "th",
+ "tr",
+ "uk",
+ "ur",
+ "vi",
+ ]);
+
+ const invalidCodes = codes.filter((code) => !validCodes.has(code));
+ if (invalidCodes.length > 0) {
+ return `Invalid language code(s): ${invalidCodes.join(", ")}`;
+ }
+ return;
+ },
+ });
+
+ const filesDirectory = await text({
+ message: "Where should language files be stored?",
+ placeholder: "src/locales",
+ defaultValue: "src/locales",
+ validate: () => undefined,
+ });
+
+ const fileFormat = await select({
+ message: "What format should language files use?",
+ options: [
+ { value: "ts", label: "TypeScript (.ts)" },
+ { value: "json", label: "JSON (.json)" },
+ { value: "yaml", label: "YAML (.yaml)" },
+ { value: "md", label: "Markdown (.md)" },
+ { value: "xml", label: "Android (.xml)" },
+ { value: "arb", label: "Flutter (.arb)" },
+ { value: "stringsdict", label: "iOS Dictionary (.stringsdict)" },
+ { value: "strings", label: "iOS Strings (.strings)" },
+ { value: "xcstrings", label: "iOS XCStrings (.xcstrings)" },
+ ],
+ });
+
+ const model = await select({
+ message: "Which OpenAI model should be used for translations?",
+ options: [
+ { value: "gpt-4", label: "GPT-4 (Default)" },
+ { value: "gpt-4-turbo", label: "GPT-4 Turbo" },
+ { 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",
+ });
+
+ const config = {
+ version: require("@midday/i18n/package.json").version,
+ locale: {
+ source: sourceLanguage,
+ targets: targetLanguages.split(",").map((l) => l.trim()),
+ },
+ files: {
+ [fileFormat]: {
+ include: [`${filesDirectory}/[locale].${fileFormat}`],
+ },
+ },
+ openai: {
+ model: model,
+ },
+ };
+
+ try {
+ // Create locales directory if it doesn't exist
+ await fs.mkdir(path.join(process.cwd(), filesDirectory), {
+ recursive: true,
+ });
+
+ // Create source language file if it doesn't exist
+ const sourceFile = path.join(
+ process.cwd(),
+ `${filesDirectory}/${String(sourceLanguage)}.${String(fileFormat)}`,
+ );
+ if (
+ !(await fs
+ .access(sourceFile)
+ .then(() => true)
+ .catch(() => false))
+ ) {
+ await fs.writeFile(sourceFile, "", "utf-8");
+ }
+
+ // Create target language files if they don't exist
+ const targetLangs =
+ typeof targetLanguages === "string" ? targetLanguages.split(",") : [];
+ for (const targetLang of targetLangs.map((l: string) => l.trim())) {
+ const targetFile = path.join(
+ process.cwd(),
+ `${filesDirectory}/${String(targetLang)}.${String(fileFormat)}`,
+ );
+ if (
+ !(await fs
+ .access(targetFile)
+ .then(() => true)
+ .catch(() => false))
+ ) {
+ await fs.writeFile(targetFile, "", "utf-8");
+ }
+ }
+
+ // Write config file
+ await fs.writeFile(
+ path.join(process.cwd(), "languine.json"),
+ JSON.stringify(config, null, 2),
+ );
+ outro("Configuration file and language files created successfully!");
+ } catch (error) {
+ outro("Failed to create config and language files");
+ process.exit(1);
+ }
+}
diff --git a/packages/i18n/src/commands/instructions.ts b/packages/i18n/src/commands/instructions.ts
new file mode 100644
index 0000000000..e4ebbef74e
--- /dev/null
+++ b/packages/i18n/src/commands/instructions.ts
@@ -0,0 +1,45 @@
+import fs from "node:fs/promises";
+import path from "node:path";
+import { intro, outro, text } from "@clack/prompts";
+import chalk from "chalk";
+import type { LanguineConfig } from "../types.js";
+
+export async function instructions() {
+ intro("Let's customize your translation prompt");
+
+ const customInstructions = await text({
+ message: "Enter additional translation instructions",
+ placeholder: "e.g. Use formal language, add a tone of voice",
+ validate: (value) => {
+ if (!value) return "Please enter some instructions";
+ return;
+ },
+ });
+
+ try {
+ // Read config file
+ const configPath = path.join(process.cwd(), "languine.json");
+ let config: LanguineConfig;
+
+ try {
+ const configContent = await fs.readFile(configPath, "utf-8");
+ config = JSON.parse(configContent);
+ } catch (error) {
+ outro(
+ chalk.red("Could not find languine.json. Run 'languine init' first."),
+ );
+ process.exit(1);
+ }
+
+ // Add custom instructions to config
+ config.instructions = customInstructions as string;
+
+ // Write updated config
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
+
+ outro(chalk.green("Translation prompt updated successfully!"));
+ } catch (error) {
+ outro(chalk.red("Failed to update config file"));
+ process.exit(1);
+ }
+}
diff --git a/packages/i18n/src/commands/translate.ts b/packages/i18n/src/commands/translate.ts
new file mode 100644
index 0000000000..a6831d4b95
--- /dev/null
+++ b/packages/i18n/src/commands/translate.ts
@@ -0,0 +1,126 @@
+import fs from "node:fs/promises";
+import path from "node:path";
+import { createOpenAI } from "@ai-sdk/openai";
+import { intro, outro, spinner } from "@clack/prompts";
+import { generateText } from "ai";
+import chalk from "chalk";
+import dedent from "dedent";
+import { prompt as defaultPrompt } from "../prompt.js";
+import type { LanguineConfig } from "../types.js";
+import { getApiKey } from "../utils.js";
+
+export async function translate(targetLocale?: string) {
+ intro("Starting translation process...");
+
+ // Read config file
+ let config: LanguineConfig;
+ try {
+ const configFile = await fs.readFile(
+ path.join(process.cwd(), "languine.json"),
+ "utf-8",
+ );
+ config = JSON.parse(configFile);
+ } catch (error) {
+ outro(
+ chalk.red("Could not find languine.json. Run 'languine init' first."),
+ );
+ process.exit(1);
+ }
+
+ const { source, targets } = config.locale;
+ const locales = targetLocale ? [targetLocale] : targets;
+
+ // Validate target locale if specified
+ if (targetLocale && !targets.includes(targetLocale)) {
+ outro(
+ chalk.red(
+ `Invalid target locale: ${targetLocale}. Available locales: ${targets.join(", ")}`,
+ ),
+ );
+ process.exit(1);
+ }
+
+ // Initialize OpenAI
+ const openai = createOpenAI({
+ apiKey: await getApiKey("OpenAI", "OPENAI_API_KEY"),
+ });
+
+ const s = spinner();
+
+ for (const locale of locales) {
+ s.start(`Translating to ${locale}...`);
+
+ // Process each file type defined in config
+ for (const [format, { include }] of Object.entries(config.files)) {
+ for (const pattern of include) {
+ const sourcePath = pattern.replace("[locale]", source);
+ const targetPath = pattern.replace("[locale]", locale);
+
+ try {
+ // Read source file
+ let sourceContent = "";
+ try {
+ sourceContent = await fs.readFile(
+ path.join(process.cwd(), sourcePath),
+ "utf-8",
+ );
+ } catch (error) {
+ // Create source file if it doesn't exist
+ const sourceDir = path.dirname(
+ path.join(process.cwd(), sourcePath),
+ );
+ await fs.mkdir(sourceDir, { recursive: true });
+ await fs.writeFile(
+ path.join(process.cwd(), sourcePath),
+ "",
+ "utf-8",
+ );
+ }
+
+ // Prepare translation prompt
+ const prompt = dedent`
+ You are a professional translator working with ${format.toUpperCase()} files.
+
+ Task: Translate the content below from ${source} to ${locale}.
+
+ ${defaultPrompt}
+
+ ${config.instructions ?? ""}
+
+ Source content:
+ ${sourceContent}
+
+ Return only the translated content with identical structure.
+ `;
+
+ // Get translation from OpenAI
+ const { text } = await generateText({
+ model: openai(config.openai.model),
+ prompt,
+ });
+
+ // Ensure translation is a string
+ const translation = text;
+
+ // Ensure target directory exists
+ const targetDir = path.dirname(path.join(process.cwd(), targetPath));
+ await fs.mkdir(targetDir, { recursive: true });
+
+ // Write translated content
+ await fs.writeFile(
+ path.join(process.cwd(), targetPath),
+ translation,
+ "utf-8",
+ );
+ } catch (error) {
+ s.stop(`Error translating ${sourcePath} to ${locale}`);
+ console.error(error);
+ }
+ }
+ }
+
+ s.stop(`Successfully translated to ${locale}`);
+ }
+
+ outro(chalk.green("Translation completed successfully!"));
+}
diff --git a/packages/i18n/src/index.ts b/packages/i18n/src/index.ts
new file mode 100644
index 0000000000..9e03558bac
--- /dev/null
+++ b/packages/i18n/src/index.ts
@@ -0,0 +1,64 @@
+#!/usr/bin/env node
+
+import dotenv from "dotenv";
+dotenv.config();
+
+import { select } from "@clack/prompts";
+import chalk from "chalk";
+import dedent from "dedent";
+import { retro } from "gradient-string";
+import { init } from "./commands/init.js";
+import { instructions } from "./commands/instructions.js";
+import { translate } from "./commands/translate.js";
+
+console.log(
+ retro(`
+ ██╗ █████╗ ███╗ ██╗ ██████╗ ██╗ ██╗██╗███╗ ██╗███████╗
+ ██║ ██╔══██╗████╗ ██║██╔════╝ ██║ ██║██║████╗ ██║██╔════╝
+ ██║ ███████║██╔██╗ ██║██║ ███╗██║ ██║██║██║██╗ ██║█████╗
+ ██║ ██╔══██║██║╚██╗██║██║ ██║██║ ██║██║██║╚██╗██║██╔══╝
+ ███████╗██║ ██║██║ ╚████║╚██████╔╝╚██████╔╝██║██║ ╚████║███████╗
+ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝╚══════╝
+ `),
+);
+
+console.log(
+ chalk.gray(dedent`
+ Automated AI localization for your applications.
+ Website: ${chalk.bold("https://languine.ai")}
+ `),
+);
+
+console.log();
+
+// Parse command line arguments
+const command =
+ process.argv[2] ||
+ (await select({
+ message: "What would you like to do?",
+ options: [
+ { value: "init", label: "Initialize a new Languine configuration" },
+ { value: "translate", label: "Translate to target languages" },
+ { value: "instructions", label: "Add custom translation instructions" },
+ ],
+ }));
+
+const targetLocale = process.argv[3];
+
+if (command === "init") {
+ init();
+} else if (command === "translate") {
+ translate(targetLocale);
+} else if (command === "instructions") {
+ instructions();
+} else {
+ console.log(chalk.bold("\nAvailable commands:"));
+ console.log(dedent`
+ ${chalk.cyan("init")} Initialize a new Languine configuration
+ ${chalk.cyan("translate")} Translate to all target locales
+ ${chalk.cyan("translate")} ${chalk.gray("")} Translate to a specific locale
+ ${chalk.cyan("instructions")} Add custom translation instructions
+
+ Run ${chalk.cyan("languine ")} to execute a command
+ `);
+}
diff --git a/packages/i18n/src/prompt.ts b/packages/i18n/src/prompt.ts
new file mode 100644
index 0000000000..a28b0bf5a1
--- /dev/null
+++ b/packages/i18n/src/prompt.ts
@@ -0,0 +1,10 @@
+export const prompt = `
+Requirements:
+- Preserve the exact file structure and formatting
+- Only translate string values inside quotes
+- Keep all object keys, syntax, and punctuation unchanged
+- Maintain consistent casing and spacing
+- Ensure translations are natural and culturally appropriate
+- Do not add or remove any code elements
+- Do not include comments or explanatory text
+`;
diff --git a/packages/i18n/src/types.ts b/packages/i18n/src/types.ts
new file mode 100644
index 0000000000..fba56403a1
--- /dev/null
+++ b/packages/i18n/src/types.ts
@@ -0,0 +1,16 @@
+export interface LanguineConfig {
+ version: string;
+ locale: {
+ source: string;
+ targets: string[];
+ };
+ files: {
+ [key: string]: {
+ include: string[];
+ };
+ };
+ openai: {
+ model: string;
+ };
+ instructions?: string;
+}
diff --git a/packages/i18n/src/utils.ts b/packages/i18n/src/utils.ts
new file mode 100644
index 0000000000..644a63ee60
--- /dev/null
+++ b/packages/i18n/src/utils.ts
@@ -0,0 +1,57 @@
+import { execSync } from "node:child_process";
+import { confirm, outro, text } from "@clack/prompts";
+import chalk from "chalk";
+import dedent from "dedent";
+
+export async function getApiKey(name: string, key: string) {
+ if (key in process.env) {
+ return process.env[key];
+ }
+ return (async () => {
+ let apiKey: string | symbol;
+ do {
+ apiKey = await text({
+ message: dedent`
+ ${chalk.bold(`Please provide your ${name} API key.`)}
+
+ To skip this message, set ${chalk.bold(key)} env variable, and run again.
+
+ You can do it in three ways:
+ - by creating an ${chalk.bold(".env.local")} file (make sure to ${chalk.bold(".gitignore")} it)
+ ${chalk.gray(`\`\`\`
+ ${key}=
+ \`\`\`
+ `)}
+ - by passing it inline:
+ ${chalk.gray(`\`\`\`
+ ${key}= npx cali
+ \`\`\`
+ `)}
+ - by setting it as an env variable in your shell (e.g. in ~/.zshrc or ~/.bashrc):
+ ${chalk.gray(`\`\`\`
+ export ${key}=
+ \`\`\`
+ `)},
+ `,
+ validate: (value) =>
+ value.length > 0 ? undefined : `Please provide a valid ${key}.`,
+ });
+ } while (typeof apiKey === "undefined");
+
+ if (typeof apiKey === "symbol") {
+ outro(chalk.gray("Bye!"));
+ process.exit(0);
+ }
+
+ const save = await confirm({
+ message: `Do you want to save it for future runs in .env.local?`,
+ });
+
+ if (save) {
+ execSync(`echo "${key}=${apiKey}" >> .env.local`);
+ execSync(`echo ".env.local" >> .gitignore`);
+ }
+
+ return apiKey;
+ })();
+}
diff --git a/packages/i18n/tsconfig.json b/packages/i18n/tsconfig.json
new file mode 100644
index 0000000000..c398b74287
--- /dev/null
+++ b/packages/i18n/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "include": ["src/**/*.ts"],
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "NodeNext",
+ "moduleResolution": "nodenext",
+ "allowJs": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "outDir": "dist",
+ "resolveJsonModule": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "noEmit": false,
+ "customConditions": ["source"]
+ }
+}