feat(i18n): migrate hardcoded German strings to t() across 104 components/pages — AST-based batch with re-parse gate, 423 new de.json keys; tsc clean; vitest failures byte-identical to clean-tree baseline (pre-existing)

This commit is contained in:
Agent Zero
2026-08-27 01:43:06 +02:00
parent 5680179260
commit 4cb5298768
105 changed files with 1204 additions and 459 deletions
@@ -303,7 +303,7 @@ export function ComposeModal({
label={t('mail.to')}
{...register('to')}
error={errorMsg(errors.to?.message)}
placeholder="recipient@example.com"
placeholder={t('composeModal.recipientexamplecom')}
required
data-testid="compose-to"
/>
@@ -322,13 +322,13 @@ export function ComposeModal({
label={t('mail.cc')}
{...register('cc')}
error={errorMsg(errors.cc?.message)}
placeholder="cc@example.com"
placeholder={t('composeModal.ccexamplecom')}
/>
<Input
label={t('mail.bcc')}
{...register('bcc')}
error={errorMsg(errors.bcc?.message)}
placeholder="bcc@example.com"
placeholder={t('composeModal.bccexamplecom')}
/>
</div>
)}
@@ -376,7 +376,7 @@ export function ComposeModal({
>
{t('mail.addAttachment')}
</Button>
<span className="text-xs text-secondary-400">Max 25 MB per file</span>
<span className="text-xs text-secondary-400">{t('composeModal.max25mbperfile')}</span>
</div>
{attachments.length > 0 && (
<ul className="mt-2 space-y-1">
@@ -304,7 +304,7 @@ export function MailComposeForm({
label={t('mail.to')}
{...register('to')}
error={errorMsg(errors.to?.message)}
placeholder="recipient@example.com"
placeholder={t('mailComposeForm.recipientexamplecom')}
required
data-testid="compose-to"
/>
@@ -323,13 +323,13 @@ export function MailComposeForm({
label={t('mail.cc')}
{...register('cc')}
error={errorMsg(errors.cc?.message)}
placeholder="cc@example.com"
placeholder={t('mailComposeForm.ccexamplecom')}
/>
<Input
label={t('mail.bcc')}
{...register('bcc')}
error={errorMsg(errors.bcc?.message)}
placeholder="bcc@example.com"
placeholder={t('mailComposeForm.bccexamplecom')}
/>
</div>
)}
@@ -377,7 +377,7 @@ export function MailComposeForm({
>
{t('mail.addAttachment')}
</Button>
<span className="text-xs text-secondary-400">Max 25 MB per file</span>
<span className="text-xs text-secondary-400">{t('mailComposeForm.max25mbperfile')}</span>
</div>
{attachments.length > 0 && (
<ul className="mt-2 space-y-1">
@@ -7,6 +7,7 @@
import React, { useState, useRef, useEffect } from 'react';
import { Filter, Plus, X, Bookmark } from 'lucide-react';
import { useTranslation } from 'react-i18next';
// ─── Field definitions ───────────────────────────────────────────────────────
@@ -183,6 +184,7 @@ function newConditionId() {
}
export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], onSaveFilter, onLoadFilter, onDeleteFilter }: MailFilterPanelProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@@ -272,8 +274,8 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
<div ref={btnRef} className="relative flex-shrink-0">
<button
onClick={handleToggle}
title="Filter"
aria-label="Filter"
title={t('mailFilterPanel.filter')}
aria-label={t('mailFilterPanel.filter')}
className={`
inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium
transition-colors duration-100 cursor-pointer relative
@@ -311,7 +313,7 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
<div className="flex items-center gap-2">
{activeCount > 0 && (
<button onClick={clearAll} className="text-xs text-secondary-500 hover:text-red-600 transition-colors">
Alle löschen
{t('mailFilterPanel.allelöschen')}
</button>
)}
<button onClick={() => setOpen(false)} className="p-1 rounded hover:bg-secondary-100 text-secondary-400 hover:text-secondary-600">
@@ -323,7 +325,7 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
{/* Logic toggle */}
{activeCount > 0 && (
<div className="flex items-center gap-2 px-4 py-2 border-b border-secondary-100">
<span className="text-xs text-secondary-500">Bedingungen verknüpfen:</span>
<span className="text-xs text-secondary-500">{t('mailFilterPanel.bedingungenverknüpfen')}</span>
<button onClick={toggleLogic} className={`px-2 py-0.5 text-xs font-medium rounded transition-colors ${filters.logic === 'AND' ? 'bg-primary-600 text-white' : 'bg-secondary-100 text-secondary-600 hover:bg-secondary-200'}`}>UND</button>
<button onClick={toggleLogic} className={`px-2 py-0.5 text-xs font-medium rounded transition-colors ${filters.logic === 'OR' ? 'bg-primary-600 text-white' : 'bg-secondary-100 text-secondary-600 hover:bg-secondary-200'}`}>ODER</button>
</div>
@@ -348,7 +350,7 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
{/* Saved filters */}
{(savedFilters?.length ?? 0) > 0 && (
<div className="px-4 py-2 border-b border-secondary-100">
<div className="text-[10px] font-semibold uppercase tracking-wide text-secondary-400 mb-1.5">Gespeicherte Filter</div>
<div className="text-[10px] font-semibold uppercase tracking-wide text-secondary-400 mb-1.5">{t('mailFilterPanel.gespeichertefilter')}</div>
<div className="space-y-0.5">
{savedFilters.map((sf) => (
<div key={sf.id} className="flex items-center gap-1 group">
@@ -369,7 +371,7 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
<div className="px-4 py-3 space-y-2">
{(filters.conditions?.length ?? 0) === 0 && (
<div className="text-center py-6 text-xs text-secondary-400">
Keine Filter aktiv. Klicke unten um eine Bedingung hinzuzufügen.
{t('mailFilterPanel.keinefilteraktivklickeuntenum')}
</div>
)}
{filters.conditions.map((cond, idx) => {
@@ -409,13 +411,13 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
{currentOp?.needsValue ? (
def?.type === 'select' ? (
<select value={cond.value} onChange={(e) => updateCondition(cond.id, { value: e.target.value })} className="px-2 py-1.5 text-xs border border-secondary-200 rounded bg-white cursor-pointer focus:outline-none focus:border-primary-400">
<option value=""> wählen </option>
<option value="">{t('mailFilterPanel.wählen')}</option>
{def.options?.map((opt) => (<option key={opt.value} value={opt.value}>{opt.label}</option>))}
</select>
) : def?.type === 'date' ? (
<input type="date" value={cond.value} onChange={(e) => updateCondition(cond.id, { value: e.target.value })} className="px-2 py-1.5 text-xs border border-secondary-200 rounded focus:outline-none focus:border-primary-400" />
) : (
<input type="text" value={cond.value} onChange={(e) => updateCondition(cond.id, { value: e.target.value })} placeholder="Wert…" className="w-24 px-2 py-1.5 text-xs border border-secondary-200 rounded focus:outline-none focus:border-primary-400" />
<input type="text" value={cond.value} onChange={(e) => updateCondition(cond.id, { value: e.target.value })} placeholder={t('mailFilterPanel.wert')} className="w-24 px-2 py-1.5 text-xs border border-secondary-200 rounded focus:outline-none focus:border-primary-400" />
)
) : (
<div className="w-24" />
@@ -434,12 +436,12 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
<div className="flex items-center gap-2">
<button onClick={addCondition} className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors">
<Plus className="w-3.5 h-3.5" />
Bedingung hinzufügen
{t('mailFilterPanel.bedingunghinzufügen')}
</button>
{activeCount > 0 && onSaveFilter && (
<button onClick={handleSaveFilter} className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors">
<Bookmark className="w-3.5 h-3.5" />
Filter speichern
{t('mailFilterPanel.filterspeichern')}
</button>
)}
</div>
@@ -116,6 +116,7 @@ function ContextMenu({
onClose: () => void;
onEmptyFolder: (folderId: string, folderName: string) => void;
}) {
const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -136,7 +137,7 @@ function ContextMenu({
onClick={() => { onEmptyFolder(state.folderId, state.folderName); onClose(); }}
className="w-full text-left px-3 py-1.5 text-sm text-red-600 hover:bg-red-50"
>
Ordner leeren
{t('mailFolderTree.ordnerleeren')}
</button>
</div>
);
@@ -7,6 +7,7 @@
import React, { useState, useRef, useEffect } from 'react';
import { Group as GroupIcon, Plus, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
type FieldType = 'text' | 'number' | 'date';
@@ -114,6 +115,7 @@ interface MailGroupPanelProps {
}
export function MailGroupPanel({ groupState, onGroupChange }: MailGroupPanelProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@@ -168,8 +170,8 @@ export function MailGroupPanel({ groupState, onGroupChange }: MailGroupPanelProp
<div ref={btnRef} className="relative flex-shrink-0">
<button
onClick={handleToggle}
title="Gruppierung"
aria-label="Gruppierung"
title={t('mailGroupPanel.gruppierung')}
aria-label={t('mailGroupPanel.gruppierung')}
className={`inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium transition-colors duration-100 cursor-pointer relative ${open || activeCount > 0 ? 'bg-primary-100 text-primary-700' : 'text-secondary-600 hover:bg-secondary-100 hover:text-secondary-900'}`}
>
<GroupIcon className="w-3.5 h-3.5" strokeWidth={2} />
@@ -184,12 +186,12 @@ export function MailGroupPanel({ groupState, onGroupChange }: MailGroupPanelProp
<div className="flex items-center justify-between px-4 py-3 border-b border-secondary-100">
<span className="text-sm font-semibold text-secondary-800">Gruppierung</span>
<div className="flex items-center gap-2">
{activeCount > 0 && <button onClick={clearAll} className="text-xs text-secondary-500 hover:text-red-600 transition-colors">Alle löschen</button>}
{activeCount > 0 && <button onClick={clearAll} className="text-xs text-secondary-500 hover:text-red-600 transition-colors">{t('mailGroupPanel.allelöschen')}</button>}
<button onClick={() => setOpen(false)} className="p-1 rounded hover:bg-secondary-100 text-secondary-400 hover:text-secondary-600"><X className="w-3.5 h-3.5" /></button>
</div>
</div>
<div className="px-4 py-3 space-y-2">
{groupState.conditions.length === 0 && <div className="text-center py-6 text-xs text-secondary-400">Keine Gruppierung aktiv. Klicke unten um ein Feld hinzuzufügen.</div>}
{groupState.conditions.length === 0 && <div className="text-center py-6 text-xs text-secondary-400">{t('mailGroupPanel.keinegruppierungaktivklickeuntenum')}</div>}
{groupState.conditions.map((cond, idx) => (
<div key={cond.id} className="flex items-center gap-1.5">
<span className="text-[10px] font-bold text-secondary-400 w-6 text-center">{idx + 1}.</span>
@@ -203,7 +205,7 @@ export function MailGroupPanel({ groupState, onGroupChange }: MailGroupPanelProp
))}
</div>
<div className="flex items-center justify-between px-4 py-3 border-t border-secondary-100">
<button onClick={addCondition} className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"><Plus className="w-3.5 h-3.5" />Feld hinzufügen</button>
<button onClick={addCondition} className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"><Plus className="w-3.5 h-3.5" />{t('mailGroupPanel.feldhinzufügen')}</button>
<button onClick={() => setOpen(false)} className="px-3 py-1 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded transition-colors">Fertig</button>
</div>
</div>
@@ -7,6 +7,7 @@
import React, { useState, useRef, useEffect } from 'react';
import { ArrowUpDown, Plus, X, ChevronUp, ChevronDown } from 'lucide-react';
import { useTranslation } from 'react-i18next';
type FieldType = 'text' | 'number' | 'date';
@@ -86,6 +87,7 @@ interface MailSortPanelProps {
}
export function MailSortPanel({ sortState, onSortChange }: MailSortPanelProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@@ -144,8 +146,8 @@ export function MailSortPanel({ sortState, onSortChange }: MailSortPanelProps) {
<div ref={btnRef} className="relative flex-shrink-0">
<button
onClick={handleToggle}
title="Sortieren"
aria-label="Sortieren"
title={t('mailSortPanel.sortieren')}
aria-label={t('mailSortPanel.sortieren')}
className={`inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium transition-colors duration-100 cursor-pointer relative ${open || activeCount > 0 ? 'bg-primary-100 text-primary-700' : 'text-secondary-600 hover:bg-secondary-100 hover:text-secondary-900'}`}
>
<ArrowUpDown className="w-3.5 h-3.5" strokeWidth={2} />
@@ -160,12 +162,12 @@ export function MailSortPanel({ sortState, onSortChange }: MailSortPanelProps) {
<div className="flex items-center justify-between px-4 py-3 border-b border-secondary-100">
<span className="text-sm font-semibold text-secondary-800">Sortieren</span>
<div className="flex items-center gap-2">
{activeCount > 0 && <button onClick={clearAll} className="text-xs text-secondary-500 hover:text-red-600 transition-colors">Alle löschen</button>}
{activeCount > 0 && <button onClick={clearAll} className="text-xs text-secondary-500 hover:text-red-600 transition-colors">{t('mailSortPanel.allelöschen')}</button>}
<button onClick={() => setOpen(false)} className="p-1 rounded hover:bg-secondary-100 text-secondary-400 hover:text-secondary-600"><X className="w-3.5 h-3.5" /></button>
</div>
</div>
<div className="px-4 py-3 space-y-2">
{sortState.conditions.length === 0 && <div className="text-center py-6 text-xs text-secondary-400">Keine Sortierung aktiv. Klicke unten um ein Feld hinzuzufügen.</div>}
{sortState.conditions.length === 0 && <div className="text-center py-6 text-xs text-secondary-400">{t('mailSortPanel.keinesortierungaktivklickeuntenum')}</div>}
{sortState.conditions.map((cond, idx) => (
<div key={cond.id} className="flex items-center gap-1.5">
<span className="text-[10px] font-bold text-secondary-400 w-6 text-center">{idx + 1}.</span>
@@ -182,7 +184,7 @@ export function MailSortPanel({ sortState, onSortChange }: MailSortPanelProps) {
))}
</div>
<div className="flex items-center justify-between px-4 py-3 border-t border-secondary-100">
<button onClick={addCondition} className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"><Plus className="w-3.5 h-3.5" />Feld hinzufügen</button>
<button onClick={addCondition} className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"><Plus className="w-3.5 h-3.5" />{t('mailSortPanel.feldhinzufügen')}</button>
<button onClick={() => setOpen(false)} className="px-3 py-1 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded transition-colors">Fertig</button>
</div>
</div>
+5 -5
View File
@@ -108,7 +108,7 @@ export function PgpSettings() {
value={privateKey}
onChange={(e) => setPrivateKey(e.target.value)}
className="w-full min-h-32 border border-secondary-300 rounded-md p-3 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="-----BEGIN PGP PRIVATE KEY BLOCK-----"
placeholder={t('pgpSettings.beginpgpprivatekeyblock')}
data-testid="pgp-private-key"
/>
</div>
@@ -135,7 +135,7 @@ export function PgpSettings() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-900">{key.user_id}</p>
<p className="text-xs text-secondary-500">Key ID: {key.key_id}</p>
<p className="text-xs text-secondary-500">{t('pgpSettings.keyid')} {key.key_id}</p>
<p className="text-xs text-secondary-400">Fingerprint: {key.fingerprint}</p>
</div>
<span className="text-xs text-secondary-400">{key.is_private ? t('mail.privateKeyLabel') : t('mail.publicKey')}</span>
@@ -152,7 +152,7 @@ export function PgpSettings() {
label={t('mail.contactId')}
value={contactId}
onChange={(e) => setContactId(e.target.value)}
placeholder="contact-uuid"
placeholder={t('pgpSettings.contactuuid')}
/>
<div>
<label className="block text-sm font-medium text-secondary-700 mb-1">{t('mail.publicKey')}</label>
@@ -160,7 +160,7 @@ export function PgpSettings() {
value={contactPublicKey}
onChange={(e) => setContactPublicKey(e.target.value)}
className="w-full min-h-24 border border-secondary-300 rounded-md p-3 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----"
placeholder={t('pgpSettings.beginpgppublickeyblock')}
data-testid="contact-public-key"
/>
</div>
@@ -175,7 +175,7 @@ export function PgpSettings() {
{contactKeys.map((ck) => (
<li key={ck.contact_id} className="p-3 rounded-md border border-secondary-200">
<p className="text-sm font-medium text-secondary-900">{ck.contact_name}</p>
<p className="text-xs text-secondary-500">Key ID: {ck.key_id}</p>
<p className="text-xs text-secondary-500">{t('pgpSettings.keyid')} {ck.key_id}</p>
<p className="text-xs text-secondary-400">Fingerprint: {ck.fingerprint}</p>
</li>
))}
@@ -195,13 +195,13 @@ export function RichTextEditor({ content, onChange, placeholder, editable = true
<div className="w-px h-6 bg-secondary-200 mx-1" />
{/* Color */}
<label className="p-1.5 rounded hover:bg-secondary-100 cursor-pointer" title="Text color">
<label className="p-1.5 rounded hover:bg-secondary-100 cursor-pointer" title={t('richTextEditor.textcolor')}>
<Heading className="w-4 h-4" strokeWidth={2} />
<input
type="color"
className="sr-only"
onChange={(e) => editor.chain().focus().setColor(e.target.value).run()}
title="Text color"
title={t('richTextEditor.textcolor')}
/>
</label>
@@ -175,7 +175,7 @@ export function SignatureManager() {
<RichTextEditor
content={bodyHtmlValue}
onChange={(html: string) => setSigValue('body_html', html)}
placeholder="<p>Mit freundlichen Grüßen,<br>{{user.name}}</p>"
placeholder={t('signatureManager.pmitfreundlichengrüßenbruser')}
/>
</div>
<label className="flex items-center gap-2 text-sm text-secondary-700">