chore: bump version to 2.23.13 and update dependencies (#450)
* chore: bump version to 2.23.13 and update dependencies - Updated application version from 2.23.12 to 2.23.13 in package.json. - Upgraded several dependencies including @marsidev/react-turnstile to 1.4.2, @next/bundle-analyzer to 16.1.6, @next/eslint-plugin-next to 16.1.6, and others for improved functionality and security. - Adjusted package versions in pnpm-lock.yaml and pnpm-workspace.yaml for consistency across the project. - Removed unused AI translation functionality from translations-comparison component to streamline the codebase. * refactor: clean up code formatting and update Stripe API version - Removed unnecessary blank lines in LineItemDetails component for improved readability. - Enhanced formatting in PricingItem component for better clarity. - Updated Stripe API version from '2025-12-15.clover' to '2026-01-28.clover' for compatibility with the latest features. - Adjusted i18n initialization in email templates for consistency. - Reformatted props in AdminReactivateUserDialog for better structure. - Cleaned up type imports in ImageUploadInput component.
This commit is contained in:
committed by
GitHub
parent
58f08c5f39
commit
68276fda8a
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { ChevronDownIcon, Loader2Icon } from 'lucide-react';
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import { Subject, debounceTime } from 'rxjs';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -32,10 +32,7 @@ import {
|
||||
} from '@kit/ui/table';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import {
|
||||
translateWithAIAction,
|
||||
updateTranslationAction,
|
||||
} from '../lib/server-actions';
|
||||
import { updateTranslationAction } from '../lib/server-actions';
|
||||
import type { TranslationData, Translations } from '../lib/translations-loader';
|
||||
|
||||
function flattenTranslations(
|
||||
@@ -64,7 +61,6 @@ export function TranslationsComparison({
|
||||
translations: Translations;
|
||||
}) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [isTranslating, setIsTranslating] = useState(false);
|
||||
|
||||
// Create RxJS Subject for handling translation updates
|
||||
const subject$ = useMemo(
|
||||
@@ -132,60 +128,6 @@ export function TranslationsComparison({
|
||||
setSelectedLocales(newSelectedLocales);
|
||||
};
|
||||
|
||||
const handleTranslateWithAI = useCallback(async () => {
|
||||
try {
|
||||
setIsTranslating(true);
|
||||
|
||||
// Get missing translations for the selected namespace
|
||||
const missingTranslations: Record<string, string> = {};
|
||||
const baseTranslations = flattenedTranslations[baseLocale] ?? {};
|
||||
|
||||
for (const locale of visibleLocales) {
|
||||
if (locale === baseLocale) continue;
|
||||
|
||||
const localeTranslations = flattenedTranslations[locale] ?? {};
|
||||
|
||||
for (const [key, value] of Object.entries(baseTranslations)) {
|
||||
if (!localeTranslations[key]) {
|
||||
missingTranslations[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(missingTranslations).length > 0) {
|
||||
await translateWithAIAction({
|
||||
sourceLocale: baseLocale,
|
||||
targetLocale: locale,
|
||||
namespace: selectedNamespace,
|
||||
translations: missingTranslations,
|
||||
});
|
||||
|
||||
toast.success(`Translated missing strings to ${locale}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to translate: ' + (error as Error).message);
|
||||
} finally {
|
||||
setIsTranslating(false);
|
||||
}
|
||||
}, [flattenedTranslations, baseLocale, visibleLocales, selectedNamespace]);
|
||||
|
||||
// Calculate if there are any missing translations
|
||||
const hasMissingTranslations = useMemo(() => {
|
||||
if (!flattenedTranslations || !baseLocale || !visibleLocales) return false;
|
||||
|
||||
const baseTranslations = flattenedTranslations[baseLocale] ?? {};
|
||||
|
||||
return visibleLocales.some((locale) => {
|
||||
if (locale === baseLocale) return false;
|
||||
|
||||
const localeTranslations = flattenedTranslations[locale] ?? {};
|
||||
|
||||
return Object.keys(baseTranslations).some(
|
||||
(key) => !localeTranslations[key],
|
||||
);
|
||||
});
|
||||
}, [flattenedTranslations, baseLocale, visibleLocales]);
|
||||
|
||||
// Set up subscription to handle debounced updates
|
||||
useEffect(() => {
|
||||
const subscription = subject$.pipe(debounceTime(500)).subscribe((props) => {
|
||||
@@ -262,22 +204,6 @@ export function TranslationsComparison({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
onClick={handleTranslateWithAI}
|
||||
disabled={isTranslating || !hasMissingTranslations}
|
||||
>
|
||||
{isTranslating ? (
|
||||
<>
|
||||
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
|
||||
Translating...
|
||||
</>
|
||||
) : (
|
||||
'Translate missing with AI'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,14 +2,10 @@
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import { openai } from '@ai-sdk/openai';
|
||||
import { generateText } from 'ai';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { 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),
|
||||
@@ -17,13 +13,6 @@ const Schema = z.object({
|
||||
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
|
||||
@@ -70,92 +59,3 @@ export async function updateTranslationAction(props: z.infer<typeof Schema>) {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user