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"] + } +}