'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useRouter } from 'next/navigation'; import PageLayout from '../../components/PageLayout'; import TranslationWizardModal from './components/TranslationWizardModal'; import AddLanguageModal from './components/AddLanguageModal'; import DeleteLanguageModal from './components/DeleteLanguageModal'; import CategoryManagerModal from './components/CategoryManagerModal'; import ScanResultsModal from './components/ScanResultsModal'; import TranslationCoverageEditor from './components/TranslationCoverageEditor'; import LanguageManagementTopSection from './components/LanguageManagementTopSection'; import { useI18nScanWorkflow } from './hooks/useI18nScanWorkflow'; import { useLanguageManagementTranslations } from './hooks/useLanguageManagementTranslations'; import { useNamespaceCategories } from './hooks/useNamespaceCategories'; import { useModalAnimation } from './hooks/useModalAnimation'; import { useToast } from '../../components/toast/toastComponent'; import { isPageTransitioning } from '../../components/animation/pageTransitionEffect'; import { getAllTranslationKeys, getEnglishValue, useTranslation, } from '../../i18n/useTranslation'; const CORE_LANGUAGES = new Set(['en', 'de']); const AUTO_SCROLL_ON_SAVE_STORAGE_KEY = 'language-management-auto-scroll-on-save'; // ── namespace categories const NAMESPACE_CATEGORIES: { label: string; namespaces: string[] }[] = [ { label: 'General', namespaces: ['common', 'nav', 'footer', 'home'] }, { label: 'Auth', namespaces: ['login', 'register', 'passwordReset'] }, { label: 'Pages', namespaces: ['dashboard', 'profile', 'community', 'shop', 'memberships', 'affiliateLinks', 'aboutUs', 'news'] }, { label: 'Coffee ABO', namespaces: ['coffeeSelection', 'coffeeSummary'] }, { label: 'Account', namespaces: ['personalMatrix', 'referralManagement', 'quickactionDashboard', 'suspended'] }, { label: 'Admin', namespaces: ['adminDashboard', 'userManagement', 'languageManagement', 'contractManagement'] }, { label: 'Notifications', namespaces: ['toasts'] }, ]; function groupKeys(keys: string[]): Record { const groups: Record = {}; for (const key of keys) { const ns = key.split('.')[0]; if (!groups[ns]) groups[ns] = []; groups[ns].push(key); } return groups; } export default function LanguageManagementPage() { const { t } = useTranslation(); const router = useRouter(); const { showToast } = useToast(); const notifyAction = useCallback((notice: { variant: 'success' | 'error' | 'info' | 'warning'; message: string }) => { showToast({ variant: notice.variant, message: notice.message, duration: notice.variant === 'error' ? 6000 : 3500, }); }, [showToast]); const handleUnauthorized = useCallback(() => { router.push('/login'); }, [router]); const { showScanModal, setShowScanModal, lastScanTime, isScanning, isAutoFixing, isAddingMissingKeys, scanError, workspaceScan, selectedFiles, fixableFiles, forceConvertToClient, setForceConvertToClient, scan, runFixSelected, addMissingKeys, toggleFileSelection, selectAllFiles, clearSelectedFiles, } = useI18nScanWorkflow({ onUnauthorized: handleUnauthorized }); // ── all flat keys from the English source-of-truth const allKeys = useMemo(() => getAllTranslationKeys(), []); const keyGroups = useMemo(() => groupKeys(allKeys), [allKeys]); const namespaces = useMemo(() => Object.keys(keyGroups).sort(), [keyGroups]); const { data, isLoading: isTranslationsLoading, loadingPhase: translationsLoadingPhase, loadingProgress: translationsLoadingProgress, loadingLogs: translationsLoadingLogs, allLanguages, activeLang, setActiveLang, getDisplayValue, isDirty, saved, saveError, handleChange, handleSave, showAddModal, setShowAddModal, newCode, setNewCode, newName, setNewName, addError, setAddError, handleAddLanguage, deleteTarget, setDeleteTarget, handleDeleteLanguage, isBuiltin, } = useLanguageManagementTranslations({ coreLanguages: CORE_LANGUAGES, onAction: notifyAction, onUnauthorized: handleUnauthorized, }); const [showTranslationWizard, setShowTranslationWizard] = useState(false); const [wizardIndex, setWizardIndex] = useState(0); const [wizardInput, setWizardInput] = useState(''); const [wizardMarkGlobal, setWizardMarkGlobal] = useState(false); const [wizardUseEnglishReference, setWizardUseEnglishReference] = useState(false); const [isWizardSavingStep, setIsWizardSavingStep] = useState(false); const [newGlobalKeySelection, setNewGlobalKeySelection] = useState(''); const [reloadAfterScanClose, setReloadAfterScanClose] = useState(false); const [pendingAutoFixResult, setPendingAutoFixResult] = useState(false); const { activeCategory, setActiveCategory, showCategoryManagerModal, setShowCategoryManagerModal, isPreferencesDirty, preferencesHydrated, preferencesLoadingPhase, preferencesLoadingProgress, preferencesLoadingLogs, isSavingPreferences, savePreferences, categoriesWithKnownNamespaces, uncategorizedNamespaces, addNamespaceToCategory, removeNamespaceFromCategory, newCategoryLabel, setNewCategoryLabel, assignNamespaceByCategory, setAssignNamespaceByCategory, expandedCategoryId, setExpandedCategoryId, dragNamespace, setDragNamespace, handleCreateCategory, deleteCategory, globalKnownKeys, globalKeySet, addGlobalKey, removeGlobalKey, englishReferenceKeysByLanguage, englishReferenceKeySetByLanguage, setEnglishReferenceKey, } = useNamespaceCategories({ namespaces, allKeys, defaultCategories: NAMESPACE_CATEGORIES, onAction: notifyAction, onUnauthorized: handleUnauthorized, }); const hasUnsavedChanges = isDirty || isPreferencesDirty; const isInitialDataLoading = isTranslationsLoading || !preferencesHydrated; const initialLoadingProgress = Math.round((translationsLoadingProgress + preferencesLoadingProgress) / 2); const [isPageTransitionDone, setIsPageTransitionDone] = useState(() => !isPageTransitioning); const [showDelayedFetchingScreen, setShowDelayedFetchingScreen] = useState(false); const [showDelayedFetchLogs, setShowDelayedFetchLogs] = useState(false); const combinedLoadingLogs = useMemo(() => { const preferenceLines = preferencesLoadingLogs.map((line) => `[PREF] ${line}`); const translationLines = translationsLoadingLogs.map((line) => `[I18N] ${line}`); return [...preferenceLines, ...translationLines].slice(-18); }, [preferencesLoadingLogs, translationsLoadingLogs]); const { isRendered: isSaveBarRendered, isVisible: isSaveBarVisible } = useModalAnimation(hasUnsavedChanges); useEffect(() => { if (isPageTransitionDone) return; let rafId: number | null = null; const pollTransition = () => { if (!isPageTransitioning) { setIsPageTransitionDone(true); return; } rafId = window.requestAnimationFrame(pollTransition); }; rafId = window.requestAnimationFrame(pollTransition); return () => { if (rafId) window.cancelAnimationFrame(rafId); }; }, [isPageTransitionDone]); useEffect(() => { if (!isInitialDataLoading || !isPageTransitionDone) { setShowDelayedFetchingScreen(false); return; } const timeoutId = window.setTimeout(() => setShowDelayedFetchingScreen(true), 350); return () => window.clearTimeout(timeoutId); }, [isInitialDataLoading, isPageTransitionDone]); useEffect(() => { if (!showDelayedFetchingScreen) { setShowDelayedFetchLogs(false); return; } const timeoutId = window.setTimeout(() => setShowDelayedFetchLogs(true), 1400); return () => window.clearTimeout(timeoutId); }, [showDelayedFetchingScreen]); const showFetchingScreen = isInitialDataLoading && isPageTransitionDone && showDelayedFetchingScreen; const showWarmGap = isInitialDataLoading && isPageTransitionDone && !showDelayedFetchingScreen; // ── search / filter const [search, setSearch] = useState(''); const [autoScrollOnPanelOpen, setAutoScrollOnPanelOpen] = useState(true); const [autoScrollOnSave, setAutoScrollOnSave] = useState(() => { if (typeof window === 'undefined') return false; return window.localStorage.getItem(AUTO_SCROLL_ON_SAVE_STORAGE_KEY) === '1'; }); const [activeNamespacePanel, setActiveNamespacePanel] = useState(null); const languageManagementHeaderRef = useRef(null); const openedNamespacePanelRef = useRef(null); const openFromPanelClickRef = useRef(false); const saveBarRef = useRef(null); const [showScrollHint, setShowScrollHint] = useState(false); const scrollHintTimerRef = useRef | null>(null); const { isRendered: isHintRendered, isVisible: isHintVisible } = useModalAnimation(showScrollHint); useEffect(() => { if (typeof window === 'undefined') return; window.localStorage.setItem(AUTO_SCROLL_ON_SAVE_STORAGE_KEY, autoScrollOnSave ? '1' : '0'); }, [autoScrollOnSave]); const filteredGroups = useMemo(() => { const q = search.toLowerCase(); if (!q) return keyGroups; const result: Record = {}; for (const [ns, keys] of Object.entries(keyGroups)) { const filtered = keys.filter( (k) => k.toLowerCase().includes(q) || getEnglishValue(k).toLowerCase().includes(q) ); if (filtered.length > 0) result[ns] = filtered; } return result; }, [keyGroups, search]); // ── scan results: coverage per namespace const scanResults = useMemo(() => { return namespaces.map((ns) => { const keys = keyGroups[ns] ?? []; let translated = 0; if (activeLang === 'en') { translated = keys.length; // English is the source } else { translated = keys.filter((k) => getDisplayValue(k) !== '').length; } return { ns, total: keys.length, translated, missing: keys.length - translated }; }); }, [namespaces, keyGroups, activeLang, getDisplayValue]); const categoryNamespaces = useMemo(() => { if (activeCategory === 'all') return null; // null = no filter if (activeCategory === 'global') return []; return categoriesWithKnownNamespaces.find((c) => c.id === activeCategory)?.namespaces ?? []; }, [activeCategory, categoriesWithKnownNamespaces]); const filteredNs = useMemo(() => { if (activeCategory === 'global') return []; const base = Object.keys(filteredGroups).sort(); if (!categoryNamespaces) return base; return base.filter((ns) => categoryNamespaces.includes(ns)); }, [activeCategory, filteredGroups, categoryNamespaces]); // Keep active panel in sync with currently visible namespaces useEffect(() => { if (activeCategory === 'global') { setActiveNamespacePanel(null); return; } if (filteredNs.length === 0) { setActiveNamespacePanel(null); return; } if (!activeNamespacePanel || !filteredNs.includes(activeNamespacePanel)) { setActiveNamespacePanel(filteredNs[0]); } }, [activeCategory, filteredNs, activeNamespacePanel]); const totalKeys = allKeys.length; const isMissingForLanguage = (key: string, languageCode: string) => { if (languageCode === 'en') return false; if (globalKeySet.has(key)) return false; if (englishReferenceKeySetByLanguage[languageCode]?.has(key)) return false; const value = (data.translations[languageCode]?.[key] ?? '').trim(); const englishValue = getEnglishValue(key).trim(); return value === '' || value === englishValue; }; const namespaceTranslationStats = useMemo(() => { const stats: Record = {}; for (const ns of namespaces) { const keys = keyGroups[ns] ?? []; const total = keys.length; let translated = 0; if (activeLang === 'en') { translated = total; } else { translated = keys.filter((k) => !isMissingForLanguage(k, activeLang)).length; } stats[ns] = { total, translated, missing: Math.max(total - translated, 0) }; } return stats; }, [namespaces, keyGroups, activeLang, data.translations, globalKeySet]); const allTabStats = useMemo(() => { return Object.values(namespaceTranslationStats).reduce( (acc, cur) => ({ total: acc.total + cur.total, translated: acc.translated + cur.translated, missing: acc.missing + cur.missing, }), { total: 0, translated: 0, missing: 0 } ); }, [namespaceTranslationStats]); const categoryTabStats = useMemo(() => { const result: Record = {}; for (const cat of categoriesWithKnownNamespaces) { result[cat.id] = cat.namespaces.reduce( (acc, ns) => { const st = namespaceTranslationStats[ns] ?? { total: 0, translated: 0, missing: 0 }; return { total: acc.total + st.total, translated: acc.translated + st.translated, missing: acc.missing + st.missing, }; }, { total: 0, translated: 0, missing: 0 } ); } return result; }, [categoriesWithKnownNamespaces, namespaceTranslationStats]); const translationProgressPercent = useMemo(() => { if (allTabStats.total === 0) return 100; return Math.round((allTabStats.translated / allTabStats.total) * 100); }, [allTabStats]); const wizardMissingKeys = useMemo(() => { if (activeLang === 'en') return [] as string[]; return allKeys.filter((k) => isMissingForLanguage(k, activeLang)); }, [activeLang, allKeys, data.translations, globalKeySet]); const currentWizardKey = showTranslationWizard ? wizardMissingKeys[wizardIndex] ?? null : null; const globalFilteredKeys = useMemo(() => { const q = search.toLowerCase().trim(); if (!q) return [...globalKnownKeys].sort(); return globalKnownKeys .filter((key) => key.toLowerCase().includes(q) || getEnglishValue(key).toLowerCase().includes(q)) .sort(); }, [globalKnownKeys, search]); const availableGlobalKeyOptions = useMemo(() => { const globalSet = new Set(globalKnownKeys); return allKeys.filter((key) => !globalSet.has(key)).sort(); }, [allKeys, globalKnownKeys]); const globalTabStats = useMemo(() => { const total = globalKnownKeys.length; // Global keys are intentionally the same across languages — never count as missing return { total, translated: total, missing: 0 }; }, [globalKnownKeys]); useEffect(() => { if (!showTranslationWizard) return; if (wizardMissingKeys.length === 0) { setShowTranslationWizard(false); return; } if (wizardIndex > wizardMissingKeys.length - 1) { setWizardIndex(0); return; } const key = wizardMissingKeys[wizardIndex]; setWizardInput(data.translations[activeLang]?.[key] ?? ''); setWizardMarkGlobal(globalKnownKeys.includes(key)); setWizardUseEnglishReference(Boolean(englishReferenceKeySetByLanguage[activeLang]?.has(key))); }, [showTranslationWizard, wizardIndex, wizardMissingKeys, data.translations, activeLang, globalKnownKeys, englishReferenceKeySetByLanguage]); const saveCurrentWizardValue = () => { if (!currentWizardKey) return; const trimmedWizardInput = wizardInput.trim(); const englishValue = getEnglishValue(currentWizardKey).trim(); const nextTranslations: Record> = { ...data.translations, [activeLang]: { ...(data.translations[activeLang] ?? {}), }, }; const nextGlobalKeysSet = new Set(globalKnownKeys); const nextEnglishReferenceKeysByLanguage = { ...englishReferenceKeysByLanguage, [activeLang]: [...(englishReferenceKeysByLanguage[activeLang] ?? [])], }; const nextEnglishReferenceSet = new Set(nextEnglishReferenceKeysByLanguage[activeLang] ?? []); if (wizardUseEnglishReference) { nextTranslations[activeLang][currentWizardKey] = englishValue; handleChange(currentWizardKey, englishValue); setEnglishReferenceKey(activeLang, currentWizardKey, true); nextEnglishReferenceSet.add(currentWizardKey); if (wizardMarkGlobal) { addGlobalKey(currentWizardKey); nextGlobalKeysSet.add(currentWizardKey); } else { removeGlobalKey(currentWizardKey); nextGlobalKeysSet.delete(currentWizardKey); } nextEnglishReferenceKeysByLanguage[activeLang] = [...nextEnglishReferenceSet].sort((a, b) => a.localeCompare(b)); return { nextTranslations, preferencesSnapshot: { globalKeys: [...nextGlobalKeysSet].sort((a, b) => a.localeCompare(b)), englishReferenceKeysByLanguage: nextEnglishReferenceKeysByLanguage, }, }; } // Global keys intentionally reuse the source term and don't require a local override. const nextValue = wizardMarkGlobal && trimmedWizardInput === englishValue ? '' : trimmedWizardInput; nextTranslations[activeLang][currentWizardKey] = nextValue; handleChange(currentWizardKey, nextValue); setEnglishReferenceKey(activeLang, currentWizardKey, false); nextEnglishReferenceSet.delete(currentWizardKey); if (wizardMarkGlobal) { addGlobalKey(currentWizardKey); nextGlobalKeysSet.add(currentWizardKey); } else { removeGlobalKey(currentWizardKey); nextGlobalKeysSet.delete(currentWizardKey); } const normalizedEnglishReferenceKeysByLanguage = { ...nextEnglishReferenceKeysByLanguage }; const normalizedKeys = [...nextEnglishReferenceSet].sort((a, b) => a.localeCompare(b)); if (normalizedKeys.length === 0) { delete normalizedEnglishReferenceKeysByLanguage[activeLang]; } else { normalizedEnglishReferenceKeysByLanguage[activeLang] = normalizedKeys; } return { nextTranslations, preferencesSnapshot: { globalKeys: [...nextGlobalKeysSet].sort((a, b) => a.localeCompare(b)), englishReferenceKeysByLanguage: normalizedEnglishReferenceKeysByLanguage, }, }; }; const openTranslationWizard = () => { if (wizardMissingKeys.length === 0) return; setWizardIndex(0); setShowTranslationWizard(true); }; const goToNextWizardStep = async () => { if (isWizardSavingStep) return; setIsWizardSavingStep(true); const saveSnapshot = saveCurrentWizardValue(); try { if (saveSnapshot) { // Persist both translation content and preferences on every wizard step. await handleSave(saveSnapshot.nextTranslations); await savePreferences(saveSnapshot.preferencesSnapshot); } if (wizardIndex >= wizardMissingKeys.length - 1) { setShowTranslationWizard(false); return; } setWizardIndex((prev) => prev + 1); } finally { setIsWizardSavingStep(false); } }; const skipWizardStep = () => { if (wizardIndex >= wizardMissingKeys.length - 1) { setShowTranslationWizard(false); return; } setWizardIndex((prev) => prev + 1); }; const goToPreviousWizardStep = () => { if (wizardIndex <= 0) return; setWizardIndex((prev) => prev - 1); }; useEffect(() => { if (!activeNamespacePanel) return; if (!openFromPanelClickRef.current) return; if (!autoScrollOnPanelOpen || !openedNamespacePanelRef.current) { openFromPanelClickRef.current = false; return; } openedNamespacePanelRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); openFromPanelClickRef.current = false; }, [activeNamespacePanel, autoScrollOnPanelOpen]); const scrollToLanguageManagementHeader = () => { if (!languageManagementHeaderRef.current) return; languageManagementHeaderRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); }; // Show scroll-to-save hint whenever a namespace panel is opened and there are unsaved changes // It stays visible until explicitly dismissed (button click) or save bar disappears useEffect(() => { if (!activeNamespacePanel || !isSaveBarRendered) { setShowScrollHint(false); return; } if (scrollHintTimerRef.current) clearTimeout(scrollHintTimerRef.current); setShowScrollHint(true); }, [activeNamespacePanel, isSaveBarRendered]); // Hide scroll hint when the save bar scrolls into view useEffect(() => { const el = saveBarRef.current; if (!el) return; const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) setShowScrollHint(false); }, { threshold: 0.5 } ); observer.observe(el); return () => observer.disconnect(); }, [isSaveBarRendered]); useEffect(() => { if (!pendingAutoFixResult || !workspaceScan) return; const changedFiles = workspaceScan.changedFileCount ?? 0; const createdKeys = workspaceScan.createdKeyCount ?? 0; if (changedFiles > 0 || createdKeys > 0) { setReloadAfterScanClose(true); showToast({ variant: 'success', message: `Auto-fix finished: ${changedFiles} files updated, ${createdKeys} keys created.`, duration: 4200, }); } setPendingAutoFixResult(false); }, [pendingAutoFixResult, workspaceScan, showToast]); const handleRunFixSelected = async () => { setPendingAutoFixResult(true); await runFixSelected(); }; const closeScanModal = () => { setShowScanModal(false); if (!reloadAfterScanClose) return; showToast({ variant: 'info', message: t('autofix.k3871d88e'), duration: 1800, }); setTimeout(() => { if (typeof window !== 'undefined') { window.location.reload(); } }, 260); setReloadAfterScanClose(false); }; const handleSaveAll = async () => { const hadChangesToSave = isDirty || isPreferencesDirty; console.debug('[LanguageManagement][SaveAll] start', { isDirty, isPreferencesDirty, hasUnsavedChanges, autoScrollOnSave, }); if (isDirty) { console.debug('[LanguageManagement][SaveAll] saving:translations'); await handleSave(); console.debug('[LanguageManagement][SaveAll] saved:translations'); } if (isPreferencesDirty) { console.debug('[LanguageManagement][SaveAll] saving:preferences'); const savedPreferences = await savePreferences(); console.debug('[LanguageManagement][SaveAll] saved:preferences', { savedPreferences }); } if (autoScrollOnSave && hadChangesToSave) { scrollToLanguageManagementHeader(); } console.debug('[LanguageManagement][SaveAll] done'); }; return (
{showFetchingScreen ? (

{t('autofix.k78e1bf35')}

I18N: {translationsLoadingPhase} | PREF: {preferencesLoadingPhase}

{initialLoadingProgress}%
{showDelayedFetchLogs && (

{t('autofix.k1c7ec4f2')}

{t('autofix.k057b3dbd')}

{combinedLoadingLogs.length === 0 ? (

{t('autofix.k835d3cbf')}

) : ( combinedLoadingLogs.map((line, idx) => (

{line}

)) )}
)}
) : showWarmGap ? (
) : ( <> router.push('/admin')} isDirty={hasUnsavedChanges} onSave={handleSaveAll} saved={saved} saveError={saveError} allLanguages={allLanguages} activeLang={activeLang} setActiveLang={setActiveLang} isBuiltin={isBuiltin} onDeleteLanguageRequest={setDeleteTarget} onOpenAddLanguage={() => setShowAddModal(true)} allTabStats={allTabStats} translationProgressPercent={translationProgressPercent} wizardMissingKeysCount={wizardMissingKeys.length} onOpenTranslationWizard={openTranslationWizard} /> ()} setEnglishReferenceForKey={(key, enabled) => { setEnglishReferenceKey(activeLang, key, enabled); if (enabled) { handleChange(key, getEnglishValue(key)); } }} filteredNs={filteredNs} filteredGroups={filteredGroups} activeNamespacePanel={activeNamespacePanel} setActiveNamespacePanel={setActiveNamespacePanel} namespaceTranslationStats={namespaceTranslationStats} openedNamespacePanelRef={openedNamespacePanelRef} openFromPanelClickRef={openFromPanelClickRef} onBackToPanels={scrollToLanguageManagementHeader} onOpenCategoryManager={() => setShowCategoryManagerModal(true)} /> )}
{/* Scroll-to-top floating button */} {/* Scroll-to-save floating button */} {/* Scroll-to-save hint tooltip */} {isHintRendered && ( )} {/* Glassy save bar */} {isSaveBarRendered && (
{t('autofix.kd63c8219')}
)} setShowTranslationWizard(false)} onPrevious={goToPreviousWizardStep} onSkip={skipWizardStep} onNext={goToNextWizardStep} isSavingStep={isWizardSavingStep} /> setShowAddModal(false)} onAdd={handleAddLanguage} /> setDeleteTarget(null)} onDelete={handleDeleteLanguage} /> { setShowCategoryManagerModal(false); if (isPreferencesDirty) void savePreferences(); }} newCategoryLabel={newCategoryLabel} setNewCategoryLabel={setNewCategoryLabel} onCreateCategory={handleCreateCategory} uncategorizedNamespaces={uncategorizedNamespaces} categoriesWithKnownNamespaces={categoriesWithKnownNamespaces} namespaces={namespaces} assignNamespaceByCategory={assignNamespaceByCategory} setAssignNamespaceByCategory={setAssignNamespaceByCategory} expandedCategoryId={expandedCategoryId} setExpandedCategoryId={setExpandedCategoryId} dragNamespace={dragNamespace} setDragNamespace={setDragNamespace} addNamespaceToCategory={addNamespaceToCategory} removeNamespaceFromCategory={removeNamespaceFromCategory} deleteCategory={deleteCategory} /> setForceConvertToClient((prev) => !prev)} onRunFixSelected={handleRunFixSelected} onAddMissingKeys={addMissingKeys} /> ); }