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