201 lines
7.3 KiB
TypeScript
201 lines
7.3 KiB
TypeScript
'use client';
|
||
|
||
import type { RefObject } from 'react';
|
||
import { useMemo } from 'react';
|
||
import { useTranslation } from '../../../i18n/useTranslation';
|
||
|
||
type LanguageEntry = {
|
||
code: string;
|
||
name: string;
|
||
};
|
||
|
||
type Props = {
|
||
headerRef: RefObject<HTMLDivElement | null>;
|
||
totalKeys: number;
|
||
onScan: () => void;
|
||
isScanning: boolean;
|
||
isAutoFixing: boolean;
|
||
onBackToAdmin: () => void;
|
||
isDirty: boolean;
|
||
onSave: () => void;
|
||
saved: boolean;
|
||
saveError: string;
|
||
allLanguages: LanguageEntry[];
|
||
activeLang: string;
|
||
setActiveLang: (code: string) => void;
|
||
isBuiltin: (code: string) => boolean;
|
||
onDeleteLanguageRequest: (code: string) => void;
|
||
onOpenAddLanguage: () => void;
|
||
allTabStats: { total: number; translated: number; missing: number };
|
||
translationProgressPercent: number;
|
||
wizardMissingKeysCount: number;
|
||
onOpenTranslationWizard: () => void;
|
||
};
|
||
|
||
export default function LanguageManagementTopSection({
|
||
headerRef,
|
||
totalKeys,
|
||
onScan,
|
||
isScanning,
|
||
isAutoFixing,
|
||
onBackToAdmin,
|
||
isDirty,
|
||
onSave,
|
||
saved,
|
||
saveError,
|
||
allLanguages,
|
||
activeLang,
|
||
setActiveLang,
|
||
isBuiltin,
|
||
onDeleteLanguageRequest,
|
||
onOpenAddLanguage,
|
||
allTabStats,
|
||
translationProgressPercent,
|
||
wizardMissingKeysCount,
|
||
onOpenTranslationWizard,
|
||
}: Props) {
|
||
const { t } = useTranslation();
|
||
|
||
const prioritizedLanguages = useMemo(() => {
|
||
const byCode = new Map(allLanguages.map((lang) => [lang.code, lang]));
|
||
const english = byCode.get('en');
|
||
const german = byCode.get('de');
|
||
|
||
const rest = allLanguages
|
||
.filter((lang) => lang.code !== 'en' && lang.code !== 'de')
|
||
.sort((a, b) => a.name.localeCompare(b.name));
|
||
|
||
return [english, german, ...rest].filter((lang): lang is LanguageEntry => Boolean(lang));
|
||
}, [allLanguages]);
|
||
|
||
const englishLanguage = prioritizedLanguages.find((lang) => lang.code === 'en');
|
||
const germanLanguage = prioritizedLanguages.find((lang) => lang.code === 'de');
|
||
const otherLanguages = prioritizedLanguages.filter((lang) => lang.code !== 'en' && lang.code !== 'de');
|
||
|
||
const renderLanguageButton = (lang: LanguageEntry) => (
|
||
<button
|
||
key={lang.code}
|
||
onClick={() => setActiveLang(lang.code)}
|
||
className={`relative rounded-lg px-4 py-2 text-sm font-medium transition flex items-center gap-2 ${
|
||
activeLang === lang.code
|
||
? 'bg-[#1C2B4A] text-white shadow'
|
||
: 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
|
||
}`}
|
||
>
|
||
{lang.name}
|
||
<span className="text-xs opacity-60">({lang.code})</span>
|
||
{!isBuiltin(lang.code) && (
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onDeleteLanguageRequest(lang.code);
|
||
}}
|
||
title={t('autofix.k5fcc9b0e')}
|
||
className={`ml-1 inline-flex items-center justify-center rounded-full w-4 h-4 text-xs leading-none ${
|
||
activeLang === lang.code
|
||
? 'bg-white/20 hover:bg-white/40 text-white'
|
||
: 'bg-gray-200 hover:bg-red-100 text-gray-500 hover:text-red-600'
|
||
}`}
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</button>
|
||
);
|
||
|
||
return (
|
||
<>
|
||
<div ref={headerRef} className="flex items-center justify-between flex-wrap gap-4">
|
||
<div>
|
||
<h1 className="text-3xl font-bold text-[#1C2B4A]">{t('autofix.k346a2c64')}</h1>
|
||
<p className="text-sm text-gray-500 mt-1">
|
||
Manage UI translations. All {totalKeys} keys scanned from the English source file.
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-3 flex-wrap">
|
||
<button
|
||
onClick={onScan}
|
||
disabled={isScanning || isAutoFixing}
|
||
className="rounded-md border border-[#1C2B4A] text-[#1C2B4A] px-3 py-2 text-sm font-medium hover:bg-[#1C2B4A] hover:text-white transition-colors flex items-center gap-2 disabled:opacity-50"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||
</svg>
|
||
{isScanning ? 'Scanning...' : 'Scan & review fixes'}
|
||
</button>
|
||
<button
|
||
onClick={onBackToAdmin}
|
||
className="rounded-md border border-gray-300 px-3 py-2 text-sm hover:bg-gray-50"
|
||
>{t('autofix.kea7cde7a')}</button>
|
||
{isDirty && (
|
||
<button
|
||
onClick={onSave}
|
||
className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 text-sm font-semibold hover:bg-[#1C2B4A]/90"
|
||
>{t('autofix.k4be6f631')}</button>
|
||
)}
|
||
{saved && !isDirty && (
|
||
<span className="rounded-md bg-green-50 border border-green-200 text-green-700 px-3 py-2 text-sm font-medium">
|
||
Saved
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{saveError && (
|
||
<div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||
{saveError}
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
{englishLanguage && renderLanguageButton(englishLanguage)}
|
||
{germanLanguage && renderLanguageButton(germanLanguage)}
|
||
<button
|
||
onClick={onOpenAddLanguage}
|
||
className="rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-sm text-gray-500 hover:border-[#1C2B4A] hover:text-[#1C2B4A] transition"
|
||
>
|
||
+ Add language
|
||
</button>
|
||
{otherLanguages.map((lang) => renderLanguageButton(lang))}
|
||
</div>
|
||
|
||
{activeLang !== 'en' && (
|
||
<div className="rounded-xl border border-gray-200 bg-white p-4 flex items-center gap-4">
|
||
<div className="flex-1">
|
||
<div className="flex justify-between text-xs text-gray-500 mb-1">
|
||
<span>{t('autofix.kb8f33873')}</span>
|
||
<span>{allTabStats.translated} / {allTabStats.total} keys translated</span>
|
||
</div>
|
||
<div className="h-2 rounded-full bg-gray-100 overflow-hidden">
|
||
<div
|
||
className="h-full rounded-full bg-[#1C2B4A] transition-all"
|
||
style={{ width: `${translationProgressPercent}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<span className="text-lg font-bold text-[#1C2B4A]">
|
||
{translationProgressPercent}%
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
{activeLang !== 'en' && allTabStats.missing > 0 && wizardMissingKeysCount > 0 && (
|
||
<div className="rounded-xl border border-indigo-200 bg-indigo-50 p-4 flex items-start justify-between gap-4">
|
||
<div>
|
||
<p className="text-sm font-semibold text-indigo-900">{t('autofix.k5e5e8744')}</p>
|
||
<p className="text-xs text-indigo-800 mt-1">
|
||
{allLanguages.find((l) => l.code === activeLang)?.name ?? activeLang} still has {wizardMissingKeysCount} missing keys. Start the wizard to fill them step by step.
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-2 shrink-0">
|
||
<button
|
||
type="button"
|
||
onClick={onOpenTranslationWizard}
|
||
className="rounded-md bg-[#1C2B4A] text-white px-3 py-1.5 text-xs font-semibold hover:bg-[#1C2B4A]/90"
|
||
>{t('autofix.k725dd1d6')}</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
} |