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:
Giancarlo Buomprisco
2026-02-06 12:55:05 +01:00
committed by GitHub
parent 58f08c5f39
commit 68276fda8a
17 changed files with 1286 additions and 1491 deletions

View File

@@ -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>

View File

@@ -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;
}
};

View File

@@ -8,11 +8,9 @@
"format": "prettier --check --write \"**/*.{ts,tsx}\" --ignore-path=\"../../.prettierignore\""
},
"dependencies": {
"@ai-sdk/openai": "^2.0.88",
"@faker-js/faker": "^10.2.0",
"@hookform/resolvers": "^5.2.2",
"@tanstack/react-query": "catalog:",
"ai": "5.0.116",
"lucide-react": "catalog:",
"next": "catalog:",
"nodemailer": "catalog:",