diff --git a/src/api/featuresApi.ts b/src/api/featuresApi.ts index 0e29e09..779dc6c 100644 --- a/src/api/featuresApi.ts +++ b/src/api/featuresApi.ts @@ -76,7 +76,7 @@ const MOCK_RESPONSE: FeaturesMyResponse = { features: [ { code: 'trustee', - label: { de: 'Treuhand', en: 'Trustee' }, + label: 'Treuhand', icon: 'briefcase', instances: [ { @@ -101,7 +101,7 @@ const MOCK_RESPONSE: FeaturesMyResponse = { }, { code: 'chatworkflow', - label: { de: 'Workflow', en: 'Workflow' }, + label: 'Workflow', icon: 'play_circle', instances: [ { @@ -124,7 +124,7 @@ const MOCK_RESPONSE: FeaturesMyResponse = { features: [ { code: 'trustee', - label: { de: 'Treuhand', en: 'Trustee' }, + label: 'Treuhand', icon: 'briefcase', instances: [ { @@ -234,9 +234,9 @@ export async function fetchMyFeatures(): Promise { export async function fetchAvailableFeatures(): Promise { if (USE_MOCK) { return [ - { code: 'trustee', label: { de: 'Treuhand', en: 'Trustee' }, icon: 'briefcase', instances: [] }, - { code: 'chatworkflow', label: { de: 'Workflow', en: 'Workflow' }, icon: 'play_circle', instances: [] }, - { code: 'chatbot', label: { de: 'Chatbot', en: 'Chatbot' }, icon: 'chat', instances: [] }, + { code: 'trustee', label: 'Treuhand', icon: 'briefcase', instances: [] }, + { code: 'chatworkflow', label: 'Workflow', icon: 'play_circle', instances: [] }, + { code: 'chatbot', label: 'Chatbot', icon: 'chat', instances: [] }, ]; } diff --git a/src/api/storeApi.ts b/src/api/storeApi.ts index 78b0768..131596d 100644 --- a/src/api/storeApi.ts +++ b/src/api/storeApi.ts @@ -17,9 +17,9 @@ export interface StoreFeatureInstance { export interface StoreFeature { featureCode: string; - label: Record; + label: string; icon: string; - description: Record; + description: string; instances: StoreFeatureInstance[]; canActivate: boolean; } @@ -49,7 +49,9 @@ export interface SubscriptionInfo { status: string | null; maxDataVolumeMB: number | null; maxFeatureInstances: number | null; + includedModules: number; budgetAiCHF: number | null; + budgetAiPerUserCHF: number | null; currentFeatureInstances: number; trialEndsAt: string | null; } diff --git a/src/api/subscriptionApi.ts b/src/api/subscriptionApi.ts index b476433..6096257 100644 --- a/src/api/subscriptionApi.ts +++ b/src/api/subscriptionApi.ts @@ -19,8 +19,10 @@ export interface SubscriptionPlan { autoRenew: boolean; maxUsers: number | null; maxFeatureInstances: number | null; + includedModules: number; maxDataVolumeMB?: number | null; budgetAiCHF?: number; + budgetAiPerUserCHF?: number; trialDays: number | null; successorPlanKey: string | null; } diff --git a/src/components/UnifiedDataBar/UnifiedDataBar.tsx b/src/components/UnifiedDataBar/UnifiedDataBar.tsx index 0517b55..4e635d4 100644 --- a/src/components/UnifiedDataBar/UnifiedDataBar.tsx +++ b/src/components/UnifiedDataBar/UnifiedDataBar.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import ChatsTab from './ChatsTab'; import FilesTab from './FilesTab'; import SourcesTab from './SourcesTab'; +import { useLanguage } from '../../providers/language/LanguageContext'; import styles from './UnifiedDataBar.module.css'; export type UdbTab = 'chats' | 'files' | 'sources'; @@ -29,10 +30,10 @@ interface UnifiedDataBarProps { className?: string; } -const _TAB_LABELS: Record> = { - chats: { de: 'Chats', en: 'Chats', fr: 'Chats' }, - files: { de: 'Dateien', en: 'Files', fr: 'Fichiers' }, - sources: { de: 'Quellen', en: 'Sources', fr: 'Sources' }, +const _TAB_KEYS: Record = { + chats: 'Chats', + files: 'Dateien', + sources: 'Quellen', }; const UnifiedDataBar: React.FC = ({ @@ -50,8 +51,9 @@ const UnifiedDataBar: React.FC = ({ onSourcesChanged, className, }) => { + const { t } = useLanguage(); const visibleTabs = (['chats', 'files', 'sources'] as UdbTab[]).filter( - t => !hideTabs?.includes(t), + (ubTab) => !hideTabs?.includes(ubTab), ); const [internalTab, setInternalTab] = useState(controlledTab ?? visibleTabs[0] ?? 'chats'); const currentTab = controlledTab ?? internalTab; @@ -70,7 +72,7 @@ const UnifiedDataBar: React.FC = ({ className={`${styles.tab} ${currentTab === tab ? styles.tabActive : ''}`} onClick={() => _handleTabChange(tab)} > - {_TAB_LABELS[tab].de} + {t(_TAB_KEYS[tab])} ))} diff --git a/src/pages/Store.tsx b/src/pages/Store.tsx index 0a82230..549a48d 100644 --- a/src/pages/Store.tsx +++ b/src/pages/Store.tsx @@ -20,47 +20,24 @@ const FEATURE_ICONS: Record = { commcoach: , }; -const FEATURE_DESCRIPTIONS: Record> = { - automation: { - de: 'Erstelle und verwalte Automatisierungen, um wiederkehrende Aufgaben effizient zu erledigen.', - en: 'Create and manage automations to handle recurring tasks efficiently.', - fr: 'Creer et gerer des automatisations pour traiter efficacement les taches recurrentes.', - }, - graphicalEditor: { - de: 'n8n-style Flow-Automatisierung mit grafischem Editor, RAG und Tools.', - en: 'n8n-style flow automation with visual editor, RAG and tools.', - fr: 'Automatisation de flux style n8n avec editeur visuel, RAG et outils.', - }, - teamsbot: { - de: 'Integriere einen AI-Bot in deine Microsoft Teams Meetings und Channels.', - en: 'Integrate an AI bot into your Microsoft Teams meetings and channels.', - fr: 'Integrez un bot IA dans vos reunions et canaux Microsoft Teams.', - }, - workspace: { - de: 'Nutze den gemeinsamen AI Workspace: Chats, Tools und Kontext pro Instanz.', - en: 'Use the shared AI workspace: chats, tools, and context per instance.', - fr: 'Utilisez l\'espace de travail IA partage: chats, outils et contexte par instance.', - }, - commcoach: { - de: 'CommCoach: Kommunikation trainieren mit KI-gestütztem Coaching und Feedback.', - en: 'CommCoach: practice communication with AI-assisted coaching and feedback.', - fr: 'CommCoach: entrainer la communication avec un coaching assiste par IA.', - }, +/** Fallback when GET /store/features omits description (German i18n keys). */ +const STORE_FEATURE_DESCRIPTION_FALLBACK: Record = { + automation: 'Erstelle und verwalte Automatisierungen, um wiederkehrende Aufgaben effizient zu erledigen.', + graphicalEditor: 'n8n-style Flow-Automatisierung mit grafischem Editor, RAG und Tools.', + teamsbot: 'Integriere einen AI-Bot in deine Microsoft Teams Meetings und Channels.', + workspace: 'Nutze den gemeinsamen AI Workspace: Chats, Tools und Kontext pro Instanz.', + commcoach: 'CommCoach: Kommunikation trainieren mit KI-gestütztem Coaching und Feedback.', }; -function _getLabel(labels: Record, lang: string): string { - return labels[lang] || labels['en'] || labels['de'] || Object.values(labels)[0] || ''; -} - -function _getDescription(featureCode: string, lang: string): string { - const desc = FEATURE_DESCRIPTIONS[featureCode]; - if (!desc) return ''; - return desc[lang] || desc['en'] || desc['de'] || ''; +function _storeCardDescription(feature: StoreFeature, t: (key: string, fallback?: string) => string): string { + const raw = + (feature.description && feature.description.trim()) || + STORE_FEATURE_DESCRIPTION_FALLBACK[feature.featureCode]; + return raw ? t(raw) : ''; } interface FeatureCardProps { feature: StoreFeature; - language: string; mandates: UserMandate[]; actionLoading: string | null; onActivate: (code: string, mandateId?: string) => void; @@ -69,7 +46,6 @@ interface FeatureCardProps { const FeatureCard: React.FC = ({ feature, - language, mandates, actionLoading, onActivate, @@ -86,13 +62,13 @@ const FeatureCard: React.FC = ({
{icon && {icon}}

- {_getLabel(feature.label, language)} + {t(feature.label)}

- {_getDescription(feature.featureCode, language)} + {_storeCardDescription(feature, t)}

@@ -111,9 +87,7 @@ const FeatureCard: React.FC = ({ onClick={() => onDeactivate(feature.featureCode, inst.mandateId, inst.instanceId)} disabled={isProcessing} > - {isProcessing - ? '...' - : (language === 'de' ? 'Deaktivieren' : language === 'fr' ? 'Desactiver' : 'Deactivate')} + {isProcessing ? '...' : t('Deaktivieren')} ))} @@ -124,7 +98,7 @@ const FeatureCard: React.FC = ({
- {language === 'de' ? 'Verfuegbar' : language === 'fr' ? 'Disponible' : 'Available'} + {t('Verfügbar')}
)} @@ -138,12 +112,8 @@ const FeatureCard: React.FC = ({ disabled={isProcessing} > {isProcessing - ? (language === 'de' ? t('store.wirdAktiviert') : t('store.activating')) - : (language === 'de' - ? `Aktivieren fuer ${m.label || m.name}` - : language === 'fr' - ? `Activer pour ${m.label || m.name}` - : `Activate for ${m.label || m.name}`)} + ? t('store.wirdAktiviert', t('store.activating')) + : t('Aktivieren für {name}', { name: String(m.label || m.name) })} ))} @@ -152,44 +122,48 @@ const FeatureCard: React.FC = ({ }; const StorePage: React.FC = () => { - const { t, currentLanguage } = useLanguage(); + const { t } = useLanguage(); const { features, mandates, subscriptionInfo, loading, actionLoading, error, activate, deactivate } = useStore(); return (
-

{currentLanguage === 'de' ? 'Feature Store' : currentLanguage === 'fr' ? t('store.featureStore') : t('store.featureStore')}

+

{t('Feature Store')}

- {currentLanguage === 'de' - ? 'Aktiviere Features fuer dein Konto. Deine Daten sind isoliert und nur fuer dich sichtbar.' - : currentLanguage === 'fr' - ? t('store.activezDesFonctionnalitesPourVotre') - : t('store.activateFeaturesForYourAccount')} + {t( + 'Aktiviere Features für dein Konto. Deine Daten sind isoliert und nur für dich sichtbar.', + t('store.activateFeaturesForYourAccount') + )}

{subscriptionInfo && subscriptionInfo.plan && (
- Plan: {subscriptionInfo.plan} - {subscriptionInfo.maxFeatureInstances != null && ( - - {currentLanguage === 'de' ? 'Instanzen' : 'Instances'}: {subscriptionInfo.currentFeatureInstances}/{subscriptionInfo.maxFeatureInstances} - - )} + {t('Plan:')} {subscriptionInfo.plan} + + {subscriptionInfo.maxFeatureInstances != null + ? <>{t('Module')}: {subscriptionInfo.currentFeatureInstances}/{subscriptionInfo.maxFeatureInstances} + : <>{subscriptionInfo.currentFeatureInstances} {t('Module aktiv')} + {subscriptionInfo.includedModules != null && subscriptionInfo.includedModules > 0 && ( + <> ({subscriptionInfo.includedModules} {t('inklusive')}) + )} + + } + {subscriptionInfo.maxDataVolumeMB != null && ( - {currentLanguage === 'de' ? 'Speicher' : 'Storage'}:{' '} + {t('Speicher')}:{' '} {formatBinaryDataSizeFromMebibytes(subscriptionInfo.maxDataVolumeMB)} )} - {subscriptionInfo.budgetAiCHF != null && subscriptionInfo.budgetAiCHF > 0 && ( + {subscriptionInfo.budgetAiPerUserCHF != null && subscriptionInfo.budgetAiPerUserCHF > 0 && ( - {currentLanguage === 'de' ? 'AI-Budget' : 'AI budget'}: {subscriptionInfo.budgetAiCHF} CHF + {t('AI-Budget')}: {subscriptionInfo.budgetAiPerUserCHF} CHF / User )} {subscriptionInfo.status === 'TRIALING' && subscriptionInfo.trialEndsAt && ( - {currentLanguage === 'de' ? t('store.trialEndet') : t('store.trialEnds')}: {new Date(subscriptionInfo.trialEndsAt).toLocaleDateString()} + {t('store.trialEndet', t('store.trialEnds'))}: {new Date(subscriptionInfo.trialEndsAt).toLocaleDateString()} )}
@@ -199,13 +173,11 @@ const StorePage: React.FC = () => { {loading ? (
- {currentLanguage === 'de' ? t('store.ladeFeatures') : t('store.loadingFeatures')} + {t('store.ladeFeatures', t('store.loadingFeatures'))}
) : features.length === 0 ? (
- {currentLanguage === 'de' - ? t('store.keineFeaturesImStoreVerfuegbar') - : t('store.noFeaturesAvailableInThe')} + {t('store.keineFeaturesImStoreVerfuegbar', t('store.noFeaturesAvailableInThe'))}
) : (
@@ -213,7 +185,6 @@ const StorePage: React.FC = () => { string): string { + return t(labelAsI18nKey(feature.label, feature.code)); } export interface InstanceWithStats extends FeatureInstance { @@ -168,7 +166,7 @@ export const AccessManagementHub: React.FC = () => { instance, mandateId: mandateId || '', mandateName: mandate ? getMandateName(mandate) : mandateId || '', - featureLabel: feature ? getFeatureLabel(feature) : instance.featureCode, + featureLabel: feature ? getFeatureLabel(feature, t) : instance.featureCode, }); }; @@ -302,7 +300,7 @@ export const AccessManagementHub: React.FC = () => { }; }), }; - }, [selectedMandateId, mandates, filteredInstances, features]); + }, [selectedMandateId, mandates, filteredInstances, features, t]); if (error && !selectedMandateId) { return ( @@ -364,7 +362,7 @@ export const AccessManagementHub: React.FC = () => { {features.map((f) => ( ))} @@ -527,7 +525,7 @@ export const AccessManagementHub: React.FC = () => {
- {getFeatureLabel(features.find((f) => f.code === inst.featureCode) || { code: inst.featureCode, label: inst.featureCode })} + {getFeatureLabel(features.find((f) => f.code === inst.featureCode) || { code: inst.featureCode, label: inst.featureCode }, t)} {inst.userCount ?? '—'} Benutzer {inst.roleCount ?? '—'} Rollen
diff --git a/src/pages/admin/AdminFeatureAccessPage.tsx b/src/pages/admin/AdminFeatureAccessPage.tsx index 23d7ea1..85fb2b7 100644 --- a/src/pages/admin/AdminFeatureAccessPage.tsx +++ b/src/pages/admin/AdminFeatureAccessPage.tsx @@ -20,6 +20,7 @@ import { TextField } from '../../components/UiComponents/TextField'; import styles from './Admin.module.css'; import { useLanguage } from '../../providers/language/LanguageContext'; +import { labelAsI18nKey } from '../../types/mandate'; export const AdminFeatureAccessPage: React.FC = () => { const { t } = useLanguage(); @@ -93,10 +94,7 @@ export const AdminFeatureAccessPage: React.FC = () => { render: (value: string) => { const feature = features.find(f => f.code === value); if (feature) { - const label = typeof feature.label === 'object' - ? (feature.label.de || feature.label.en || value) - : feature.label; - return label; + return t(labelAsI18nKey(feature.label, value)); } return value; } @@ -327,9 +325,7 @@ export const AdminFeatureAccessPage: React.FC = () => { const getFeatureLabel = (code: string) => { const feature = features.find(f => f.code === code); if (feature) { - return typeof feature.label === 'object' - ? (feature.label.de || feature.label.en || code) - : (feature.label || code); + return t(labelAsI18nKey(feature.label, code)); } return code; }; @@ -514,9 +510,7 @@ export const AdminFeatureAccessPage: React.FC = () => { ({ id: f.code, - label: typeof f.label === 'object' - ? (f.label.de || f.label.en || f.code) - : (f.label || f.code), + label: t(labelAsI18nKey(f.label, f.code)), value: f.code }))} selectedItemId={createFeatureCode} diff --git a/src/pages/admin/AdminFeatureInstanceUsersPage.tsx b/src/pages/admin/AdminFeatureInstanceUsersPage.tsx index 6fe73de..ad0d2cf 100644 --- a/src/pages/admin/AdminFeatureInstanceUsersPage.tsx +++ b/src/pages/admin/AdminFeatureInstanceUsersPage.tsx @@ -17,6 +17,7 @@ import api from '../../api'; import styles from './Admin.module.css'; import { useLanguage } from '../../providers/language/LanguageContext'; +import { labelAsI18nKey } from '../../types/mandate'; export const AdminFeatureInstanceUsersPage: React.FC = () => { const { t } = useLanguage(); @@ -368,9 +369,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => { const getFeatureLabel = (code: string) => { const feature = features.find(f => f.code === code); if (feature) { - return typeof feature.label === 'object' - ? (feature.label.de || feature.label.en || code) - : (feature.label || code); + return t(labelAsI18nKey(feature.label, code)); } return code; }; diff --git a/src/pages/admin/AdminFeatureRolesPage.tsx b/src/pages/admin/AdminFeatureRolesPage.tsx index 1ac8c8e..1e0f238 100644 --- a/src/pages/admin/AdminFeatureRolesPage.tsx +++ b/src/pages/admin/AdminFeatureRolesPage.tsx @@ -263,8 +263,11 @@ export const AdminFeatureRolesPage: React.FC = () => { setEditingRole(role); }; - // Get feature name - Backend uses 'label' field - const getFeatureName = (feature: Feature) => getTextValue(feature.label || feature.name); + // Get feature name - Backend uses 'label' field (German i18n key or legacy multilingual) + const getFeatureName = (feature: Feature) => { + const raw = getTextValue(feature.label || feature.name); + return raw === '-' ? '-' : t(raw); + }; if (error && !selectedFeatureCode) { return ( diff --git a/src/pages/admin/wizards/AdminMandateWizardPage.tsx b/src/pages/admin/wizards/AdminMandateWizardPage.tsx index 725ee29..fbd8e86 100644 --- a/src/pages/admin/wizards/AdminMandateWizardPage.tsx +++ b/src/pages/admin/wizards/AdminMandateWizardPage.tsx @@ -22,6 +22,7 @@ import { FormGeneratorForm } from '../../../components/FormGenerator/FormGenerat import styles from '../Admin.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; +import { labelAsI18nKey } from '../../../types/mandate'; const TOTAL_STEPS = 4; const STEP_LABELS = ['Mandant', 'Benutzer', 'Instances', 'Feature-Benutzer']; @@ -115,9 +116,7 @@ export const AdminMandateWizardPage: React.FC = () => { const getFeatureLabel = (code: string): string => { const f = features.find(feat => feat.code === code); if (f) { - return typeof f.label === 'object' - ? (f.label.de || f.label.en || code) - : (f.label || code); + return t(labelAsI18nKey(f.label, code)); } return code; }; diff --git a/src/pages/admin/wizards/FeatureInstanceWizard.tsx b/src/pages/admin/wizards/FeatureInstanceWizard.tsx index bfc93c0..2b17c8d 100644 --- a/src/pages/admin/wizards/FeatureInstanceWizard.tsx +++ b/src/pages/admin/wizards/FeatureInstanceWizard.tsx @@ -15,17 +15,13 @@ import styles from '../Admin.module.css'; import wizardStyles from './FeatureInstanceWizard.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; +import { labelAsI18nKey } from '../../../types/mandate'; function getMandateName(m: Mandate): string { if (typeof m.name === 'object') return m.name.de || m.name.en || Object.values(m.name)[0] || m.id; return m.name || m.id; } -function getFeatureLabel(f: Feature): string { - if (typeof f.label === 'object') return f.label.de || f.label.en || f.code; - return f.label || f.code; -} - export interface FeatureInstanceWizardProps { mandateId: string; mandates: Mandate[]; @@ -63,8 +59,8 @@ export const FeatureInstanceWizard: React.FC = ({ ma const [selectedUserRoles, setSelectedUserRoles] = useState>([]); const featureOptions = useMemo( - () => features.map((f) => ({ value: f.code, label: getFeatureLabel(f) })), - [features] + () => features.map((f) => ({ value: f.code, label: t(labelAsI18nKey(f.label, f.code)) })), + [features, t] ); const mandateOptions = useMemo( () => mandates.map((m) => ({ value: m.id, label: getMandateName(m) })), diff --git a/src/pages/billing/AdminSubscriptionsPage.tsx b/src/pages/billing/AdminSubscriptionsPage.tsx index aa0d390..b743b6a 100644 --- a/src/pages/billing/AdminSubscriptionsPage.tsx +++ b/src/pages/billing/AdminSubscriptionsPage.tsx @@ -16,12 +16,12 @@ function _getColumns(t: (key: string) => string): ColumnConfig[] { { key: 'status', label: t('adminSubscriptions.status'), type: 'text', sortable: true, filterable: true, width: 110 }, { key: 'recurring', label: t('adminSubscriptions.wiederkehrend'), type: 'boolean', sortable: true, filterable: true, width: 120 }, { key: 'activeUsers', label: t('adminSubscriptions.user'), type: 'number', sortable: true, width: 70 }, - { key: 'activeInstances', label: t('adminSubscriptions.instanzen'), type: 'number', sortable: true, width: 90 }, + { key: 'activeInstances', label: t('adminSubscriptions.module'), type: 'number', sortable: true, width: 90 }, { key: 'monthlyRevenueCHF', label: t('adminSubscriptions.revenueProMonat'), type: 'number', sortable: true, width: 140 }, { key: 'startedAt', label: t('adminSubscriptions.gestartet'), type: 'date', sortable: true, filterable: true, width: 130 }, { key: 'currentPeriodEnd', label: t('adminSubscriptions.periodenende'), type: 'date', sortable: true, filterable: true, width: 130 }, { key: 'snapshotPricePerUserCHF', label: t('adminSubscriptions.preisProUser'), type: 'number', sortable: true, width: 100 }, - { key: 'snapshotPricePerInstanceCHF', label: t('adminSubscriptions.preisProInstanz'), type: 'number', sortable: true, width: 110 }, + { key: 'snapshotPricePerInstanceCHF', label: t('adminSubscriptions.preisProModul'), type: 'number', sortable: true, width: 110 }, ]; } diff --git a/src/pages/billing/SubscriptionTab.tsx b/src/pages/billing/SubscriptionTab.tsx index 2c6551c..95aedc0 100644 --- a/src/pages/billing/SubscriptionTab.tsx +++ b/src/pages/billing/SubscriptionTab.tsx @@ -109,20 +109,29 @@ const PlanCard: React.FC = ({ plan, isCurrent, onActivate, activa {!isFreePlan && (
User: {_formatCurrency(plan.pricePerUserCHF)} / {periodLabel[plan.billingPeriod] || plan.billingPeriod}
-
Instanz: {_formatCurrency(plan.pricePerFeatureInstanceCHF)} / {periodLabel[plan.billingPeriod] || plan.billingPeriod}
+
+ Module inkl.: {plan.includedModules ?? 0} + {(plan.pricePerFeatureInstanceCHF ?? 0) > 0 && ( + <> · Zusatzmodul: {_formatCurrency(plan.pricePerFeatureInstanceCHF)} / Monat + )} +
- AI-Budget (inkl.): {_formatCurrency(plan.budgetAiCHF ?? 0)} / Periode + AI-Budget: {_formatCurrency(plan.budgetAiPerUserCHF ?? 0)} / User / Monat {' · '} - Speicher (inkl.):{' '} + Speicher:{' '} {plan.maxDataVolumeMB == null ? 'unbegrenzt' : formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)}
-
- Speicher über Plan: {_formatCurrency(storageOverageChfPerGbMonth)} / GB / Monat -
+ {plan.maxUsers != null && ( +
+ Max. User: {plan.maxUsers} + {' · '} + Speicher über Plan: {_formatCurrency(storageOverageChfPerGbMonth)} / GB / Monat +
+ )}
)} @@ -130,13 +139,13 @@ const PlanCard: React.FC = ({ plan, isCurrent, onActivate, activa
{plan.trialDays} Tage kostenlos {plan.maxUsers && <> · max. {plan.maxUsers} User} - {plan.maxFeatureInstances && <> · max. {plan.maxFeatureInstances} Instanzen} - {(plan.maxDataVolumeMB != null || (plan.budgetAiCHF ?? 0) > 0) && ( + {(plan.includedModules ?? 0) > 0 && <> · {plan.includedModules} Module inkl.} + {(plan.maxDataVolumeMB != null || (plan.budgetAiPerUserCHF ?? 0) > 0) && ( <> {plan.maxDataVolumeMB != null && ( - <> · Speicher inkl. {formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)} + <> · Speicher {formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)} )} - {(plan.budgetAiCHF ?? 0) > 0 && <> · AI-Budget { _formatCurrency(plan.budgetAiCHF ?? 0)}} + {(plan.budgetAiPerUserCHF ?? 0) > 0 && <> · AI-Budget {_formatCurrency(plan.budgetAiPerUserCHF ?? 0)} / User} )}
@@ -252,15 +261,16 @@ const SubInfoCard: React.FC = ({ sub, plan, label, onCancel, onRea )} {plan && ( <> - AI-Budget (inkl.): {_formatCurrency(plan.budgetAiCHF ?? 0)} / Periode + AI-Budget: {_formatCurrency(plan.budgetAiPerUserCHF ?? 0)} / User / Monat + Module inkl.: {plan.includedModules ?? 0} - Speicher (inkl.):{' '} + Speicher:{' '} {plan.maxDataVolumeMB == null ? 'unbegrenzt' : formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)} - - Speicher über Plan: {_formatCurrency(storageOverageChfPerGbMonth)} / GB / Monat (High-Watermark) + + Speicher über Plan: {_formatCurrency(storageOverageChfPerGbMonth)} / GB / Monat )} diff --git a/src/pages/views/trustee/TrusteeAbschlussView.tsx b/src/pages/views/trustee/TrusteeAbschlussView.tsx index ffe62a3..0740f6b 100644 --- a/src/pages/views/trustee/TrusteeAbschlussView.tsx +++ b/src/pages/views/trustee/TrusteeAbschlussView.tsx @@ -30,15 +30,13 @@ const _TABS: TabDef[] = [ { id: 'year-end', templateTag: 'template:trustee-year-end-check', icon: '\u2705', color: '#795548' }, ]; -const _TAB_LABELS: Record> = { - 'year-end': { de: 'Jahresabschluss prüfen', en: 'Year-End Review', fr: 'Contrôle de clôture' }, +const _TAB_LABEL_KEYS: Record = { + 'year-end': 'Jahresabschluss prüfen', }; -const _TAB_DESCRIPTIONS: Record> = { - 'year-end': { - de: 'Automatische Prüfungen für den Jahresabschluss: Saldovalidierung, Vorjahresvergleich, gesetzliche Checks.', - en: 'Automated year-end review: balance validation, prior-year comparison, legal compliance checks.', - }, +const _TAB_DESCRIPTION_KEYS: Record = { + 'year-end': + 'Automatische Prüfungen für den Jahresabschluss: Saldovalidierung, Vorjahresvergleich, gesetzliche Checks.', }; // --------------------------------------------------------------------------- @@ -58,8 +56,7 @@ type RunState = 'idle' | 'starting' | 'running' | 'completed' | 'error'; // --------------------------------------------------------------------------- export const TrusteeAbschlussView: React.FC = () => { - const { t, currentLanguage } = useLanguage(); - const lang = currentLanguage || 'de'; + const { t } = useLanguage(); const { instanceId } = useCurrentInstance(); const { showSuccess, showError } = useToast(); const [searchParams, setSearchParams] = useSearchParams(); @@ -228,7 +225,7 @@ export const TrusteeAbschlussView: React.FC = () => { }} > {tab.icon} - {_TAB_LABELS[tab.id]?.[lang] || _TAB_LABELS[tab.id]?.de || tab.id} + {t(_TAB_LABEL_KEYS[tab.id] || tab.id)} ))}
@@ -237,7 +234,7 @@ export const TrusteeAbschlussView: React.FC = () => { {/* Tab content */}

- {_TAB_DESCRIPTIONS[activeTab]?.[lang] || _TAB_DESCRIPTIONS[activeTab]?.de || ''} + {_TAB_DESCRIPTION_KEYS[activeTab] ? t(_TAB_DESCRIPTION_KEYS[activeTab]) : ''}

{workflowsLoading ? ( diff --git a/src/pages/views/trustee/TrusteeAnalyseView.tsx b/src/pages/views/trustee/TrusteeAnalyseView.tsx index f2862a9..61edc90 100644 --- a/src/pages/views/trustee/TrusteeAnalyseView.tsx +++ b/src/pages/views/trustee/TrusteeAnalyseView.tsx @@ -33,18 +33,18 @@ const _TABS: TabDef[] = [ { id: 'forecast', templateTag: 'template:trustee-forecast', icon: '\uD83D\uDCC8', color: '#E91E63' }, ]; -const _TAB_LABELS: Record> = { - budget: { de: 'Budget-Vergleich', en: 'Budget Comparison', fr: 'Comparaison budgétaire' }, - kpi: { de: 'KPI-Dashboard', en: 'KPI Dashboard', fr: 'Tableau de bord KPI' }, - cashflow: { de: 'Cashflow-Rechnung', en: 'Cash Flow Statement', fr: 'Flux de trésorerie' }, - forecast: { de: 'Prognose', en: 'Forecast', fr: 'Prévision' }, +const _TAB_LABEL_KEYS: Record = { + budget: 'Budget-Vergleich', + kpi: 'KPI-Dashboard', + cashflow: 'Cashflow-Rechnung', + forecast: 'Prognose', }; -const _TAB_DESCRIPTIONS: Record> = { - budget: { de: 'Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel', en: 'Compare actuals vs. budget from Excel' }, - kpi: { de: 'Kennzahlen berechnen und visualisieren', en: 'Calculate and visualize key metrics' }, - cashflow: { de: 'Cashflow berechnen und analysieren', en: 'Calculate and analyze cash flow' }, - forecast: { de: 'Trend-Analyse und Prognose der nächsten Monate', en: 'Trend analysis and forecast for coming months' }, +const _TAB_DESCRIPTION_KEYS: Record = { + budget: 'Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel', + kpi: 'Kennzahlen berechnen und visualisieren', + cashflow: 'Cashflow berechnen und analysieren', + forecast: 'Trend-Analyse und Prognose der nächsten Monate', }; // --------------------------------------------------------------------------- @@ -64,8 +64,7 @@ type RunState = 'idle' | 'starting' | 'running' | 'completed' | 'error'; // --------------------------------------------------------------------------- export const TrusteeAnalyseView: React.FC = () => { - const { t, currentLanguage } = useLanguage(); - const lang = currentLanguage || 'de'; + const { t } = useLanguage(); const { instanceId } = useCurrentInstance(); const { showSuccess, showError } = useToast(); const [searchParams, setSearchParams] = useSearchParams(); @@ -241,7 +240,7 @@ export const TrusteeAnalyseView: React.FC = () => { }} > {tab.icon} - {_TAB_LABELS[tab.id]?.[lang] || _TAB_LABELS[tab.id]?.de || tab.id} + {t(_TAB_LABEL_KEYS[tab.id] || tab.id)} ))}
@@ -249,7 +248,7 @@ export const TrusteeAnalyseView: React.FC = () => { {/* Tab content */}

- {_TAB_DESCRIPTIONS[activeTab]?.[lang] || _TAB_DESCRIPTIONS[activeTab]?.de || ''} + {_TAB_DESCRIPTION_KEYS[activeTab] ? t(_TAB_DESCRIPTION_KEYS[activeTab]) : ''}

{workflowsLoading ? ( diff --git a/src/pages/views/trustee/TrusteeDashboardView.tsx b/src/pages/views/trustee/TrusteeDashboardView.tsx index d52856b..a7cad66 100644 --- a/src/pages/views/trustee/TrusteeDashboardView.tsx +++ b/src/pages/views/trustee/TrusteeDashboardView.tsx @@ -100,7 +100,7 @@ export const TrusteeDashboardView: React.FC = () => {
{isLoading ? '...' : positions.length}
-
Positionen
+
{t('Positionen')}
@@ -110,7 +110,7 @@ export const TrusteeDashboardView: React.FC = () => {
{isLoading ? '...' : documents.length}
-
Dokumente
+
{t('Dokumente')}
@@ -122,11 +122,11 @@ export const TrusteeDashboardView: React.FC = () => {
{isLoading ? '...' : ( accountingConfig?.configured - ? <>{syncedCount} synced{syncErrorCount > 0 && / {syncErrorCount} errors} - : 'Not configured' + ? <>{syncedCount} {t('synchronisiert')}{syncErrorCount > 0 && / {syncErrorCount} {t('Fehler')}} + : t('Nicht konfiguriert') )}
-
Buchhaltung
+
{t('Buchhaltung')}
@@ -155,10 +155,10 @@ export const TrusteeDashboardView: React.FC = () => { />
-

Instanz-Details

+

{t('Instanz-Details')}

- Instanz: + {t('Instanz:')} {instance?.instanceLabel}
@@ -167,7 +167,7 @@ export const TrusteeDashboardView: React.FC = () => {
{accountingConfig?.configured && (
- Buchhaltungssystem: + {t('Buchhaltungssystem:')} {accountingConfig.displayLabel || accountingConfig.connectorType} {accountingConfig.lastSyncStatus && ` (${accountingConfig.lastSyncStatus})`} diff --git a/src/pages/views/trustee/TrusteeDocumentsView.tsx b/src/pages/views/trustee/TrusteeDocumentsView.tsx index c5acccf..a530346 100644 --- a/src/pages/views/trustee/TrusteeDocumentsView.tsx +++ b/src/pages/views/trustee/TrusteeDocumentsView.tsx @@ -140,7 +140,7 @@ export const TrusteeDocumentsView: React.FC = () => { window.URL.revokeObjectURL(url); } catch (err) { console.error('Download error:', err); - showError('Fehler', 'Fehler beim Herunterladen des Dokuments.'); + showError(t('Fehler'), t('Fehler beim Herunterladen des Dokuments.')); } finally { setDownloadingId(null); } @@ -172,9 +172,9 @@ export const TrusteeDocumentsView: React.FC = () => {
⚠️ -

Fehler beim Laden der Dokumente: {error}

+

{t('Fehler beim Laden der Dokumente: {detail}', { detail: String(error) })}

@@ -193,14 +193,14 @@ export const TrusteeDocumentsView: React.FC = () => { onClick={() => refetch()} disabled={loading} > - Aktualisieren + {t('Aktualisieren')} {canCreate && ( )}
diff --git a/src/pages/views/trustee/TrusteeInstanceRolesView.tsx b/src/pages/views/trustee/TrusteeInstanceRolesView.tsx index 4d324f9..b7c794b 100644 --- a/src/pages/views/trustee/TrusteeInstanceRolesView.tsx +++ b/src/pages/views/trustee/TrusteeInstanceRolesView.tsx @@ -57,13 +57,13 @@ export const TrusteeInstanceRolesView: React.FC = () => { const rolesList = response.data?.items || response.data || []; setRoles(Array.isArray(rolesList) ? rolesList : []); } catch (err: any) { - const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Laden der Rollen'; + const errorMsg = err.response?.data?.detail || err.message || t('Fehler beim Laden der Rollen'); setError(errorMsg); console.error('Error loading instance roles:', err); } finally { setLoading(false); } - }, [instance?.id, instance?.mandateId]); + }, [instance?.id, instance?.mandateId, t]); useEffect(() => { fetchRoles(); @@ -96,7 +96,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {

{error}

@@ -109,7 +109,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {

- Verwalten Sie die Berechtigungen für die Rollen dieser Trustee-Instanz + {t('Verwalten Sie die Berechtigungen für die Rollen dieser Trustee-Instanz')}

@@ -122,8 +122,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
- Diese Rollen wurden von den Feature-Templates kopiert. - Änderungen hier gelten nur für diese Instanz. + {t('Diese Rollen wurden von den Feature-Templates kopiert. Änderungen hier gelten nur für diese Instanz.')}
@@ -132,7 +131,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {

{t('trusteeInstanceRoles.keineInstanzrollenGefunden')}

- Instanz-Rollen werden automatisch erstellt, wenn Benutzer dieser Instanz zugewiesen werden. + {t('Instanz-Rollen werden automatisch erstellt, wenn Benutzer dieser Instanz zugewiesen werden.')}

) : ( @@ -154,7 +153,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
{role.isSystemRole && ( - System + {t('System')} )}
diff --git a/src/pages/views/trustee/TrusteePositionDocumentsView.tsx b/src/pages/views/trustee/TrusteePositionDocumentsView.tsx index 6a94338..76ff5e7 100644 --- a/src/pages/views/trustee/TrusteePositionDocumentsView.tsx +++ b/src/pages/views/trustee/TrusteePositionDocumentsView.tsx @@ -140,9 +140,9 @@ export const TrusteePositionDocumentsView: React.FC = () => {
⚠️ -

Fehler beim Laden der Verknüpfungen: {error}

+

{t('Fehler beim Laden der Verknüpfungen: {detail}', { detail: String(error) })}

@@ -161,14 +161,14 @@ export const TrusteePositionDocumentsView: React.FC = () => { onClick={() => refetch()} disabled={loading} > - Aktualisieren + {t('Aktualisieren')} {canCreate && ( )} diff --git a/src/pages/views/trustee/TrusteePositionsView.tsx b/src/pages/views/trustee/TrusteePositionsView.tsx index fefa7f7..7d4cd04 100644 --- a/src/pages/views/trustee/TrusteePositionsView.tsx +++ b/src/pages/views/trustee/TrusteePositionsView.tsx @@ -146,7 +146,7 @@ export const TrusteePositionsView: React.FC = () => { window.URL.revokeObjectURL(url); } catch (err) { console.error('Download error:', err); - showError('Fehler', 'Fehler beim Herunterladen des Dokuments.'); + showError(t('Fehler'), t('Fehler beim Herunterladen des Dokuments.')); } finally { setDownloadingDocIds(prev => { const next = new Set(prev); @@ -394,9 +394,9 @@ export const TrusteePositionsView: React.FC = () => {
⚠️ -

Fehler beim Laden der Positionen: {error}

+

{t('Fehler beim Laden der Positionen: {detail}', { detail: String(error) })}

@@ -415,14 +415,14 @@ export const TrusteePositionsView: React.FC = () => { onClick={() => refetch()} disabled={loading} > - Aktualisieren + {t('Aktualisieren')} {canCreate && ( )} diff --git a/src/types/mandate.ts b/src/types/mandate.ts index af66307..e35f5f4 100644 --- a/src/types/mandate.ts +++ b/src/types/mandate.ts @@ -101,7 +101,7 @@ export interface FeatureInstance { */ export interface MandateFeature { code: string; // "trustee", "chatbot", "chatworkflow", etc. - label: I18nLabel; // { de: "Treuhand", en: "Trustee" } + label: string; // German plaintext i18n key icon: string; // Material/React Icon Name instances: FeatureInstance[]; } @@ -163,7 +163,7 @@ export interface User { */ export interface FeatureView { code: string; // z.B. "dashboard", "contracts", "documents" - label: I18nLabel; + label: string; // German plaintext i18n key icon?: string; path: string; // Relativer Pfad innerhalb der Instanz adminOnly?: boolean; // Nur für Admin-Rollen sichtbar @@ -175,7 +175,7 @@ export interface FeatureView { */ export interface FeatureConfig { code: string; - label: I18nLabel; + label: string; // German plaintext i18n key icon: string; views: FeatureView[]; deprecated?: boolean; @@ -201,100 +201,100 @@ export interface FeatureConfig { export const FEATURE_REGISTRY: Record = { trustee: { code: 'trustee', - label: { de: 'Treuhand', en: 'Trustee' }, + label: 'Treuhand', icon: 'briefcase', views: [ - { code: 'dashboard', label: { de: 'Übersicht', en: 'Dashboard' }, path: 'dashboard' }, - { code: 'positions', label: { de: 'Positionen', en: 'Positions' }, path: 'positions' }, - { code: 'documents', label: { de: 'Dokumente', en: 'Documents' }, path: 'documents' }, - { code: 'position-documents', label: { de: 'Zuordnungen', en: 'Assignments' }, path: 'position-documents' }, - { code: 'expense-import', label: { de: 'Spesen Import', en: 'Expense Import' }, path: 'expense-import' }, - { code: 'scan-upload', label: { de: 'Scannen / Hochladen', en: 'Scan / Upload' }, path: 'scan-upload' }, - { code: 'instance-roles', label: { de: 'Rollen & Rechte', en: 'Roles & Permissions' }, path: 'instance-roles', adminOnly: true }, - { code: 'settings', label: { de: 'Buchhaltungseinstellungen', en: 'Accounting Settings' }, path: 'settings' }, + { code: 'dashboard', label: 'Übersicht', path: 'dashboard' }, + { code: 'positions', label: 'Positionen', path: 'positions' }, + { code: 'documents', label: 'Dokumente', path: 'documents' }, + { code: 'position-documents', label: 'Zuordnungen', path: 'position-documents' }, + { code: 'expense-import', label: 'Spesen Import', path: 'expense-import' }, + { code: 'scan-upload', label: 'Scannen / Hochladen', path: 'scan-upload' }, + { code: 'instance-roles', label: 'Rollen & Rechte', path: 'instance-roles', adminOnly: true }, + { code: 'settings', label: 'Buchhaltungseinstellungen', path: 'settings' }, ] }, chatworkflow: { code: 'chatworkflow', - label: { de: 'Workflow', en: 'Workflow' }, + label: 'Workflow', icon: 'play_circle', views: [ - { code: 'dashboard', label: { de: 'Übersicht', en: 'Dashboard' }, path: 'dashboard' }, - { code: 'runs', label: { de: 'Runs', en: 'Runs' }, path: 'runs' }, - { code: 'files', label: { de: 'Dateien', en: 'Files' }, path: 'files' }, + { code: 'dashboard', label: 'Übersicht', path: 'dashboard' }, + { code: 'runs', label: 'Runs', path: 'runs' }, + { code: 'files', label: 'Dateien', path: 'files' }, ] }, chatbot: { code: 'chatbot', - label: { de: 'Chatbot', en: 'Chatbot' }, + label: 'Chatbot', icon: 'chat', views: [ - { code: 'conversations', label: { de: 'Konversationen', en: 'Conversations' }, path: 'conversations' }, - { code: 'settings', label: { de: 'Einstellungen', en: 'Settings' }, path: 'settings' }, + { code: 'conversations', label: 'Konversationen', path: 'conversations' }, + { code: 'settings', label: 'Einstellungen', path: 'settings' }, ] }, realestate: { code: 'realestate', - label: { de: 'Immobilien', en: 'Real Estate' }, + label: 'Immobilien', icon: 'home', views: [ - { code: 'dashboard', label: { de: 'Karte', en: 'Map' }, path: 'dashboard' }, - { code: 'instance-roles', label: { de: 'Rollen & Rechte', en: 'Roles & Permissions' }, path: 'instance-roles', adminOnly: true }, + { code: 'dashboard', label: 'Karte', path: 'dashboard' }, + { code: 'instance-roles', label: 'Rollen & Rechte', path: 'instance-roles', adminOnly: true }, ] }, teamsbot: { code: 'teamsbot', - label: { de: 'Teams Bot', en: 'Teams Bot' }, + label: 'Teams Bot', icon: 'headset_mic', views: [ - { code: 'dashboard', label: { de: 'Übersicht', en: 'Dashboard' }, path: 'dashboard' }, - { code: 'sessions', label: { de: 'Sitzungen', en: 'Sessions' }, path: 'sessions' }, - { code: 'settings', label: { de: 'Einstellungen', en: 'Settings' }, path: 'settings' }, + { code: 'dashboard', label: 'Übersicht', path: 'dashboard' }, + { code: 'sessions', label: 'Sitzungen', path: 'sessions' }, + { code: 'settings', label: 'Einstellungen', path: 'settings' }, ] }, graphicalEditor: { code: 'graphicalEditor', - label: { de: 'Grafischer Editor', en: 'Graphical Editor' }, + label: 'Grafischer Editor', icon: 'sitemap', views: [ - { code: 'editor', label: { de: 'Editor', en: 'Editor' }, path: 'editor' }, - { code: 'workflows', label: { de: 'Workflows', en: 'Workflows' }, path: 'workflows' }, - { code: 'templates', label: { de: 'Vorlagen', en: 'Templates' }, path: 'templates' }, - { code: 'workflows-tasks', label: { de: 'Tasks', en: 'Tasks' }, path: 'workflows-tasks' }, - { code: 'dashboard', label: { de: 'Dashboard', en: 'Dashboard' }, path: 'dashboard' }, + { code: 'editor', label: 'Editor', path: 'editor' }, + { code: 'workflows', label: 'Workflows', path: 'workflows' }, + { code: 'templates', label: 'Vorlagen', path: 'templates' }, + { code: 'workflows-tasks', label: 'Tasks', path: 'workflows-tasks' }, + { code: 'dashboard', label: 'Dashboard', path: 'dashboard' }, ] }, neutralization: { code: 'neutralization', - label: { de: 'Neutralisierung', en: 'Neutralization', fr: 'Neutralisation' }, + label: 'Neutralisierung', icon: 'shield_check', views: [ - { code: 'dashboard', label: { de: 'Neutralisierung testen', en: 'Test Neutralization', fr: 'Tester neutralisation' }, path: 'playground' }, - { code: 'playground', label: { de: 'Neutralisierung testen', en: 'Test Neutralization', fr: 'Tester neutralisation' }, path: 'playground' }, - { code: 'config', label: { de: 'Einstellungen', en: 'Settings', fr: 'Paramètres' }, path: 'config' }, - { code: 'attributes', label: { de: 'Attribute', en: 'Attributes', fr: 'Attributs' }, path: 'attributes' }, + { code: 'dashboard', label: 'Neutralisierung testen', path: 'playground' }, + { code: 'playground', label: 'Neutralisierung testen', path: 'playground' }, + { code: 'config', label: 'Einstellungen', path: 'config' }, + { code: 'attributes', label: 'Attribute', path: 'attributes' }, ] }, commcoach: { code: 'commcoach', - label: { de: 'Kommunikations-Coach', en: 'Communication Coach', fr: 'Coach Communication' }, + label: 'Kommunikations-Coach', icon: 'account_voice', views: [ - { code: 'dashboard', label: { de: 'Dashboard', en: 'Dashboard', fr: 'Tableau de bord' }, path: 'dashboard' }, - { code: 'coaching', label: { de: 'Coaching', en: 'Coaching', fr: 'Coaching' }, path: 'coaching' }, - { code: 'dossier', label: { de: 'Dossier', en: 'Dossier', fr: 'Dossier' }, path: 'dossier' }, - { code: 'settings', label: { de: 'Einstellungen', en: 'Settings', fr: 'Paramètres' }, path: 'settings' }, + { code: 'dashboard', label: 'Dashboard', path: 'dashboard' }, + { code: 'coaching', label: 'Coaching', path: 'coaching' }, + { code: 'dossier', label: 'Dossier', path: 'dossier' }, + { code: 'settings', label: 'Einstellungen', path: 'settings' }, ] }, workspace: { code: 'workspace', - label: { de: 'AI Workspace', en: 'AI Workspace', fr: 'AI Workspace' }, + label: 'AI Workspace', icon: 'psychology', views: [ - { code: 'dashboard', label: { de: 'Dashboard', en: 'Dashboard', fr: 'Tableau de bord' }, path: 'dashboard' }, - { code: 'editor', label: { de: 'Editor', en: 'Editor', fr: 'Editeur' }, path: 'editor' }, - { code: 'rag-insights', label: { de: 'Wissens-Insights', en: 'Knowledge insights', fr: 'Aperçu des connaissances' }, path: 'rag-insights' }, - { code: 'settings', label: { de: 'Einstellungen', en: 'Settings', fr: 'Parametres' }, path: 'settings' }, + { code: 'dashboard', label: 'Dashboard', path: 'dashboard' }, + { code: 'editor', label: 'Editor', path: 'editor' }, + { code: 'rag-insights', label: 'Wissens-Insights', path: 'rag-insights' }, + { code: 'settings', label: 'Einstellungen', path: 'settings' }, ] }, }; @@ -332,8 +332,20 @@ export function canAccessRecord( } /** - * Holt das Label für die aktuelle Sprache + * Holt Navigations-Label: i18n-Key (String) oder Legacy-I18nLabel. */ -export function getLabel(label: I18nLabel, lang: 'de' | 'en' | 'fr' = 'de'): string { +export function getLabel(label: I18nLabel | string, lang: 'de' | 'en' | 'fr' = 'de'): string { + if (typeof label === 'string') return label; return label[lang] || label.de || label.en || ''; } + +/** German i18n key from API label (string or legacy multilingual object). */ +export function labelAsI18nKey( + label: string | I18nLabel | { [key: string]: string } | undefined, + fallback: string +): string { + if (label === undefined || label === null) return fallback; + if (typeof label === 'string') return label || fallback; + const o = label as I18nLabel; + return o.de || o.en || o.fr || fallback; +}