Refactor: - Improved consistency and robustness by standardizing file encoding arguments from 'utf-8' to 'utf8' across various file read/write operations. - Simplified status mapping logic in billing components and services by replacing switch statements with direct mapping objects for clearer and more maintainable code. - Enhanced type conversion and error handling in billing and internationalization components for improved reliability. - Updated sorting logic in team member tables for more predictable member ordering. - Improved error logging with sanitized output to prevent formatting issues. - Adjusted environment variable whitelisting to use a more flexible matching pattern. - Fix variables for sidebar style handling Style: - Refined spacing and layout in account selector and sidebar header components for better visual consistency.
162 lines
4.5 KiB
TypeScript
162 lines
4.5 KiB
TypeScript
'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<typeof Schema>) {
|
|
// 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, 'utf8');
|
|
const translations = JSON.parse(translationsFile) as Record<string, any>;
|
|
|
|
// 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), 'utf8');
|
|
|
|
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<typeof TranslateSchema>,
|
|
) => {
|
|
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), 'utf8');
|
|
}
|
|
|
|
const results: Record<string, string> = {};
|
|
|
|
// 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;
|
|
}
|
|
};
|