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:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user