From 9f751002dac1b1d705cf9baab317abcb3ddfd2fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:15:00 +0000 Subject: [PATCH 1/3] Initial plan From ed83aec4b3c441d6307167834408a18eeddb154d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:22:21 +0000 Subject: [PATCH 2/3] feat: integrate AI translation via DeepL API Co-authored-by: itzmxritz <89535320+itzmxritz@users.noreply.github.com> --- .env.example | 3 + package-lock.json | 51 ++-- .../[projectId]/translations/page.tsx | 52 ++++ .../translations/ai-translate/route.ts | 228 ++++++++++++++++++ src/lib/locales.ts | 50 ++++ 5 files changed, 357 insertions(+), 27 deletions(-) create mode 100644 src/app/api/v1/projects/[projectId]/translations/ai-translate/route.ts diff --git a/.env.example b/.env.example index 223d7e8..d646421 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,6 @@ SIGNUPS_ENABLED=true # Node Environment NODE_ENV=development + +# DeepL API Key (for AI Translation feature) +DEEPL_API_KEY=your_deepl_api_key_here diff --git a/package-lock.json b/package-lock.json index be2afe1..3228dcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,6 +154,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -453,7 +454,8 @@ "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@electric-sql/pglite-socket": { "version": "0.0.20", @@ -2300,6 +2302,7 @@ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.4.0.tgz", "integrity": "sha512-Sc+ncr7+ph1hMf1LQfn6UyEXDEamCd5pXMsx8Q3SBH0NGX+zjqs3eaABt9hXwbcK9l7f8UyK8ldxOWA2LyPynQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/client-runtime-utils": "7.4.0" }, @@ -4161,18 +4164,6 @@ } } }, - "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": { - "version": "0.22.4", - "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz", - "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.3.0", - "node-gyp-build": "^4.8.4" - } - }, "node_modules/@swagger-api/apidom-reference": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.1.0.tgz", @@ -4667,6 +4658,7 @@ "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4692,6 +4684,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5177,6 +5170,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5596,6 +5590,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6637,6 +6632,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6759,6 +6755,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7005,6 +7002,7 @@ "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", @@ -7912,6 +7910,7 @@ "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -7970,6 +7969,7 @@ "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -10077,6 +10077,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "7.4.0", "@prisma/dev": "0.20.0", @@ -10227,6 +10228,7 @@ "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/ramda" @@ -10286,6 +10288,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -10295,6 +10298,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10461,7 +10465,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-immutable": { "version": "4.0.0", @@ -11526,7 +11531,8 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -11602,6 +11608,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11642,18 +11649,6 @@ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", "license": "MIT" }, - "node_modules/tree-sitter": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", - "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.0.0", - "node-gyp-build": "^4.8.0" - } - }, "node_modules/tree-sitter-json": { "version": "0.24.8", "resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.24.8.tgz", @@ -11843,6 +11838,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12290,6 +12286,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/app/(dashboard)/projects/[projectId]/translations/page.tsx b/src/app/(dashboard)/projects/[projectId]/translations/page.tsx index 8ae764b..e8671b9 100644 --- a/src/app/(dashboard)/projects/[projectId]/translations/page.tsx +++ b/src/app/(dashboard)/projects/[projectId]/translations/page.tsx @@ -14,6 +14,7 @@ import { Lock, Info, Copy, + Wand2, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -129,6 +130,7 @@ export default function TranslationsPage() { const [currentPage, setCurrentPage] = React.useState(1); const [labelsDialogPage, setLabelsDialogPage] = React.useState(1); const [labelsSearchQuery, setLabelsSearchQuery] = React.useState(""); + const [aiTranslatingTermId, setAiTranslatingTermId] = React.useState(null); // Get translations for selected locale const { data: translations = [] } = useTranslations( @@ -352,6 +354,41 @@ export default function TranslationsPage() { ); }; + const handleAiTranslate = async (termId: string) => { + if (!selectedLocale) return; + + setAiTranslatingTermId(termId); + try { + const response = await fetch( + `/api/v1/projects/${projectId}/translations/ai-translate`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ sourceLocaleCode: selectedLocale, termId }), + } + ); + + const json = await response.json() as { data?: { translated: number; skipped: number }; error?: string }; + + if (!response.ok) { + throw new Error(json.error || 'AI translation failed'); + } + + const translated = json.data?.translated ?? 0; + const skipped = json.data?.skipped ?? 0; + if (translated === 0) { + toast.info("No missing translations to fill"); + } else { + toast.success(`Translated to ${translated} language${translated !== 1 ? 's' : ''}${skipped > 0 ? ` (${skipped} skipped)` : ''}`); + } + } catch (error) { + toast.error(error instanceof Error ? error.message : "AI translation failed"); + } finally { + setAiTranslatingTermId(null); + } + }; + // Filter labels based on search query const filteredLabels = React.useMemo(() => { if (!labelsSearchQuery.trim()) return labels; @@ -746,6 +783,21 @@ export default function TranslationsPage() { )} + {locales.length > 1 && ( + + )} )} {isDisabled && ( diff --git a/src/app/api/v1/projects/[projectId]/translations/ai-translate/route.ts b/src/app/api/v1/projects/[projectId]/translations/ai-translate/route.ts new file mode 100644 index 0000000..b75bd3c --- /dev/null +++ b/src/app/api/v1/projects/[projectId]/translations/ai-translate/route.ts @@ -0,0 +1,228 @@ +/** + * AI Translation endpoint using DeepL + * POST /api/v1/projects/:projectId/translations/ai-translate + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { v4 as uuidv4 } from 'uuid'; +import { authenticateRequest, checkProjectAccess, canAccessLocale } from '@/lib/middleware'; +import { prisma } from '@/lib/db'; +import { toDeepLSourceLang, toDeepLTargetLang } from '@/lib/locales'; + +interface AiTranslateRequest { + sourceLocaleCode: string; + termId: string; +} + +interface DeepLResponse { + translations: Array<{ text: string; detected_source_language: string }>; +} + +async function translateWithDeepL( + text: string, + targetLang: string, + sourceLang: string | null +): Promise { + const apiKey = process.env.DEEPL_API_KEY; + if (!apiKey) { + throw new Error('DEEPL_API_KEY is not configured'); + } + + const baseUrl = apiKey.endsWith(':fx') + ? 'https://api-free.deepl.com/v2/translate' + : 'https://api.deepl.com/v2/translate'; + + const body: Record = { + text: [text], + target_lang: targetLang, + }; + if (sourceLang) { + body.source_lang = sourceLang; + } + + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + Authorization: `DeepL-Auth-Key ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`DeepL API error (${response.status}): ${errorText}`); + } + + const data = (await response.json()) as DeepLResponse; + return data.translations[0].text; +} + +// POST /api/v1/projects/:projectId/translations/ai-translate +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ projectId: string }> } +) { + try { + const auth = await authenticateRequest(request); + + if (!auth) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { projectId } = await params; + const body: AiTranslateRequest = await request.json(); + + if (auth.isApiKey) { + if (auth.projectId !== projectId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + } else { + const access = await checkProjectAccess(auth.userId!, projectId); + + if (!access.hasAccess) { + return NextResponse.json({ error: 'Project not found' }, { status: 404 }); + } + + // Editors can use AI translate only for their assigned locales + if (access.memberRole === 'editor') { + if (!canAccessLocale(body.sourceLocaleCode, access.assignedLocales)) { + return NextResponse.json( + { error: 'You do not have access to translate this locale' }, + { status: 403 } + ); + } + } + } + + const { sourceLocaleCode, termId } = body; + + if (!sourceLocaleCode || !termId) { + return NextResponse.json( + { error: 'sourceLocaleCode and termId are required' }, + { status: 400 } + ); + } + + // Verify the source locale exists in this project + const sourceLocale = await prisma.locale.findFirst({ + where: { projectId, code: sourceLocaleCode }, + select: { id: true }, + }); + + if (!sourceLocale) { + return NextResponse.json({ error: 'Source locale not found' }, { status: 404 }); + } + + // Verify the term exists in this project + const term = await prisma.term.findFirst({ + where: { id: termId, projectId }, + select: { id: true, isLocked: true }, + }); + + if (!term) { + return NextResponse.json({ error: 'Term not found' }, { status: 404 }); + } + + // Locked terms require admin role + if (term.isLocked) { + if (auth.isApiKey) { + if (auth.apiKeyRole !== 'admin') { + return NextResponse.json( + { error: 'This term is locked and can only be translated by admins' }, + { status: 403 } + ); + } + } else { + const access = await checkProjectAccess(auth.userId!, projectId); + if (!access.isOwner && access.memberRole !== 'admin') { + return NextResponse.json( + { error: 'This term is locked and can only be translated by admins' }, + { status: 403 } + ); + } + } + } + + // Get the source translation value + const sourceTranslation = await prisma.translation.findFirst({ + where: { termId, localeId: sourceLocale.id }, + select: { value: true }, + }); + + if (!sourceTranslation?.value) { + return NextResponse.json( + { error: 'No source translation available to translate from' }, + { status: 400 } + ); + } + + const sourceText = sourceTranslation.value; + const sourceLang = toDeepLSourceLang(sourceLocaleCode); + + // Get all other locales in this project + const allLocales = await prisma.locale.findMany({ + where: { projectId, NOT: { code: sourceLocaleCode } }, + select: { id: true, code: true }, + }); + + // Find which locales are already missing translations for this term + const existingTranslations = await prisma.translation.findMany({ + where: { + termId, + localeId: { in: allLocales.map((l) => l.id) }, + }, + select: { localeId: true, value: true }, + }); + + const existingMap = new Map(existingTranslations.map((t) => [t.localeId, t.value])); + + const localesToTranslate = allLocales.filter((locale) => { + const existing = existingMap.get(locale.id); + return !existing; // Only translate if missing + }); + + if (localesToTranslate.length === 0) { + return NextResponse.json({ data: { translated: 0, skipped: allLocales.length } }); + } + + let translated = 0; + let skipped = allLocales.length - localesToTranslate.length; + + for (const locale of localesToTranslate) { + const targetLang = toDeepLTargetLang(locale.code); + if (!targetLang) { + skipped++; + continue; + } + + try { + const translatedText = await translateWithDeepL(sourceText, targetLang, sourceLang); + + await prisma.translation.create({ + data: { + id: uuidv4(), + termId, + localeId: locale.id, + value: translatedText, + }, + }); + + translated++; + } catch (err) { + console.error(`AI translate failed for locale ${locale.code}:`, err); + skipped++; + } + } + + return NextResponse.json({ data: { translated, skipped } }); + } catch (error) { + if (error instanceof Error && error.message.includes('DEEPL_API_KEY')) { + return NextResponse.json( + { error: 'AI translation is not configured on this server' }, + { status: 503 } + ); + } + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/src/lib/locales.ts b/src/lib/locales.ts index b75a065..5ee04b8 100644 --- a/src/lib/locales.ts +++ b/src/lib/locales.ts @@ -138,3 +138,53 @@ export function parseLocaleCode(code: string): { language: string; region: strin } return null; } + +/** + * Map a locale code (e.g. en_US) to a DeepL target language code (e.g. EN-US) + * Returns null if the locale is not supported by DeepL + */ +export function toDeepLTargetLang(code: string): string | null { + const mapping: Record = { + en_US: 'EN-US', + en_GB: 'EN-GB', + // en_CA and en_AU have no dedicated DeepL target variant; fall back to EN-US + en_CA: 'EN-US', + en_AU: 'EN-US', + pt_BR: 'PT-BR', + pt_PT: 'PT-PT', + zh_CN: 'ZH-HANS', + zh_TW: 'ZH-HANT', + zh_HK: 'ZH-HANT', + no_NO: 'NB', + }; + if (mapping[code]) return mapping[code]; + // For remaining locales derive the 2-letter uppercase code + const lang = code.split('_')[0].toUpperCase(); + const supported = [ + 'AR', 'BG', 'CS', 'DA', 'DE', 'EL', 'ES', 'ET', 'FI', 'FR', + 'HU', 'ID', 'IT', 'JA', 'KO', 'LT', 'LV', 'NL', 'PL', 'RO', + 'RU', 'SK', 'SL', 'SV', 'TR', 'UK', + ]; + return supported.includes(lang) ? lang : null; +} + +/** + * Map a locale code (e.g. en_US) to a DeepL source language code (e.g. EN) + * Returns null if the locale is not supported by DeepL + */ +export function toDeepLSourceLang(code: string): string | null { + const mapping: Record = { + no_NO: 'NB', + zh_CN: 'ZH', + zh_TW: 'ZH', + zh_HK: 'ZH', + }; + if (mapping[code]) return mapping[code]; + const lang = code.split('_')[0].toUpperCase(); + const supported = [ + 'AR', 'BG', 'CS', 'DA', 'DE', 'EL', 'EN', 'ES', 'ET', 'FI', 'FR', + 'HU', 'ID', 'IT', 'JA', 'KO', 'LT', 'LV', 'NB', 'NL', 'PL', 'PT', + 'RO', 'RU', 'SK', 'SL', 'SV', 'TR', 'UK', 'ZH', + ]; + return supported.includes(lang) ? lang : null; +} From 5e92a6bf92e476ce0cee94441829e20f1a54a4fa Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 23 Feb 2026 17:39:42 +0100 Subject: [PATCH 3/3] fix: remove unnecessary whitespace in TranslationsPage component --- .../[projectId]/translations/page.tsx | 626 +++++++++--------- 1 file changed, 316 insertions(+), 310 deletions(-) diff --git a/src/app/(dashboard)/projects/[projectId]/translations/page.tsx b/src/app/(dashboard)/projects/[projectId]/translations/page.tsx index e8671b9..2558ac8 100644 --- a/src/app/(dashboard)/projects/[projectId]/translations/page.tsx +++ b/src/app/(dashboard)/projects/[projectId]/translations/page.tsx @@ -104,7 +104,7 @@ export default function TranslationsPage() { const addLocaleMutation = useAddLocale(projectId); const deleteLocaleMutation = useDeleteLocale(projectId); const updateTranslationMutation = useUpdateTranslation(projectId); - + // Check permissions const permissions = useProjectPermissions(projectId); @@ -125,7 +125,7 @@ export default function TranslationsPage() { } | null>(null); const [selectedLabelIds, setSelectedLabelIds] = React.useState([]); const [isSavingLabels, setIsSavingLabels] = React.useState(false); - + // Pagination state const [currentPage, setCurrentPage] = React.useState(1); const [labelsDialogPage, setLabelsDialogPage] = React.useState(1); @@ -253,7 +253,7 @@ export default function TranslationsPage() { if (selectedLocale === localeCode) { setSelectedLocale(""); } - + // Close the dialog after successful deletion setDeletingLocale(null); } catch (error) { @@ -335,7 +335,7 @@ export default function TranslationsPage() { // Refresh terms to show updated labels refetchTerms(); - + setIsLabelsOpen(false); setLabelingTerm(null); setSelectedLabelIds([]); @@ -443,61 +443,61 @@ export default function TranslationsPage() { - - Add Locale - - Add a new language to your project - - -
-
- - -

- Choose from predefined locale codes with language and region -

+ + Add Locale + + Add a new language to your project + + +
+
+ + +

+ Choose from predefined locale codes with language and region +

+
-
- - - - - - + + + + + + )}
@@ -575,54 +575,54 @@ export default function TranslationsPage() { {permissions.canManageLocales && locales.length > 0 && ( - { - if (open) { - const locale = locales.find((l) => l.locale.code === selectedLocale); - if (locale && 'id' in locale) setDeletingLocale((locale as { id: string }).id); - } else if (!deleteLocaleMutation.isPending) { - setDeletingLocale(null); - } - }} - > - - - - - - Delete Locale - - Are you sure you want to delete this locale? All translations for this language will be permanently removed. - - - - Cancel - { - const locale = locales.find((l) => l.locale.code === selectedLocale); - if (locale && 'locale' in locale && 'code' in locale.locale) { - handleDeleteLocale(locale.locale.code); - } - }} - disabled={deleteLocaleMutation.isPending && deletingLocale !== null} - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" - > - {deleteLocaleMutation.isPending && deletingLocale !== null ? ( - <> - - Deleting... - - ) : ( - "Delete" - )} - - - - + { + if (open) { + const locale = locales.find((l) => l.locale.code === selectedLocale); + if (locale && 'id' in locale) setDeletingLocale((locale as { id: string }).id); + } else if (!deleteLocaleMutation.isPending) { + setDeletingLocale(null); + } + }} + > + + + + + + Delete Locale + + Are you sure you want to delete this locale? All translations for this language will be permanently removed. + + + + Cancel + { + const locale = locales.find((l) => l.locale.code === selectedLocale); + if (locale && 'locale' in locale && 'code' in locale.locale) { + handleDeleteLocale(locale.locale.code); + } + }} + disabled={deleteLocaleMutation.isPending && deletingLocale !== null} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + {deleteLocaleMutation.isPending && deletingLocale !== null ? ( + <> + + Deleting... + + ) : ( + "Delete" + )} + + + + )} @@ -655,217 +655,223 @@ export default function TranslationsPage() { const isTermLocked = term.isLocked || false; const canEditLockedTerm = permissions.isOwner || permissions.isAdmin; const isDisabled = isTermLocked && !canEditLockedTerm; - + return ( - - -
-
- {term.value} - {isTermLocked && ( - - - - )} -
- {term.labels && term.labels.length > 0 && ( - -
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} - {term.labels.map((label: any) => ( - label.value ? ( - - - + +
+
+ {term.value} + {isTermLocked && ( + + + + )} +
+ {term.labels && term.labels.length > 0 && ( + +
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {term.labels.map((label: any) => ( + label.value ? ( + + + + {label.name} + + + +

{label.value}

+
+
+ ) : ( + {label.name} - - -

{label.value}

-
- - ) : ( - - {label.name} - - ) - ))} -
-
- )} -
-
- -
-