'use server'; import { revalidatePath } from 'next/cache'; import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:url'; import { z } from 'zod'; import { getLogger } from '@kit/shared/logger'; const Schema = z.object({ locale: z.string().min(1), namespace: z.string().min(1), key: z.string().min(1), value: z.string(), }); const TranslateSchema = z.object({ sourceLocale: z.string(), targetLocale: z.string(), namespace: z.string(), translations: z.record(z.string(), z.string()), }); /** * Update a translation value in the specified locale and namespace. * @param props */ export async function updateTranslationAction(props: z.infer) { // Validate the input const { locale, namespace, key, value } = Schema.parse(props); const root = resolve(process.cwd(), '..'); const filePath = `${root}apps/web/public/locales/${locale}/${namespace}.json`; try { // Read the current translations file const translationsFile = readFileSync(filePath, 'utf-8'); const translations = JSON.parse(translationsFile) as Record; // Update the nested key value const keys = key.split('.') as string[]; let current = translations; // Navigate through nested objects until the second-to-last key for (let i = 0; i < keys.length - 1; i++) { const currentKey = keys[i] as string; if (!current[currentKey]) { current[currentKey] = {}; } current = current[currentKey]; } // Set the value at the final key const finalKey = keys[keys.length - 1] as string; current[finalKey] = value; // Write the updated translations back to the file writeFileSync(filePath, JSON.stringify(translations, null, 2), 'utf-8'); revalidatePath(`/translations`); return { success: true }; } catch (error) { console.error('Failed to update translation:', error); throw new Error('Failed to update translation'); } } export const translateWithAIAction = async ( data: z.infer, ) => { const logger = await getLogger(); z.string().min(1).parse(process.env.OPENAI_API_KEY); try { const { sourceLocale, targetLocale, namespace, translations } = TranslateSchema.parse(data); // if the path does not exist, create it using an empty object const root = resolve(process.cwd(), '..'); const folderPath = `${root}apps/web/public/locales/${targetLocale}`; if (!existsSync(folderPath)) { // create the directory if it doesn't exist mkdirSync(folderPath, { recursive: true }); } const filePath = `${folderPath}/${namespace}.json`; if (!existsSync(filePath)) { // create the file if it doesn't exist writeFileSync(filePath, JSON.stringify({}, null, 2), 'utf-8'); } const results: Record = {}; // Process translations in batches of 5 for efficiency const entries = Object.entries(translations); const batches = []; for (let i = 0; i < entries.length; i += 5) { batches.push(entries.slice(i, i + 5)); } for (const batch of batches) { const batchPromises = batch.map(async ([key, value]) => { const prompt = `Translate the following text from ${sourceLocale} to ${targetLocale}. Maintain any placeholders (like {name} or %{count}) and HTML tags. Only return the translated text, nothing else. Original text: ${value}`; const MODEL_NAME = process.env.LLM_MODEL_NAME ?? 'gpt-4o-mini'; const model = openai(MODEL_NAME); const { text } = await generateText({ model, prompt, temperature: 0.3, maxTokens: 200, }); return [key, text.trim()] as [string, string]; }); const batchResults = await Promise.all(batchPromises); for (const [key, translation] of batchResults) { results[key] = translation; } } // Update each translation for (const [key, translation] of Object.entries(results)) { await updateTranslationAction({ locale: targetLocale, namespace, key, value: translation, }); } logger.info('AI translation completed', { sourceLocale, targetLocale, namespace, count: Object.keys(results).length, }); revalidatePath('/translations'); return { success: true, translations: results }; } catch (error) { logger.error('AI translation failed', { error }); throw error; } };