diff --git a/scripts/check-translations.mjs b/scripts/check-translations.mjs new file mode 100644 index 0000000..ddc1847 --- /dev/null +++ b/scripts/check-translations.mjs @@ -0,0 +1,25 @@ +import { readFileSync } from 'fs'; + +const enRaw = readFileSync('src/app/i18n/translations/en.ts', 'utf8'); +const deRaw = readFileSync('src/app/i18n/translations/de.ts', 'utf8'); + +// Extract all "key": "value" pairs +const kvRegex = /"(k[0-9a-f]+)": "([^"\\]*(\\.[^"\\]*)*)"/g; + +const en = {}; +const de = {}; +let m; +while ((m = kvRegex.exec(enRaw)) !== null) en[m[1]] = m[2]; +kvRegex.lastIndex = 0; +while ((m = kvRegex.exec(deRaw)) !== null) de[m[1]] = m[2]; + +// Find keys in both with identical values +const same = Object.keys(en).filter(k => de[k] !== undefined && en[k] === de[k]); +console.log(`Keys with identical en/de values (${same.length} total):`); +same.forEach(k => console.log(` ${k}: ${JSON.stringify(en[k])}`)); + +// Find keys in en that look German (contain typical German words or chars) +const germanPattern = /\b(den|die|das|der|und|nicht|Sie|Ihr|Bitte|werden|wurde|kein|eine|einem|ist|sind|können|bitte|beim|durch|für|mit|Keine|Alle|Alle|beim|oder)\b/; +const germanInEn = Object.keys(en).filter(k => germanPattern.test(en[k])); +console.log(`\nKeys in en.ts that look German (${germanInEn.length} total):`); +germanInEn.forEach(k => console.log(` ${k}: ${JSON.stringify(en[k])}`)); diff --git a/src/app/admin/affiliate-management/page.tsx b/src/app/admin/affiliate-management/page.tsx index af581bc..5c52c45 100644 --- a/src/app/admin/affiliate-management/page.tsx +++ b/src/app/admin/affiliate-management/page.tsx @@ -173,9 +173,7 @@ export default function AffiliateManagementPage() { className="w-full sm:w-auto px-4 py-2.5 rounded-lg border border-gray-300 bg-white text-gray-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent" > {categories.map(cat => ( - + ))} @@ -347,11 +345,9 @@ export default function AffiliateManagementPage() {

{t('autofix.k19f2c5dc')}

-

- {searchQuery || categoryFilter !== 'all' - ? 'Try adjusting your search or filter' - : 'Get started by adding a new affiliate partner'} -

+

{searchQuery || categoryFilter !== 'all' + ? t('autofix.k6c341c65') + : t('autofix.kfd0ee006')}

)} @@ -877,9 +873,7 @@ function EditAffiliateModal({ affiliate, onClose, onUpdate }: { alt="Logo" className="max-h-[180px] max-w-full object-contain" /> -

- {logoFile ? '🆕 New logo selected' : '📷 Current logo'} -

+

{logoFile ? t('autofix.k55d88592') : t('autofix.k86d84b6d')}

+ >{saving ? t('autofix.kac6cedc7') : 'Save'} {saved && ( {t('autofix.ka29ac729')} )} diff --git a/src/app/admin/contract-management/components/contractEditor.tsx b/src/app/admin/contract-management/components/contractEditor.tsx index e67f000..fd29bc6 100644 --- a/src/app/admin/contract-management/components/contractEditor.tsx +++ b/src/app/admin/contract-management/components/contractEditor.tsx @@ -283,9 +283,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi type="button" onClick={() => setIsPreview((v) => !v)} className="inline-flex items-center rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-900 px-4 py-2 text-sm font-medium shadow transition" - > - {isPreview ? 'Switch to Code' : 'Preview HTML'} - + >{isPreview ? t('autofix.k49165061') : t('autofix.k95d19932')} {isPreview && ( + >{loading ? t('autofix.k832387c5') : 'Refresh'} @@ -838,11 +836,9 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props) )} {!activeFamily && ( -
- {items.length === 0 - ? 'No templates available yet. Create the first template to populate this workspace.' - : t('autofix.k047a175d')} -
+
{items.length === 0 + ? t('autofix.k3772baff') + : t('autofix.k047a175d')}
)} diff --git a/src/app/admin/contract-management/components/contractUploadCompanyStamp.tsx b/src/app/admin/contract-management/components/contractUploadCompanyStamp.tsx index 7754b3c..26f0a01 100644 --- a/src/app/admin/contract-management/components/contractUploadCompanyStamp.tsx +++ b/src/app/admin/contract-management/components/contractUploadCompanyStamp.tsx @@ -397,9 +397,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) { onClick={confirmUpload} disabled={modalUploading || !modalFile} className="text-sm px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white disabled:opacity-60 transition" - > - {modalUploading ? 'Uploading…' : 'Upload'} - + >{modalUploading ? t('autofix.ka3076020') : 'Upload'} diff --git a/src/app/admin/contract-management/components/mailTemplatesManager.tsx b/src/app/admin/contract-management/components/mailTemplatesManager.tsx index 23e2938..4f59ad3 100644 --- a/src/app/admin/contract-management/components/mailTemplatesManager.tsx +++ b/src/app/admin/contract-management/components/mailTemplatesManager.tsx @@ -1,7 +1,7 @@ 'use client'; import { useTranslation } from '../../../i18n/useTranslation'; -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; import { authFetch } from '../../../utils/authFetch'; import { useToast } from '../../../components/toast/toastComponent'; @@ -70,16 +70,19 @@ type EditorState = { html_content: string; }; -const BLANK_EDITOR: EditorState = { - template_type: '', - name: '', - subject: '', - html_content: '

New Template

Edit this HTML content.

', -}; +function createBlankEditor(t: (key: string) => string): EditorState { + return { + template_type: '', + name: '', + subject: '', + html_content: `

${t('autofix.k88f0d12a')}

${t('autofix.k4f530782')}

`, + }; +} export default function MailTemplatesManager() { const { t } = useTranslation(); const { showToast } = useToast(); + const blankEditor = useMemo(() => createBlankEditor(t), [t]); const [tab, setTab] = useState<'active' | 'archived'>('active'); const [templates, setTemplates] = useState([]); @@ -89,7 +92,7 @@ export default function MailTemplatesManager() { const [selectedId, setSelectedId] = useState(null); const [isCreating, setIsCreating] = useState(false); const [i18nSyncStatus, setI18nSyncStatus] = useState<'idle' | 'syncing' | 'synced' | 'error'>('idle'); - const [editor, setEditor] = useState(BLANK_EDITOR); + const [editor, setEditor] = useState(blankEditor); const [isSaving, setIsSaving] = useState(false); const [actionLoadingId, setActionLoadingId] = useState(null); @@ -124,8 +127,8 @@ export default function MailTemplatesManager() { fetchTemplates(tab === 'archived'); setSelectedId(null); setIsCreating(false); - setEditor(BLANK_EDITOR); - }, [tab, fetchTemplates]); + setEditor(blankEditor); + }, [tab, fetchTemplates, blankEditor]); const visibleTemplates = tab === 'archived' ? templates.filter((tmpl) => tmpl.is_archived) @@ -149,7 +152,7 @@ export default function MailTemplatesManager() { setIsCreating(true); setSelectedId(null); setI18nSyncStatus('idle'); - setEditor(BLANK_EDITOR); + setEditor(blankEditor); }; const handleSave = async () => { @@ -179,7 +182,7 @@ export default function MailTemplatesManager() { savedTemplateType = created.template_type; savedSubject = created.subject ?? editor.subject; savedHtmlContent = created.html_content; - showToast({ type: 'success', message: 'Mail template created.' }); + showToast({ variant: 'success', message: t('autofix.ke8a3bd92') }); await fetchTemplates(false); setTab('active'); setIsCreating(false); @@ -205,7 +208,7 @@ export default function MailTemplatesManager() { const err = await res.json().catch(() => ({})); throw new Error((err as any)?.message || `Error ${res.status}`); } - showToast({ type: 'success', message: 'Mail template updated.' }); + showToast({ variant: 'success', message: t('autofix.k9535ed27') }); await fetchTemplates(tab === 'archived'); } @@ -217,7 +220,7 @@ export default function MailTemplatesManager() { .catch(() => setI18nSyncStatus('error')); } } catch (e: any) { - showToast({ type: 'error', message: e?.message || 'Failed to save template.' }); + showToast({ variant: 'error', message: e?.message || t('autofix.kb743b7c2') }); } finally { setIsSaving(false); } @@ -231,10 +234,10 @@ export default function MailTemplatesManager() { const err = await res.json().catch(() => ({})); throw new Error((err as any)?.message || `Error ${res.status}`); } - showToast({ type: 'success', message: 'Template activated.' }); + showToast({ variant: 'success', message: t('autofix.ke1a18ce6') }); await fetchTemplates(tab === 'archived'); } catch (e: any) { - showToast({ type: 'error', message: e?.message || 'Failed to activate template.' }); + showToast({ variant: 'error', message: e?.message || t('autofix.k1c5f641a') }); } finally { setActionLoadingId(null); } @@ -248,21 +251,67 @@ export default function MailTemplatesManager() { const err = await res.json().catch(() => ({})); throw new Error((err as any)?.message || `Error ${res.status}`); } - showToast({ type: 'success', message: 'Template archived.' }); + showToast({ variant: 'success', message: t('autofix.k2f162f5d') }); if (selectedId === id) { setSelectedId(null); - setEditor(BLANK_EDITOR); + setEditor(blankEditor); } await fetchTemplates(tab === 'archived'); } catch (e: any) { - showToast({ type: 'error', message: e?.message || 'Failed to archive template.' }); + showToast({ variant: 'error', message: e?.message || t('autofix.kff7b3b21') }); } finally { setActionLoadingId(null); } }; + const handleUnarchive = async (id: number) => { + setActionLoadingId(id); + try { + const res = await authFetch(`${MAIL_TEMPLATES_BASE}/${id}/unarchive`, { method: 'PATCH' }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error((err as any)?.message || (err as any)?.error || `Error ${res.status}`); + } + showToast({ variant: 'success', message: t('autofix.kc3a71e92') }); + setSelectedId(null); + setEditor(blankEditor); + setTab('active'); + await fetchTemplates(false); + } catch (e: any) { + showToast({ variant: 'error', message: e?.message || t('autofix.ka91f3c05') }); + } finally { + setActionLoadingId(null); + } + }; + + const removeI18nKeys = async (templateType: string) => { + try { + const res = await fetch('/api/i18n/translations', { cache: 'no-store' }); + const data = await res.json(); + if (!res.ok || !data?.ok) return; + const translations: Record> = data.translations ?? {}; + const sKey = subjectI18nKey(templateType); + const hKey = htmlI18nKey(templateType); + const updated: Record> = {}; + for (const lang of Object.keys(translations)) { + const copy = { ...translations[lang] }; + delete copy[sKey]; + delete copy[hKey]; + updated[lang] = copy; + } + await fetch('/api/i18n/translations', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ translations: updated }), + }); + } catch { + // best-effort — don't block the delete flow + } + }; + const handleDelete = async (id: number) => { - if (!window.confirm('Delete this mail template? This cannot be undone.')) return; + if (!window.confirm(t('autofix.ka63bb731'))) return; + const templateType = templates.find((tmpl) => tmpl.id === id)?.template_type ?? ''; setActionLoadingId(id); try { const res = await authFetch(`${MAIL_TEMPLATES_BASE}/${id}`, { method: 'DELETE' }); @@ -270,14 +319,17 @@ export default function MailTemplatesManager() { const err = await res.json().catch(() => ({})); throw new Error((err as any)?.message || `Error ${res.status}`); } - showToast({ type: 'success', message: 'Template deleted.' }); + showToast({ variant: 'success', message: t('autofix.kf6b83106') }); if (selectedId === id) { setSelectedId(null); - setEditor(BLANK_EDITOR); + setEditor(blankEditor); } await fetchTemplates(tab === 'archived'); + if (templateType.trim()) { + await removeI18nKeys(templateType); + } } catch (e: any) { - showToast({ type: 'error', message: e?.message || 'Failed to delete template.' }); + showToast({ variant: 'error', message: e?.message || t('autofix.kccf6593a') }); } finally { setActionLoadingId(null); } @@ -311,9 +363,7 @@ export default function MailTemplatesManager() { onClick={handleSave} disabled={isSaving} className="inline-flex items-center rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-800 hover:bg-slate-50 transition disabled:opacity-50" - > - {isSaving ? 'Saving…' : (isCreating ? 'Create' : 'Save')} - + >{isSaving ? t('autofix.kac6cedc7') : (isCreating ? t('autofix.k987f2b90') : t('autofix.k9f7c3d1e'))} )} @@ -337,9 +387,7 @@ export default function MailTemplatesManager() { className={`rounded-xl px-4 py-2 text-sm font-semibold transition ${ tab === 'active' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-800' }`} - > - Active Templates - + >{t('autofix.kbdcb654a')} )} + {template.is_archived && ( + + )} + + {saving ? t('autofix.kac6cedc7') : 'Save'} diff --git a/src/app/admin/dev-management/page.tsx b/src/app/admin/dev-management/page.tsx index f465144..e565612 100644 --- a/src/app/admin/dev-management/page.tsx +++ b/src/app/admin/dev-management/page.tsx @@ -433,8 +433,7 @@ export default function DevManagementPage() { disabled={fixingAll || exoscaleLoading || structureUsers.length === 0} className="inline-flex items-center justify-center gap-2 rounded-lg bg-blue-900 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-800 disabled:opacity-60" > - {fixingAll ? 'Creating...' : 'Create All'} - + {fixingAll ? 'Creating...' : t('autofix.k1db77fc0')} @@ -545,8 +544,7 @@ export default function DevManagementPage() { disabled={fixingAll || exoscaleLoading || looseUsers.length === 0} className="inline-flex items-center justify-center gap-2 rounded-lg bg-blue-900 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-800 disabled:opacity-60" > - {fixingAll ? 'Moving...' : 'Move All to Contract'} - + {fixingAll ? 'Moving...' : t('autofix.k0188c7bc')} diff --git a/src/app/admin/finance-management/vat-edit/page.tsx b/src/app/admin/finance-management/vat-edit/page.tsx index ee1558b..9da656f 100644 --- a/src/app/admin/finance-management/vat-edit/page.tsx +++ b/src/app/admin/finance-management/vat-edit/page.tsx @@ -66,9 +66,7 @@ export default function VatEditPage() { className="hidden" onChange={e => onImport(e.target.files?.[0] || null)} disabled={importing} - /> - {importing ? 'Importing...' : 'Import CSV'} - + />{importing ? 'Importing...' : t('autofix.k8a59f09e')} + : (wizardIndex >= wizardMissingCount - 1 ? t('autofix.k230e2c3c') : t('autofix.kb270a988'))} diff --git a/src/app/admin/language-management/page.tsx b/src/app/admin/language-management/page.tsx index 0079a99..c04278c 100644 --- a/src/app/admin/language-management/page.tsx +++ b/src/app/admin/language-management/page.tsx @@ -609,7 +609,7 @@ export default function LanguageManagementPage() { showToast({ variant: 'info', - message: 'Reloading page to refresh namespace and key updates from auto-fix output.', + message: t('autofix.k3871d88e'), duration: 1800, }); @@ -866,9 +866,7 @@ export default function LanguageManagementPage() { onClick={handleSaveAll} disabled={isSavingPreferences} className="rounded-2xl bg-slate-900 text-white px-5 py-2 text-sm font-semibold shadow-[0_18px_40px_-24px_rgba(15,23,42,0.85)] hover:bg-slate-800 transition disabled:opacity-50" - > - {isSavingPreferences ? 'Saving…' : 'Save'} - + >{isSavingPreferences ? t('autofix.kac6cedc7') : 'Save'} )} diff --git a/src/app/admin/matrix-management/detail/components/searchModal.tsx b/src/app/admin/matrix-management/detail/components/searchModal.tsx index 63b9d95..38b16ba 100644 --- a/src/app/admin/matrix-management/detail/components/searchModal.tsx +++ b/src/app/admin/matrix-management/detail/components/searchModal.tsx @@ -323,9 +323,7 @@ export default function SearchModal({ type="submit" disabled={loading || query.trim().length < 3} className="flex-1 rounded-md bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white px-3 py-2 text-sm font-medium shadow-sm transition" - > - {loading ? 'Searching…' : 'Search'} - + >{loading ? t('autofix.kdff3e58d') : 'Search'} + >{adding ? t('autofix.k12f2d162') : t('autofix.k59b7a324')} )} diff --git a/src/app/admin/matrix-management/page.tsx b/src/app/admin/matrix-management/page.tsx index 184abae..ce621a3 100644 --- a/src/app/admin/matrix-management/page.tsx +++ b/src/app/admin/matrix-management/page.tsx @@ -396,11 +396,9 @@ export default function MatrixManagementPage() { ${m.status === 'active' ? 'border-red-300 text-red-700 hover:bg-red-50 disabled:opacity-60' : 'border-green-300 text-green-700 hover:bg-green-50 disabled:opacity-60'}`} - > - {mutatingId === m.id - ? (m.status === 'active' ? 'Deactivating…' : 'Activating…') - : (m.status === 'active' ? 'Deactivate' : 'Activate')} - + >{mutatingId === m.id + ? (m.status === 'active' ? t('autofix.k871d457e') : t('autofix.k5bcb3e1f')) + : (m.status === 'active' ? 'Deactivate' : 'Activate')} {t('autofix.k27f56959')} + >{creating ? t('autofix.k27b5b842') : t('autofix.k75078d0b')} diff --git a/src/app/admin/subscriptions/createSubscription/page.tsx b/src/app/admin/subscriptions/createSubscription/page.tsx index d952839..e987816 100644 --- a/src/app/admin/subscriptions/createSubscription/page.tsx +++ b/src/app/admin/subscriptions/createSubscription/page.tsx @@ -27,6 +27,9 @@ export default function CreateSubscriptionPage() { const [showCropModal, setShowCropModal] = useState(false); const [currency, setCurrency] = useState('EUR'); const [isFeatured, setIsFeatured] = useState(false); + // Gallery images (multi-upload, no crop) + const [galleryFiles, setGalleryFiles] = useState([]); + const [galleryPreviews, setGalleryPreviews] = useState([]); // Fixed billing defaults (locked: month / 1) const billingInterval: 'month' = 'month'; const intervalCount: number = 1; @@ -42,7 +45,8 @@ export default function CreateSubscriptionPage() { currency, is_featured: isFeatured, state: state === 'available', - pictureFile + pictureFile, + pictureFiles: galleryFiles.length ? galleryFiles : undefined, }); router.push('/admin/subscriptions'); } catch (e: any) { @@ -55,8 +59,9 @@ export default function CreateSubscriptionPage() { return () => { if (previewUrl) URL.revokeObjectURL(previewUrl); if (originalImageSrc) URL.revokeObjectURL(originalImageSrc); + galleryPreviews.forEach(u => URL.revokeObjectURL(u)); }; - }, []); + }, [previewUrl, originalImageSrc, galleryPreviews]); function handleSelectFile(file?: File) { if (!file) return; @@ -71,6 +76,7 @@ export default function CreateSubscriptionPage() { } setError(null); + if (originalImageSrc) URL.revokeObjectURL(originalImageSrc); // Create object URL for cropping const url = URL.createObjectURL(file); setOriginalImageSrc(url); @@ -83,193 +89,316 @@ export default function CreateSubscriptionPage() { setPictureFile(croppedFile); // Create preview URL + if (previewUrl) URL.revokeObjectURL(previewUrl); const url = URL.createObjectURL(croppedBlob); setPreviewUrl(url); } + function handleAddGalleryFiles(files: FileList | null) { + if (!files || files.length === 0) return; + const allowed = ['image/jpeg', 'image/png', 'image/webp']; + const newFiles: File[] = []; + const newPreviews: string[] = []; + for (const file of Array.from(files)) { + if (!allowed.includes(file.type)) { + setError(`"${file.name}" is not a valid image type (JPG, PNG, WebP only).`); + continue; + } + if (file.size > 10 * 1024 * 1024) { + setError(`"${file.name}" exceeds the 10MB limit.`); + continue; + } + newFiles.push(file); + newPreviews.push(URL.createObjectURL(file)); + } + setGalleryFiles(prev => [...prev, ...newFiles]); + setGalleryPreviews(prev => [...prev, ...newPreviews]); + } + + function handleRemoveGalleryImage(index: number) { + setGalleryPreviews(prev => { + URL.revokeObjectURL(prev[index]); + return prev.filter((_, i) => i !== index); + }); + setGalleryFiles(prev => prev.filter((_, i) => i !== index)); + } + + function handleSetThumbnailFromGallery(index: number) { + const source = galleryFiles[index]; + if (!source) return; + setError(null); + const thumbFile = new File([source], source.name, { type: source.type }); + setPictureFile(thumbFile); + if (previewUrl) URL.revokeObjectURL(previewUrl); + const thumbPreview = URL.createObjectURL(source); + setPreviewUrl(thumbPreview); + } + return ( - -
-
- {/* Header */} -
-
-
-

{t('autofix.kaa30f0cd')}

-

{t('autofix.kf72d41db')}

-
- - {t('autofix.kd8a5ad17')} -
-
+ +
-
-
- {/* Picture Upload moved to top */} -
- -

Upload an image and crop it to fit the coffee thumbnail (16:9 aspect ratio, 144px height)

-
document.getElementById('file-upload')?.click()} - onDragOver={e => e.preventDefault()} - onDrop={e => { - e.preventDefault(); - if (e.dataTransfer.files?.[0]) handleSelectFile(e.dataTransfer.files[0]); - }} - > - {!previewUrl && ( -
-
- )} - {previewUrl && ( -
- Preview -
- - -
-
- )} - handleSelectFile(e.target.files?.[0])} - /> -
-
- - {/* Title moved above description */} -
- - setTitle(e.target.value)} - /> -
- - {/* Description now after title */} -
- -