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
+10 -8
View File
@@ -44,10 +44,11 @@ interface TabDef {
}
function ChatPanel() {
const { t } = useTranslation();
return (
<div className="flex flex-col h-full" data-testid="chat-panel">
<div className="flex-1 overflow-y-auto p-3">
<p className="text-sm text-secondary-400 text-center py-8">Chat-Sidebar - Coming Soon</p>
<p className="text-sm text-secondary-400 text-center py-8">{t('aISidebar.chatsidebarcomingsoon')}</p>
</div>
</div>
);
@@ -103,6 +104,7 @@ export function AISidebar() {
}
const renderTabContent = () => {
const { t } = useTranslation();
if (aiSidebarTab === 'proactive') {
return (
<div className="flex flex-col h-full">
@@ -117,14 +119,14 @@ export function AISidebar() {
return (
<div className="flex flex-col h-full overflow-y-auto p-3 gap-2" data-testid="notification-list">
{notifications.length === 0 && (
<p className="text-sm text-secondary-400 text-center py-4">Keine Benachrichtigungen</p>
<p className="text-sm text-secondary-400 text-center py-4">{t('aISidebar.keinebenachrichtigungen')}</p>
)}
{notifications.map((msg, i) => (
<div key={i} className="flex items-start justify-between gap-2 px-3 py-2 rounded-lg border border-secondary-200 bg-secondary-50 hover:border-secondary-300 hover:bg-secondary-100 transition-colors text-sm text-secondary-700 group">
<span className="flex-1 leading-snug">{msg}</span>
<button
className="p-1 rounded text-secondary-300 hover:text-danger-600 hover:bg-danger-50 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0 mt-0.5"
aria-label="Benachrichtigung löschen"
aria-label={t('aISidebar.benachrichtigunglöschen')}
onClick={() => removeNotification(i)}
>
<X className="w-3.5 h-3.5" aria-hidden="true" strokeWidth={2} />
@@ -150,13 +152,13 @@ export function AISidebar() {
return (
<div className="flex flex-col items-center justify-center h-full text-center px-4 gap-3">
<p className="text-sm text-secondary-500">
Der volle KI-Chat ist auf der AI-Assistant-Seite verfügbar.
{t('aISidebar.dervollekichatistauf')}
</p>
<Link
to="/ai-assistant"
className="inline-flex items-center px-4 py-2 rounded-md bg-primary-600 text-white text-sm font-medium hover:bg-primary-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 min-h-touch"
>
AI Assistant öffnen
{t('aISidebar.aiassistantöffnen')}
</Link>
</div>
);
@@ -190,8 +192,8 @@ export function AISidebar() {
<button
onClick={toggleAISidebar}
className="p-1.5 rounded-md text-secondary-400 hover:text-secondary-600 hover:bg-secondary-100 min-h-touch min-w-touch flex items-center justify-center"
title="Einklappen"
aria-label="KI Assistent einklappen"
title={t('aISidebar.einklappen')}
aria-label={t('aISidebar.kiassistenteinklappen')}
>
{chevronRightIcon}
</button>
@@ -210,7 +212,7 @@ export function AISidebar() {
<button
onClick={toggleAISidebar}
className="inline-flex items-center gap-1 px-2 py-1 rounded text-sm text-secondary-700 hover:bg-secondary-100 min-h-touch"
aria-label="Zurück"
aria-label={t('aISidebar.zurück')}
data-testid="ai-sidebar-back"
>
<ChevronLeft className="w-5 h-5" aria-hidden="true" strokeWidth={2} />
@@ -128,6 +128,7 @@ function MessageFeed({
messages: Message[];
loading: boolean;
}) {
const { t } = useTranslation();
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -147,7 +148,7 @@ function MessageFeed({
if (messages.length === 0) {
return (
<div className="flex items-center justify-center h-full text-sm text-secondary-400">
Keine Nachrichten
{t('messageSidebar.keinenachrichten')}
</div>
);
}
@@ -205,6 +206,7 @@ function MessageInput({
onSend: (text: string) => void;
disabled: boolean;
}) {
const { t } = useTranslation();
const [text, setText] = useState('');
const handleSend = () => {
@@ -236,7 +238,7 @@ function MessageInput({
onClick={handleSend}
disabled={disabled || !text.trim()}
className="p-2 rounded-lg bg-primary-500 text-white hover:bg-primary-600 disabled:bg-secondary-200 disabled:text-secondary-400 transition-colors min-h-touch min-w-touch flex items-center justify-center"
aria-label="Senden"
aria-label={t('messageSidebar.senden')}
data-testid="message-send-btn"
>
{sendIcon}
@@ -386,6 +388,7 @@ export function MessageSidebar() {
// Determine which pinned conv to select when a quick-access button is clicked
const handleQuickAccess = (qa: QuickAccessDef) => {
const { t } = useTranslation();
setCurrentView(qa.view);
if (qa.view === 'conversations' && qa.filterPinned) {
// Find pinned conversation matching the filter by title
@@ -480,8 +483,8 @@ export function MessageSidebar() {
<button
onClick={toggleMessageSidebar}
className="p-1.5 rounded-md text-secondary-400 hover:text-secondary-600 hover:bg-secondary-100 min-h-touch min-w-touch flex items-center justify-center"
title="Einklappen"
aria-label="Messaging einklappen"
title={t('messageSidebar.einklappen')}
aria-label={t('messageSidebar.messagingeinklappen')}
>
{chevronRightIcon}
</button>
@@ -498,7 +501,7 @@ export function MessageSidebar() {
<p className="text-sm text-red-500 text-center py-2">{typeof error === "string" ? error : (error as any)?.message || "Ein Fehler ist aufgetreten"}</p>
)}
{!loading && conversations.length === 0 && !error && (
<p className="text-sm text-secondary-400 text-center py-4">Keine Konversationen</p>
<p className="text-sm text-secondary-400 text-center py-4">{t('messageSidebar.keinekonversationen')}</p>
)}
{/* Pinned conversations */}
{pinnedConversations.length > 0 && (
@@ -537,6 +540,7 @@ export function MessageSidebar() {
// ─── Main Content Area ───
const renderContent = () => {
const { t } = useTranslation();
if (currentView === 'team') {
return <TeamPanel onStartDirectChat={handleStartDirectChat} />;
}
@@ -564,7 +568,7 @@ export function MessageSidebar() {
)}
{isSystemLocked && (
<div className="p-3 border-t border-secondary-200 text-center">
<span className="text-xs text-secondary-400">System-Konversation (schreibgeschützt)</span>
<span className="text-xs text-secondary-400">{t('messageSidebar.systemkonversationschreibgeschützt')}</span>
</div>
)}
</div>
@@ -584,7 +588,7 @@ export function MessageSidebar() {
<button
onClick={toggleMessageSidebar}
className="inline-flex items-center gap-1 px-2 py-1 rounded text-sm text-secondary-700 hover:bg-secondary-100 min-h-touch"
aria-label="Zurück"
aria-label={t('messageSidebar.zurück')}
data-testid="message-sidebar-back"
>
<ChevronLeft className="w-5 h-5" aria-hidden="true" strokeWidth={2} />
@@ -1,7 +1,9 @@
import React from 'react';
import { usePluginToolbarStore, type ToolbarItem } from '@/store/pluginToolbarStore';
import { useTranslation } from 'react-i18next';
function ToolbarButton({ item }: { item: ToolbarItem }) {
const { t } = useTranslation();
return (
<button
onClick={item.onClick}
@@ -21,6 +23,7 @@ function ToolbarButton({ item }: { item: ToolbarItem }) {
}
function ToolbarSearch({ item }: { item: ToolbarItem }) {
const { t } = useTranslation();
const [expanded, setExpanded] = React.useState(false);
const [value, setValue] = React.useState('');
const inputRef = React.useRef<HTMLInputElement>(null);
@@ -74,7 +77,7 @@ function ToolbarSearch({ item }: { item: ToolbarItem }) {
<button
onClick={handleToggle}
className="ml-1 p-1 rounded hover:bg-secondary-100 text-secondary-400 hover:text-secondary-600 transition-colors"
aria-label="Suche schließen"
aria-label={t('pluginToolbar.sucheschließen')}
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<line x1="18" y1="6" x2="6" y2="18" />
+4 -4
View File
@@ -182,21 +182,21 @@ export function Sidebar() {
'flex flex-col transition-transform motion-safe:duration-300',
sidebarOpen ? 'translate-x-0' : '-translate-x-full md:hidden'
)}
aria-label="Seitenleiste Navigation"
aria-label={t('sidebar.seitenleistenavigation')}
data-testid="sidebar"
>
<div className="h-[58px] flex items-center gap-2 px-4 border-b border-secondary-700">
<button
onClick={() => navigate('/start')}
className="p-1.5 rounded-md text-secondary-400 hover:bg-secondary-700 hover:text-white flex items-center justify-center focus:outline-none"
aria-label="Zurück zur Startseite"
title="Zurück zur Startseite"
aria-label={t('sidebar.zurückzurstartseite')}
title={t('sidebar.zurückzurstartseite')}
>
<ArrowLeft className="w-4 h-4" />
</button>
<span className="text-xl font-bold text-white">leocrm</span>
</div>
<nav className="flex-1 overflow-y-auto py-4" aria-label="Hauptnavigation">
<nav className="flex-1 overflow-y-auto py-4" aria-label={t('sidebar.hauptnavigation')}>
<ul className="space-y-1 px-2">
{(() => {
const groups = new Map<string, typeof allMenuItems>();
@@ -27,6 +27,7 @@ import {
Workflow,
} from 'lucide-react';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
// Curated icon map — avoids `import * as LucideIcons` which loads ALL icons
// and causes OOM in tests (ARCH-063).
@@ -69,6 +70,7 @@ function getIcon(name: string): React.ReactNode {
}
export function SortableMenuItem({ id, label, icon, isGroup }: SortableMenuItemProps) {
const { t } = useTranslation();
const {
attributes,
listeners,
@@ -99,7 +101,7 @@ export function SortableMenuItem({ id, label, icon, isGroup }: SortableMenuItemP
className="cursor-grab active:cursor-grabbing text-secondary-400 hover:text-secondary-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 rounded"
{...attributes}
{...listeners}
aria-label="Ziehen zum Sortieren"
aria-label={t('sortableMenuItem.ziehenzumsortieren')}
type="button"
>
<GripVertical className="w-4 h-4" />
+1 -1
View File
@@ -63,7 +63,7 @@ export function TopBar() {
<button
onClick={toggleSidebar}
className="p-2 rounded-md bg-primary-600 text-white hover:bg-primary-700 min-h-touch min-w-touch flex items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
aria-label="Seitenleiste ein-/ausklappen"
aria-label={t('topBar.seitenleisteeinausklappen')}
aria-expanded={true}
>
<Menu className="w-4 h-4" aria-hidden="true" strokeWidth={2} />