fixed sysuser and removed redundant fallbacks

This commit is contained in:
ValueOn AG 2026-04-11 22:23:35 +02:00
parent d1f0b3c3d6
commit 994664f0b4
27 changed files with 140 additions and 177 deletions

View file

@ -28,20 +28,13 @@ function _getModeOptions(t: (key: string) => string): { value: ScheduleMode; tit
]; ];
} }
const MONTH_NAMES_DE = [ function _monthNames(t: (k: string) => string): string[] {
'Januar', return [
'Februar', t('Januar'), t('Februar'), t('März'), t('April'),
'März', t('Mai'), t('Juni'), t('Juli'), t('August'),
'April', t('September'), t('Oktober'), t('November'), t('Dezember'),
'Mai', ];
'Juni', }
'Juli',
'August',
'September',
'Oktober',
'November',
'Dezember',
];
function _getIntervalUnits(t: (key: string) => string): { value: IntervalUnit; label: string; title: string }[] { function _getIntervalUnits(t: (key: string) => string): { value: IntervalUnit; label: string; title: string }[] {
return [ return [
@ -285,7 +278,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
<div className={styles.scheduleFieldCol}> <div className={styles.scheduleFieldCol}>
<span className={styles.scheduleFieldLabel}>{t('Wochentage')}</span> <span className={styles.scheduleFieldLabel}>{t('Wochentage')}</span>
<div className={styles.scheduleWeekdayToggles}> <div className={styles.scheduleWeekdayToggles}>
{WEEKDAYS_MO_SO.map(({ cronDow, label }) => ( {WEEKDAYS_MO_SO.map(({ cronDow }) => (
<button <button
key={cronDow} key={cronDow}
type="button" type="button"
@ -294,7 +287,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
} }
onClick={() => toggleWeekday(cronDow)} onClick={() => toggleWeekday(cronDow)}
> >
{t(label)} {cronDow === 1 ? t('Mo') : cronDow === 2 ? t('Di') : cronDow === 3 ? t('Mi') : cronDow === 4 ? t('Do') : cronDow === 5 ? t('Fr') : cronDow === 6 ? t('Sa') : t('So')}
</button> </button>
))} ))}
</div> </div>
@ -365,9 +358,9 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
value={spec.monthIndex} value={spec.monthIndex}
onChange={(e) => push({ ...spec, monthIndex: Number(e.target.value) })} onChange={(e) => push({ ...spec, monthIndex: Number(e.target.value) })}
> >
{MONTH_NAMES_DE.map((name, i) => ( {_monthNames(t).map((name, i) => (
<option key={i + 1} value={i + 1}> <option key={i + 1} value={i + 1}>
{t(name)} {name}
</option> </option>
))} ))}
</select> </select>

View file

@ -117,7 +117,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
<option value="">{t('MIME-Typ wählen')}</option> <option value="">{t('MIME-Typ wählen')}</option>
{mimeTypeOptions.map((o) => ( {mimeTypeOptions.map((o) => (
<option key={o.value} value={o.value}> <option key={o.value} value={o.value}>
{t(o.label)} ({o.value}) {o.label} ({o.value})
</option> </option>
))} ))}
</select> </select>
@ -221,7 +221,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
> >
{operators.map((o) => ( {operators.map((o) => (
<option key={o.value} value={o.value}> <option key={o.value} value={o.value}>
{t(o.label)} {o.label}
</option> </option>
))} ))}
</select> </select>

View file

@ -53,7 +53,7 @@ export function CustomActionButton<T = any>({
if (typeof title === 'function') { if (typeof title === 'function') {
buttonTitle = title(row); buttonTitle = title(row);
} else if (typeof title === 'string') { } else if (typeof title === 'string') {
buttonTitle = t(title, title); // Try to translate, fallback to original buttonTitle = title;
} }
// Determine the final tooltip // Determine the final tooltip

View file

@ -258,7 +258,7 @@ export const MandateNavigation: React.FC = () => {
for (const sg of systemBlock.subgroups) { for (const sg of systemBlock.subgroups) {
children.push({ children.push({
id: sg.id, id: sg.id,
label: t(sg.title), label: sg.title,
children: sg.items.map(i => navigationItemToTreeNode(i, t)), children: sg.items.map(i => navigationItemToTreeNode(i, t)),
defaultExpanded: true, defaultExpanded: true,
}); });
@ -288,7 +288,7 @@ export const MandateNavigation: React.FC = () => {
if (items.length > 0) items.push({ type: 'separator' }); if (items.length > 0) items.push({ type: 'separator' });
const subgroupNodes: TreeNodeItem[] = adminSubgroups.map(sg => ({ const subgroupNodes: TreeNodeItem[] = adminSubgroups.map(sg => ({
id: sg.id, id: sg.id,
label: t(sg.title), label: sg.title,
children: sg.items.map(i => navigationItemToTreeNode(i, t)), children: sg.items.map(i => navigationItemToTreeNode(i, t)),
defaultExpanded: false, defaultExpanded: false,
})); }));

View file

@ -87,9 +87,9 @@ const _PROVIDER_LABEL_KEYS: Record<string, string> = {
internal: 'Internal', internal: 'Internal',
}; };
function _providerLabel(provider: string, t: (key: string) => string): string { function _providerLabel(provider: string): string {
const key = _PROVIDER_LABEL_KEYS[provider]; const key = _PROVIDER_LABEL_KEYS[provider];
return key ? t(key) : provider; return key ? key : provider;
} }
const PROVIDER_ICONS: Record<string, string> = { const PROVIDER_ICONS: Record<string, string> = {
@ -135,7 +135,7 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({ value,
const providerOptions = useMemo(() => { const providerOptions = useMemo(() => {
return allowedProviders.map((provider) => ({ return allowedProviders.map((provider) => ({
value: provider, value: provider,
label: `${PROVIDER_ICONS[provider] || '🔌'} ${_providerLabel(provider, t)}`, label: `${PROVIDER_ICONS[provider] || '🔌'} ${_providerLabel(provider)}`,
})); }));
}, [allowedProviders, t]); }, [allowedProviders, t]);
@ -325,7 +325,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
/> />
<span className={styles.icon}>{PROVIDER_ICONS[provider] || '🔌'}</span> <span className={styles.icon}>{PROVIDER_ICONS[provider] || '🔌'}</span>
<span className={styles.providerName}> <span className={styles.providerName}>
{_providerLabel(provider, t)} {_providerLabel(provider)}
</span> </span>
</label> </label>
))} ))}
@ -361,7 +361,7 @@ export const ProviderBadges: React.FC<ProviderBadgesProps> = ({
<div className={`${styles.providerBadges} ${className || ''}`}> <div className={`${styles.providerBadges} ${className || ''}`}>
{providers.map((provider) => ( {providers.map((provider) => (
<span key={provider} className={styles.badge}> <span key={provider} className={styles.badge}>
{PROVIDER_ICONS[provider] || '🔌'} {_providerLabel(provider, t)} {PROVIDER_ICONS[provider] || '🔌'} {_providerLabel(provider)}
</span> </span>
))} ))}
</div> </div>

View file

@ -144,21 +144,8 @@ const CreateButton: React.FC<CreateButtonProps> = ({
const isDisabled = disabled || loading || isCreating; const isDisabled = disabled || loading || isCreating;
// Resolve language text for popup title const resolvedPopupTitle = popupTitle;
const resolvedPopupTitle = typeof popupTitle === 'string' const resolvedAttributes = attributes;
? t(popupTitle, popupTitle)
: popupTitle;
// Resolve language text for attributes
const resolvedAttributes: AttributeDefinition[] = useMemo(() => {
return attributes.map(attr => ({
...attr,
label: typeof attr.label === 'string' ? t(attr.label, attr.label) : attr.label,
placeholder: attr.placeholder
? (typeof attr.placeholder === 'string' ? t(attr.placeholder, attr.placeholder) : attr.placeholder)
: undefined
}));
}, [attributes, t]);
return ( return (
<> <>

View file

@ -23,13 +23,13 @@ const getStatusBadgeClass = (status?: string | null): string => {
const Log: React.FC<LogProps> = ({ const Log: React.FC<LogProps> = ({
className = '', className = '',
emptyMessage = 'Keine Log-Informationen verfügbar', emptyMessage,
dashboardTree, dashboardTree,
onToggleOperationExpanded, onToggleOperationExpanded,
getChildOperations getChildOperations
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const resolvedEmptyMessage = typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage; const resolvedEmptyMessage = emptyMessage || t('Keine Log-Informationen verfügbar');
const formatLogTimestamp = (timestamp: number): string => { const formatLogTimestamp = (timestamp: number): string => {
try { try {
const formatted = formatUnixTimestamp(timestamp, undefined, { const formatted = formatUnixTimestamp(timestamp, undefined, {

View file

@ -48,7 +48,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
onParcelClick, onParcelClick,
height = '600px', height = '600px',
className = '', className = '',
emptyMessage = 'Klicken Sie auf die Karte, um einen Standort auszuwählen', emptyMessage,
showWfsParcels = false, showWfsParcels = false,
parcelsApiBaseUrl = '' parcelsApiBaseUrl = ''
}) => { }) => {
@ -361,7 +361,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
<div ref={mapContainerRef} style={{ width: '100%', height: '100%' }} /> <div ref={mapContainerRef} style={{ width: '100%', height: '100%' }} />
{parcels.length === 0 && !center && ( {parcels.length === 0 && !center && (
<div className={styles.emptyStateOverlay}> <div className={styles.emptyStateOverlay}>
<p>{typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage}</p> <p>{emptyMessage || t('Klicken Sie auf die Karte, um einen Standort auszuwählen')}</p>
</div> </div>
)} )}
{showWfsParcels && isWfsLoading && ( {showWfsParcels && isWfsLoading && (

View file

@ -16,7 +16,7 @@ const Messages: React.FC<MessagesProps> = ({
showProgress = true, showProgress = true,
renderMessage, renderMessage,
renderDocument, renderDocument,
emptyMessage = 'Noch keine Nachrichten', emptyMessage,
onFileDelete, onFileDelete,
onFileRemove, onFileRemove,
onFileView, onFileView,
@ -30,7 +30,7 @@ const Messages: React.FC<MessagesProps> = ({
deletingMessages deletingMessages
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const resolvedEmptyMessage = typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage; const resolvedEmptyMessage = emptyMessage || t('Noch keine Nachrichten');
if (!messages || messages.length === 0) { if (!messages || messages.length === 0) {
return ( return (
<div className={`${styles.messagesContainer} ${styles.emptyContainer} ${className}`}> <div className={`${styles.messagesContainer} ${styles.emptyContainer} ${className}`}>

View file

@ -30,11 +30,14 @@ interface UnifiedDataBarProps {
className?: string; className?: string;
} }
const _TAB_KEYS: Record<UdbTab, string> = { function _tabLabel(tab: UdbTab, t: (k: string) => string): string {
chats: 'Chatverläufe', switch (tab) {
files: 'Dateien', case 'chats': return t('Chatverläufe');
sources: 'Quellen', case 'files': return t('Dateien');
}; case 'sources': return t('Quellen');
default: return tab;
}
}
const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({ const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({
context, context,
@ -72,7 +75,7 @@ const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({
className={`${styles.tab} ${currentTab === tab ? styles.tabActive : ''}`} className={`${styles.tab} ${currentTab === tab ? styles.tabActive : ''}`}
onClick={() => _handleTabChange(tab)} onClick={() => _handleTabChange(tab)}
> >
{t(_TAB_KEYS[tab])} {_tabLabel(tab, t)}
</button> </button>
))} ))}
</div> </div>

View file

@ -82,23 +82,10 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
} }
// Helper function to resolve node name // Helper function to resolve node name
const resolveNodeName = (pathSegment: string, fullPath: string, page?: GenericPageData): string => { const resolveNodeName = (pathSegment: string, _fullPath: string, page?: GenericPageData): string => {
if (page) { if (page) {
return resolveLanguageText(page.name, t); return resolveLanguageText(page.name, t);
} }
// Try translation key (e.g., "start.real-estate.title")
const translationKey = `${fullPath}.title`;
const translated = t(translationKey);
if (translated !== translationKey) {
return translated;
}
// Try just the segment (e.g., "real-estate.title")
const segmentKey = `${pathSegment}.title`;
const segmentTranslated = t(segmentKey);
if (segmentTranslated !== segmentKey) {
return segmentTranslated;
}
// Fallback to capitalized segment
return pathSegment.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' '); return pathSegment.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');
}; };

View file

@ -82,11 +82,11 @@ export const FeatureLayout: React.FC = () => {
? navFeature.instances.find(i => i.id === instanceId) ? navFeature.instances.find(i => i.id === instanceId)
: undefined; : undefined;
return { return {
mandate: t(navMandate.uiLabel), mandate: navMandate.uiLabel,
feature: navFeature ? t(navFeature.uiLabel) : undefined, feature: navFeature ? navFeature.uiLabel : undefined,
instance: navInstance ? t(navInstance.uiLabel) : undefined, instance: navInstance ? navInstance.uiLabel : undefined,
}; };
}, [dynamicBlock, mandateId, featureCode, instanceId, t]); }, [dynamicBlock, mandateId, featureCode, instanceId]);
// Warten bis Features geladen sind // Warten bis Features geladen sind
if (!initialized || loading || isLoading) { if (!initialized || loading || isLoading) {

View file

@ -171,7 +171,7 @@ export const AutomationsDashboardPage: React.FC = () => {
filterable: true, filterable: true,
formatter: (v: string) => ( formatter: (v: string) => (
<span style={{ color: _STATUS_COLORS[v] || 'inherit', fontWeight: 600 }}> <span style={{ color: _STATUS_COLORS[v] || 'inherit', fontWeight: 600 }}>
{t(v === 'completed' ? 'Abgeschlossen' : v === 'failed' ? 'Fehlgeschlagen' : v === 'running' ? 'Laufend' : v)} {v === 'completed' ? t('Abgeschlossen') : v === 'failed' ? t('Fehlgeschlagen') : v === 'running' ? t('Laufend') : v}
</span> </span>
), ),
}, },

View file

@ -29,11 +29,9 @@ const STORE_FEATURE_DESCRIPTION_FALLBACK: Record<string, string> = {
commcoach: 'CommCoach: Kommunikation trainieren mit KI-gestütztem Coaching und Feedback.', commcoach: 'CommCoach: Kommunikation trainieren mit KI-gestütztem Coaching und Feedback.',
}; };
function _storeCardDescription(feature: StoreFeature, t: (key: string, fallback?: string) => string): string { function _storeCardDescription(feature: StoreFeature): string {
const raw = return (feature.description && feature.description.trim()) ||
(feature.description && feature.description.trim()) || STORE_FEATURE_DESCRIPTION_FALLBACK[feature.featureCode] || '';
STORE_FEATURE_DESCRIPTION_FALLBACK[feature.featureCode];
return raw ? t(raw) : '';
} }
interface FeatureCardProps { interface FeatureCardProps {
@ -62,13 +60,13 @@ const FeatureCard: React.FC<FeatureCardProps> = ({
<div className={styles.cardHeader}> <div className={styles.cardHeader}>
{icon && <span className={styles.cardIcon}>{icon}</span>} {icon && <span className={styles.cardIcon}>{icon}</span>}
<h3 className={styles.cardTitle}> <h3 className={styles.cardTitle}>
{t(feature.label)} {feature.label}
</h3> </h3>
</div> </div>
<div className={styles.cardBody}> <div className={styles.cardBody}>
<p className={styles.cardDescription}> <p className={styles.cardDescription}>
{_storeCardDescription(feature, t)} {_storeCardDescription(feature)}
</p> </p>
</div> </div>

View file

@ -29,8 +29,8 @@ function getMandateName(mandate: Mandate): string {
return mandate.label || mandate.name || mandate.id; return mandate.label || mandate.name || mandate.id;
} }
function getFeatureLabel(feature: Feature, t: (k: string) => string): string { function getFeatureLabel(feature: Feature): string {
return t(feature.label || feature.code); return feature.label || feature.code;
} }
export interface InstanceWithStats extends FeatureInstance { export interface InstanceWithStats extends FeatureInstance {
@ -165,7 +165,7 @@ export const AccessManagementHub: React.FC = () => {
instance, instance,
mandateId: mandateId || '', mandateId: mandateId || '',
mandateName: mandate ? getMandateName(mandate) : mandateId || '', mandateName: mandate ? getMandateName(mandate) : mandateId || '',
featureLabel: feature ? getFeatureLabel(feature, t) : instance.featureCode, featureLabel: feature ? getFeatureLabel(feature) : instance.featureCode,
}); });
}; };
@ -294,7 +294,7 @@ export const AccessManagementHub: React.FC = () => {
return { return {
id: inst.id, id: inst.id,
label: inst.label, label: inst.label,
featureLabel: feature ? getFeatureLabel(feature, t) : inst.featureCode, featureLabel: feature ? getFeatureLabel(feature) : inst.featureCode,
userCount: inst.userCount ?? 0, userCount: inst.userCount ?? 0,
}; };
}), }),
@ -363,7 +363,7 @@ export const AccessManagementHub: React.FC = () => {
<option value="">{t('Alle')}</option> <option value="">{t('Alle')}</option>
{features.map((f) => ( {features.map((f) => (
<option key={f.code} value={f.code}> <option key={f.code} value={f.code}>
{getFeatureLabel(f, t)} {getFeatureLabel(f)}
</option> </option>
))} ))}
</select> </select>
@ -424,7 +424,7 @@ export const AccessManagementHub: React.FC = () => {
instancesByMandate={instancesByMandate} instancesByMandate={instancesByMandate}
instanceUsersMap={instanceUsersMap} instanceUsersMap={instanceUsersMap}
features={features} features={features}
getFeatureLabel={(f) => getFeatureLabel(f, t)} getFeatureLabel={getFeatureLabel}
loading={hierarchyUsersLoading} loading={hierarchyUsersLoading}
onOpenDetail={handleOpenDetail} onOpenDetail={handleOpenDetail}
/> />
@ -526,7 +526,7 @@ export const AccessManagementHub: React.FC = () => {
</span> </span>
</div> </div>
<div className={hubStyles.instanceMeta}> <div className={hubStyles.instanceMeta}>
<span>{getFeatureLabel(features.find((f) => f.code === inst.featureCode) || { code: inst.featureCode, label: inst.featureCode }, t)}</span> <span>{getFeatureLabel(features.find((f) => f.code === inst.featureCode) || { code: inst.featureCode, label: inst.featureCode })}</span>
<span> <span>
{inst.userCount ?? '—'} {t('Benutzer')} {inst.userCount ?? '—'} {t('Benutzer')}
</span> </span>

View file

@ -92,10 +92,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
{ key: 'featureCode', label: t('Feature'), type: 'string' as const, sortable: true, filterable: true, width: 150, { key: 'featureCode', label: t('Feature'), type: 'string' as const, sortable: true, filterable: true, width: 150,
render: (value: string) => { render: (value: string) => {
const feature = features.find(f => f.code === value); const feature = features.find(f => f.code === value);
if (feature) { return feature ? (feature.label || value) : value;
return t(feature.label || value);
}
return value;
} }
}, },
{ key: 'enabled', label: t('Aktiv'), type: 'boolean' as const, sortable: true, filterable: true, width: 80 }, { key: 'enabled', label: t('Aktiv'), type: 'boolean' as const, sortable: true, filterable: true, width: 80 },
@ -323,10 +320,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
// Get feature label // Get feature label
const getFeatureLabel = (code: string) => { const getFeatureLabel = (code: string) => {
const feature = features.find(f => f.code === code); const feature = features.find(f => f.code === code);
if (feature) { return feature ? (feature.label || code) : code;
return t(feature.label || code);
}
return code;
}; };
if (error && !selectedMandateId) { if (error && !selectedMandateId) {
@ -514,7 +508,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
<DropdownSelect <DropdownSelect
items={features.map(f => ({ items={features.map(f => ({
id: f.code, id: f.code,
label: t(f.label || f.code), label: f.label || f.code,
value: f.code value: f.code
}))} }))}
selectedItemId={createFeatureCode} selectedItemId={createFeatureCode}

View file

@ -367,10 +367,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
// Get feature label // Get feature label
const getFeatureLabel = (code: string) => { const getFeatureLabel = (code: string) => {
const feature = features.find(f => f.code === code); const feature = features.find(f => f.code === code);
if (feature) { return feature ? (feature.label || code) : code;
return t(feature.label || code);
}
return code;
}; };
// Get selected instance info from combined options // Get selected instance info from combined options

View file

@ -260,10 +260,8 @@ export const AdminFeatureRolesPage: React.FC = () => {
setEditingRole(role); setEditingRole(role);
}; };
// Get feature name - Backend uses 'label' field (German i18n key or legacy multilingual)
const getFeatureName = (feature: Feature) => { const getFeatureName = (feature: Feature) => {
const raw = getTextValue(feature.label || feature.name); return feature.label || feature.name || '-';
return raw === '-' ? '-' : t(raw);
}; };
if (error && !selectedFeatureCode) { if (error && !selectedFeatureCode) {

View file

@ -109,10 +109,7 @@ export const AdminMandateWizardPage: React.FC = () => {
const getFeatureLabel = (code: string): string => { const getFeatureLabel = (code: string): string => {
const f = features.find(feat => feat.code === code); const f = features.find(feat => feat.code === code);
if (f) { return f ? (f.label || code) : code;
return t(f.label || code);
}
return code;
}; };
const getUserDisplayName = (u: { fullName?: string; firstname?: string | null; lastname?: string | null; username: string }): string => { const getUserDisplayName = (u: { fullName?: string; firstname?: string | null; lastname?: string | null; username: string }): string => {

View file

@ -57,8 +57,8 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
const [selectedUserRoles, setSelectedUserRoles] = useState<Array<{ userId: string; roleIds: string[] }>>([]); const [selectedUserRoles, setSelectedUserRoles] = useState<Array<{ userId: string; roleIds: string[] }>>([]);
const featureOptions = useMemo( const featureOptions = useMemo(
() => features.map((f) => ({ value: f.code, label: t(f.label || f.code) })), () => features.map((f) => ({ value: f.code, label: f.label || f.code })),
[features, t] [features]
); );
const mandateOptions = useMemo( const mandateOptions = useMemo(
() => mandates.map((m) => ({ value: m.id, label: getMandateName(m) })), () => mandates.map((m) => ({ value: m.id, label: getMandateName(m) })),

View file

@ -858,7 +858,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
{_groupScoresByDimension(coach.scores).map(group => ( {_groupScoresByDimension(coach.scores).map(group => (
<div key={group.dimension} className={styles.scoreGroup}> <div key={group.dimension} className={styles.scoreGroup}>
<div className={styles.scoreDimension}> <div className={styles.scoreDimension}>
<span className={styles.scoreDimensionLabel}>{t(_dimensionLabel(group.dimension))}</span> <span className={styles.scoreDimensionLabel}>{_dimensionLabel(group.dimension, t)}</span>
<span className={styles.scoreLatest}>{Math.round(group.latest.score)}/100</span> <span className={styles.scoreLatest}>{Math.round(group.latest.score)}/100</span>
<span className={`${styles.scoreTrend} ${styles[`trend_${group.latest.trend}`]}`}> <span className={`${styles.scoreTrend} ${styles[`trend_${group.latest.trend}`]}`}>
{group.latest.trend === 'improving' ? 'steigend' : group.latest.trend === 'declining' ? 'sinkend' : 'stabil'} {group.latest.trend === 'improving' ? 'steigend' : group.latest.trend === 'declining' ? 'sinkend' : 'stabil'}
@ -926,13 +926,15 @@ function _groupScoresByDimension(scores: any[]): ScoreGroup[] {
return Object.values(groups); return Object.values(groups);
} }
function _dimensionLabel(dim: string): string { function _dimensionLabel(dim: string, t: (k: string) => string): string {
const labels: Record<string, string> = { switch (dim) {
empathy: 'Einfühlungsvermögen', clarity: 'Klarheit', case 'empathy': return t('Einfühlungsvermögen');
assertiveness: 'Durchsetzung', listening: 'Zuhören', case 'clarity': return t('Klarheit');
selfReflection: 'Selbstreflexion', case 'assertiveness': return t('Durchsetzung');
}; case 'listening': return t('Zuhören');
return labels[dim] || dim; case 'selfReflection': return t('Selbstreflexion');
default: return dim;
}
} }
function _formatToolPayload(payload: Record<string, unknown>): string { function _formatToolPayload(payload: Record<string, unknown>): string {

View file

@ -30,15 +30,18 @@ import styles from './Automation2WorkflowsTasks.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext'; import { useLanguage } from '../../../providers/language/LanguageContext';
const NODE_TYPE_LABELS: Record<string, string> = { function _nodeTypeLabel(nodeType: string, t: (k: string) => string): string {
'input.form': 'Formular', switch (nodeType) {
'input.approval': 'Genehmigung', case 'input.form': return t('Formular');
'input.upload': 'Upload', case 'input.approval': return t('Genehmigung');
'input.comment': 'Kommentar', case 'input.upload': return t('Upload');
'input.review': 'Prüfung', case 'input.comment': return t('Kommentar');
'input.selection': 'Auswahl', case 'input.review': return t('Prüfung');
'input.confirmation': 'Bestätigung', case 'input.selection': return t('Auswahl');
}; case 'input.confirmation': return t('Bestätigung');
default: return nodeType;
}
}
function formatTimestamp(ts?: number): string { function formatTimestamp(ts?: number): string {
if (ts == null || ts <= 0) return '—'; if (ts == null || ts <= 0) return '—';
@ -941,7 +944,7 @@ const TaskCard: React.FC<TaskCardProps> = ({
<div className={styles.taskMetaRow}> <div className={styles.taskMetaRow}>
<span className={styles.metaLabel}>{t('Typ')}</span> <span className={styles.metaLabel}>{t('Typ')}</span>
<span className={styles.metaValue}> <span className={styles.metaValue}>
{t(NODE_TYPE_LABELS[nodeType] ?? nodeType)} {_nodeTypeLabel(nodeType, t)}
</span> </span>
</div> </div>

View file

@ -30,14 +30,19 @@ const _TABS: TabDef[] = [
{ id: 'year-end', templateTag: 'template:trustee-year-end-check', icon: '\u2705', color: '#795548' }, { id: 'year-end', templateTag: 'template:trustee-year-end-check', icon: '\u2705', color: '#795548' },
]; ];
const _TAB_LABEL_KEYS: Record<string, string> = { function _tabLabel(tabId: string, t: (k: string) => string): string {
'year-end': 'Jahresabschluss prüfen', switch (tabId) {
}; case 'year-end': return t('Jahresabschluss prüfen');
default: return tabId;
}
}
const _TAB_DESCRIPTION_KEYS: Record<string, string> = { function _tabDescription(tabId: string, t: (k: string) => string): string {
'year-end': switch (tabId) {
'Automatische Prüfungen für den Jahresabschluss: Saldovalidierung, Vorjahresvergleich, gesetzliche Checks.', case 'year-end': return t('Automatische Prüfungen für den Jahresabschluss: Saldovalidierung, Vorjahresvergleich, gesetzliche Checks.');
}; default: return '';
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types // Types
@ -225,7 +230,7 @@ export const TrusteeAbschlussView: React.FC = () => {
}} }}
> >
<span style={{ marginRight: '0.375rem' }}>{tab.icon}</span> <span style={{ marginRight: '0.375rem' }}>{tab.icon}</span>
{t(_TAB_LABEL_KEYS[tab.id] || tab.id)} {_tabLabel(tab.id, t)}
</button> </button>
))} ))}
</div> </div>
@ -234,7 +239,7 @@ export const TrusteeAbschlussView: React.FC = () => {
{/* Tab content */} {/* Tab content */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<p className={styles.sectionDescription}> <p className={styles.sectionDescription}>
{_TAB_DESCRIPTION_KEYS[activeTab] ? t(_TAB_DESCRIPTION_KEYS[activeTab]) : ''} {_tabDescription(activeTab, t)}
</p> </p>
{workflowsLoading ? ( {workflowsLoading ? (

View file

@ -33,19 +33,25 @@ const _TABS: TabDef[] = [
{ id: 'forecast', templateTag: 'template:trustee-forecast', icon: '\uD83D\uDCC8', color: '#E91E63' }, { id: 'forecast', templateTag: 'template:trustee-forecast', icon: '\uD83D\uDCC8', color: '#E91E63' },
]; ];
const _TAB_LABEL_KEYS: Record<string, string> = { function _tabLabel(tabId: string, t: (k: string) => string): string {
budget: 'Budget-Vergleich', switch (tabId) {
kpi: 'KPI-Dashboard', case 'budget': return t('Budget-Vergleich');
cashflow: 'Cashflow-Rechnung', case 'kpi': return t('KPI-Dashboard');
forecast: 'Prognose', case 'cashflow': return t('Cashflow-Rechnung');
}; case 'forecast': return t('Prognose');
default: return tabId;
}
}
const _TAB_DESCRIPTION_KEYS: Record<string, string> = { function _tabDescription(tabId: string, t: (k: string) => string): string {
budget: 'Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel', switch (tabId) {
kpi: 'Kennzahlen berechnen und visualisieren', case 'budget': return t('Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel');
cashflow: 'Cashflow berechnen und analysieren', case 'kpi': return t('Kennzahlen berechnen und visualisieren');
forecast: 'Trend-Analyse und Prognose der nächsten Monate', case 'cashflow': return t('Cashflow berechnen und analysieren');
}; case 'forecast': return t('Trend-Analyse und Prognose der nächsten Monate');
default: return '';
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types // Types
@ -240,7 +246,7 @@ export const TrusteeAnalyseView: React.FC = () => {
}} }}
> >
<span style={{ marginRight: '0.375rem' }}>{tab.icon}</span> <span style={{ marginRight: '0.375rem' }}>{tab.icon}</span>
{t(_TAB_LABEL_KEYS[tab.id] || tab.id)} {_tabLabel(tab.id, t)}
</button> </button>
))} ))}
</div> </div>
@ -248,7 +254,7 @@ export const TrusteeAnalyseView: React.FC = () => {
{/* Tab content */} {/* Tab content */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<p className={styles.sectionDescription}> <p className={styles.sectionDescription}>
{_TAB_DESCRIPTION_KEYS[activeTab] ? t(_TAB_DESCRIPTION_KEYS[activeTab]) : ''} {_tabDescription(activeTab, t)}
</p> </p>
{workflowsLoading ? ( {workflowsLoading ? (

View file

@ -7,7 +7,7 @@
* - Full details shown on error for debugging * - Full details shown on error for debugging
*/ */
import React, { useState, useCallback } from 'react'; import React, { useState} from 'react';
import type { ToolActivity } from './useWorkspace'; import type { ToolActivity } from './useWorkspace';
import { useLanguage } from '../../../providers/language/LanguageContext'; import { useLanguage } from '../../../providers/language/LanguageContext';
@ -124,7 +124,7 @@ export const ToolActivityLog: React.FC<ToolActivityLogProps> = ({ activities })
const { t } = useLanguage(); const { t } = useLanguage();
const [expandedId, setExpandedId] = useState<string | null>(null); const [expandedId, setExpandedId] = useState<string | null>(null);
const translate = useCallback<TranslateFn>((key, params) => t(key, params), [t]); const translate: TranslateFn = t;
if (!activities.length) { if (!activities.length) {
return ( return (

View file

@ -25,16 +25,19 @@ import { formatBinaryDataSizeBytes } from '../../../utils/formatDataSize';
import { useLanguage } from '../../../providers/language/LanguageContext'; import { useLanguage } from '../../../providers/language/LanguageContext';
const MIME_LABELS: Record<string, string> = { function _mimeLabel(key: string, t: (k: string) => string): string {
pdf: 'PDF', switch (key) {
office_doc: 'Office (Text)', case 'pdf': return t('PDF');
office_sheet: 'Office (Tabellen)', case 'office_doc': return t('Office (Text)');
office_slides: 'Office (Folien)', case 'office_sheet': return t('Office (Tabellen)');
text: 'Text', case 'office_slides': return t('Office (Folien)');
image: 'Bild', case 'text': return t('Text');
html: 'HTML', case 'image': return t('Bild');
other: 'Sonstige', case 'html': return t('HTML');
}; case 'other': return t('Sonstige');
default: return key;
}
}
const CHART_COLORS = ['#1976d2', '#00897b', '#6a1b9a', '#e65100', '#5d4037', '#455a64', '#c62828']; const CHART_COLORS = ['#1976d2', '#00897b', '#6a1b9a', '#e65100', '#5d4037', '#455a64', '#c62828'];
@ -118,7 +121,7 @@ export const WorkspaceRagInsightsPage: React.FC = () => {
const kpis = stats?.kpis; const kpis = stats?.kpis;
const timeline = stats?.timelineIndexedDocuments ?? []; const timeline = stats?.timelineIndexedDocuments ?? [];
const mimeRows = Object.entries(stats?.documentsByMimeCategory ?? {}).map(([key, value]) => ({ const mimeRows = Object.entries(stats?.documentsByMimeCategory ?? {}).map(([key, value]) => ({
name: t(MIME_LABELS[key] ?? key), name: _mimeLabel(key, t),
value, value,
})); }));
const statusRows = Object.entries(stats?.indexedDocumentsByStatus ?? {}).map(([name, value]) => ({ const statusRows = Object.entries(stats?.indexedDocumentsByStatus ?? {}).map(([name, value]) => ({

View file

@ -10,7 +10,7 @@ type TranslateParams = Record<string, string | number | boolean | null | undefin
interface LanguageContextType { interface LanguageContextType {
currentLanguage: Language; currentLanguage: Language;
setLanguage: (language: Language) => void; setLanguage: (language: Language) => void;
t: (key: string, paramsOrFallback?: TranslateParams | string) => string; t: (key: string, params?: TranslateParams) => string;
isLoading: boolean; isLoading: boolean;
reloadLanguage: () => Promise<void>; reloadLanguage: () => Promise<void>;
availableLanguages: I18nCodeInfo[]; availableLanguages: I18nCodeInfo[];
@ -123,18 +123,8 @@ export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children })
return out; return out;
}; };
const t = (key: string, paramsOrFallback?: TranslateParams | string): string => { const t = (key: string, params?: TranslateParams): string => {
let params: TranslateParams | undefined; const resolved = translations[key] ?? `[${key}]`;
if (typeof paramsOrFallback === 'string') {
params = undefined;
} else {
params = paramsOrFallback;
}
const resolved =
translations[key] ??
(typeof paramsOrFallback === 'string' ? paramsOrFallback : undefined) ??
`[${key}]`;
return _applyParams(resolved, params); return _applyParams(resolved, params);
}; };