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'; '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 { Subject, debounceTime } from 'rxjs';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -32,10 +32,7 @@ import {
} from '@kit/ui/table'; } from '@kit/ui/table';
import { cn } from '@kit/ui/utils'; import { cn } from '@kit/ui/utils';
import { import { updateTranslationAction } from '../lib/server-actions';
translateWithAIAction,
updateTranslationAction,
} from '../lib/server-actions';
import type { TranslationData, Translations } from '../lib/translations-loader'; import type { TranslationData, Translations } from '../lib/translations-loader';
function flattenTranslations( function flattenTranslations(
@@ -64,7 +61,6 @@ export function TranslationsComparison({
translations: Translations; translations: Translations;
}) { }) {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [isTranslating, setIsTranslating] = useState(false);
// Create RxJS Subject for handling translation updates // Create RxJS Subject for handling translation updates
const subject$ = useMemo( const subject$ = useMemo(
@@ -132,60 +128,6 @@ export function TranslationsComparison({
setSelectedLocales(newSelectedLocales); 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 // Set up subscription to handle debounced updates
useEffect(() => { useEffect(() => {
const subscription = subject$.pipe(debounceTime(500)).subscribe((props) => { const subscription = subject$.pipe(debounceTime(500)).subscribe((props) => {
@@ -262,22 +204,6 @@ export function TranslationsComparison({
</SelectContent> </SelectContent>
</Select> </Select>
</div> </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>
</div> </div>

View File

@@ -2,14 +2,10 @@
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { openai } from '@ai-sdk/openai'; import { readFileSync, writeFileSync } from 'node:fs';
import { generateText } from 'ai';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:url'; import { resolve } from 'node:url';
import { z } from 'zod'; import { z } from 'zod';
import { getLogger } from '@kit/shared/logger';
const Schema = z.object({ const Schema = z.object({
locale: z.string().min(1), locale: z.string().min(1),
namespace: z.string().min(1), namespace: z.string().min(1),
@@ -17,13 +13,6 @@ const Schema = z.object({
value: z.string(), 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. * Update a translation value in the specified locale and namespace.
* @param props * @param props
@@ -70,92 +59,3 @@ export async function updateTranslationAction(props: z.infer<typeof Schema>) {
throw new Error('Failed to update translation'); 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\"" "format": "prettier --check --write \"**/*.{ts,tsx}\" --ignore-path=\"../../.prettierignore\""
}, },
"dependencies": { "dependencies": {
"@ai-sdk/openai": "^2.0.88",
"@faker-js/faker": "^10.2.0", "@faker-js/faker": "^10.2.0",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@tanstack/react-query": "catalog:", "@tanstack/react-query": "catalog:",
"ai": "5.0.116",
"lucide-react": "catalog:", "lucide-react": "catalog:",
"next": "catalog:", "next": "catalog:",
"nodemailer": "catalog:", "nodemailer": "catalog:",

View File

@@ -11,10 +11,10 @@
}, },
"author": "Makerkit", "author": "Makerkit",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.0", "@playwright/test": "^1.58.1",
"@supabase/supabase-js": "catalog:", "@supabase/supabase-js": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"dotenv": "17.2.3", "dotenv": "17.2.4",
"node-html-parser": "^7.0.2", "node-html-parser": "^7.0.2",
"totp-generator": "^2.0.1" "totp-generator": "^2.0.1"
} }

View File

@@ -152,14 +152,12 @@ test.describe('Admin', () => {
), ),
]); ]);
// TODO: find out why we need to reload the page only in CI
await page.reload();
// Verify ban badge is removed // Verify ban badge is removed
await expect(page.getByText('Banned')).not.toBeVisible(); await expect(page.getByText('Banned')).not.toBeVisible();
// Log out // Log out
await page.context().clearCookies(); await page.context().clearCookies();
await page.reload();
// Verify user can log in again // Verify user can log in again
await page.goto('/auth/sign-in'); await page.goto('/auth/sign-in');

View File

@@ -53,8 +53,8 @@
"@kit/ui": "workspace:*", "@kit/ui": "workspace:*",
"@makerkit/data-loader-supabase-core": "^0.0.10", "@makerkit/data-loader-supabase-core": "^0.0.10",
"@makerkit/data-loader-supabase-nextjs": "^1.2.5", "@makerkit/data-loader-supabase-nextjs": "^1.2.5",
"@marsidev/react-turnstile": "^1.4.1", "@marsidev/react-turnstile": "^1.4.2",
"@nosecone/next": "1.0.0", "@nosecone/next": "1.1.0",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@supabase/supabase-js": "catalog:", "@supabase/supabase-js": "catalog:",
"@tanstack/react-query": "catalog:", "@tanstack/react-query": "catalog:",

View File

@@ -1,6 +1,6 @@
{ {
"name": "next-supabase-saas-kit-turbo", "name": "next-supabase-saas-kit-turbo",
"version": "2.23.12", "version": "2.23.13",
"private": true, "private": true,
"sideEffects": false, "sideEffects": false,
"engines": { "engines": {

View File

@@ -183,7 +183,6 @@ export function LineItemDetails(
</If> </If>
</span> </span>
<If condition={!item.tiers?.length}> <If condition={!item.tiers?.length}>
<span>-</span> <span>-</span>

View File

@@ -162,10 +162,12 @@ function PricingItem(
const i18nKey = `billing:units.${lineItem.unit}`; const i18nKey = `billing:units.${lineItem.unit}`;
const unitLabel = lineItem?.unit const unitLabel = lineItem?.unit
? i18n.exists(i18nKey) ? t(i18nKey, { ? i18n.exists(i18nKey)
count: 1, ? t(i18nKey, {
defaultValue: lineItem.unit, count: 1,
}) : lineItem.unit defaultValue: lineItem.unit,
})
: lineItem.unit
: ''; : '';
const isDefaultSeatUnit = lineItem?.unit === 'member'; const isDefaultSeatUnit = lineItem?.unit === 'member';

View File

@@ -2,7 +2,7 @@ import 'server-only';
import { StripeServerEnvSchema } from '../schema/stripe-server-env.schema'; import { StripeServerEnvSchema } from '../schema/stripe-server-env.schema';
const STRIPE_API_VERSION = '2025-12-15.clover'; const STRIPE_API_VERSION = '2026-01-28.clover';
/** /**
* @description returns a Stripe instance * @description returns a Stripe instance

View File

@@ -1,11 +1,12 @@
import { initializeServerI18n } from '@kit/i18n/server';
import { createI18nSettings } from '@kit/i18n'; import { createI18nSettings } from '@kit/i18n';
import { initializeServerI18n } from '@kit/i18n/server';
export function initializeEmailI18n(params: { export function initializeEmailI18n(params: {
language: string | undefined; language: string | undefined;
namespace: string; namespace: string;
}) { }) {
const language = params.language ?? process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? 'en'; const language =
params.language ?? process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? 'en';
return initializeServerI18n( return initializeServerI18n(
createI18nSettings({ createI18nSettings({

View File

@@ -52,7 +52,10 @@ export function AdminReactivateUserDialog(
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<ReactivateUserForm userId={props.userId} onSuccess={() => setOpen(false)} /> <ReactivateUserForm
userId={props.userId}
onSuccess={() => setOpen(false)}
/>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
); );

View File

@@ -24,7 +24,7 @@
"devDependencies": { "devDependencies": {
"@kit/prettier-config": "workspace:*", "@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*", "@kit/tsconfig": "workspace:*",
"@modelcontextprotocol/sdk": "1.25.3", "@modelcontextprotocol/sdk": "1.26.0",
"@types/node": "catalog:", "@types/node": "catalog:",
"postgres": "3.4.8", "postgres": "3.4.8",
"zod": "catalog:" "zod": "catalog:"

View File

@@ -16,7 +16,7 @@
"input-otp": "1.4.2", "input-otp": "1.4.2",
"lucide-react": "catalog:", "lucide-react": "catalog:",
"radix-ui": "1.4.3", "radix-ui": "1.4.3",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.4.0",
"react-top-loading-bar": "3.0.2", "react-top-loading-bar": "3.0.2",
"recharts": "2.15.3", "recharts": "2.15.3",
"tailwind-merge": "^3.4.0" "tailwind-merge": "^3.4.0"

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import type { FormEvent, MouseEventHandler } from 'react'; import type { MouseEventHandler } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import Image from 'next/image'; import Image from 'next/image';
@@ -39,40 +39,41 @@ export const ImageUploadInput: React.FC<Props> =
fileName: '', fileName: '',
}); });
const onInputChange = useCallback( const onInputChange: React.InputEventHandler<HTMLInputElement> =
(e: FormEvent<HTMLInputElement>) => { useCallback(
e.preventDefault(); (e) => {
e.preventDefault();
const files = e.currentTarget.files; const files = e.currentTarget.files;
if (files?.length) { if (files?.length) {
const file = files[0]; const file = files[0];
if (!file) { if (!file) {
return; return;
} }
const data = URL.createObjectURL(file); const data = URL.createObjectURL(file);
setState({ setState({
image: data,
fileName: file.name,
});
if (onValueChange) {
onValueChange({
image: data, image: data,
file, fileName: file.name,
}); });
}
}
if (onInput) { if (onValueChange) {
onInput(e); onValueChange({
} image: data,
}, file,
[onInput, onValueChange], });
); }
}
if (onInput) {
onInput(e);
}
},
[onInput, onValueChange],
);
const onRemove = useCallback(() => { const onRemove = useCallback(() => {
setState({ setState({

2461
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,36 +4,36 @@ packages:
- tooling/* - tooling/*
catalog: catalog:
'@marsidev/react-turnstile': 1.4.1 '@marsidev/react-turnstile': 1.4.2
'@next/bundle-analyzer': 16.1.5 '@next/bundle-analyzer': 16.1.6
'@next/eslint-plugin-next': 16.1.5 '@next/eslint-plugin-next': 16.1.6
'@react-email/components': 1.0.6 '@react-email/components': 1.0.7
'@sentry/nextjs': 10.37.0 '@sentry/nextjs': 10.38.0
'@stripe/react-stripe-js': 5.5.0 '@stripe/react-stripe-js': 5.6.0
'@stripe/stripe-js': 8.6.4 '@stripe/stripe-js': 8.7.0
'@supabase/supabase-js': 2.93.1 '@supabase/supabase-js': 2.95.3
'@tailwindcss/postcss': 4.1.18 '@tailwindcss/postcss': 4.1.18
'@tanstack/react-query': 5.90.20 '@tanstack/react-query': 5.90.20
'@types/eslint': 9.6.1 '@types/eslint': 9.6.1
'@types/node': 25.0.10 '@types/node': 25.2.1
'@types/nodemailer': 7.0.9 '@types/nodemailer': 7.0.9
'@types/react': 19.2.9 '@types/react': 19.2.13
'@types/react-dom': 19.2.3 '@types/react-dom': 19.2.3
eslint: 9.39.2 eslint: 9.39.2
eslint-config-next: 16.1.5 eslint-config-next: 16.1.6
eslint-config-turbo: 2.7.6 eslint-config-turbo: 2.8.3
i18next: 25.8.0 i18next: 25.8.4
i18next-browser-languagedetector: 8.2.0 i18next-browser-languagedetector: 8.2.0
i18next-resources-to-backend: 1.2.1 i18next-resources-to-backend: 1.2.1
lucide-react: 0.563.0 lucide-react: 0.563.0
next: 16.1.5 next: 16.1.6
nodemailer: 7.0.12 nodemailer: 8.0.0
react: 19.2.4 react: 19.2.4
react-dom: 19.2.4 react-dom: 19.2.4
react-hook-form: 7.71.1 react-hook-form: 7.71.1
react-i18next: 16.5.3 react-i18next: 16.5.4
stripe: 20.2.0 stripe: 20.3.1
supabase: 2.72.8 supabase: 2.75.5
tailwindcss: 4.1.18 tailwindcss: 4.1.18
tw-animate-css: 1.4.0 tw-animate-css: 1.4.0
zod: 3.25.76 zod: 3.25.76