Adds front-end set-up
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Languages } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheck,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
changeLanguage,
|
||||
LANGUAGE_LABELS,
|
||||
SUPPORTED_LANGUAGES,
|
||||
type SupportedLanguage,
|
||||
} from '@/i18n/config';
|
||||
|
||||
/** Language selector for the topbar; lazy-loads the chosen locale (Q4-B). */
|
||||
export function LanguageSwitcher() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const current = (i18n.resolvedLanguage ?? 'en') as SupportedLanguage;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={t('userMenu.language')}
|
||||
data-testid="language-switcher-button"
|
||||
>
|
||||
<Languages />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{t('userMenu.language')}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{SUPPORTED_LANGUAGES.map((lng) => (
|
||||
<DropdownMenuItem
|
||||
key={lng}
|
||||
onSelect={() => {
|
||||
void changeLanguage(lng);
|
||||
}}
|
||||
data-testid={`language-option-${lng}`}
|
||||
>
|
||||
{LANGUAGE_LABELS[lng]}
|
||||
<DropdownMenuCheck checked={current === lng} />
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import enTranslation from './locales/en/translation.json';
|
||||
|
||||
export const SUPPORTED_LANGUAGES = ['en', 'nl'] as const;
|
||||
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
|
||||
|
||||
export const LANGUAGE_LABELS: Record<SupportedLanguage, string> = {
|
||||
en: 'English',
|
||||
nl: 'Nederlands',
|
||||
};
|
||||
|
||||
// English (the fallback) is bundled eagerly so the UI never flashes raw keys.
|
||||
// Other locales are lazy-loaded on demand (Q4-B / NFR-U1-05).
|
||||
const loaded = new Set<string>(['en']);
|
||||
|
||||
async function loadLocale(lng: string): Promise<void> {
|
||||
if (loaded.has(lng) || !SUPPORTED_LANGUAGES.includes(lng as SupportedLanguage)) {
|
||||
return;
|
||||
}
|
||||
const module = await import(`./locales/${lng}/translation.json`);
|
||||
i18n.addResourceBundle(lng, 'translation', module.default, true, true);
|
||||
loaded.add(lng);
|
||||
}
|
||||
|
||||
void i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en: { translation: enTranslation },
|
||||
},
|
||||
fallbackLng: 'en',
|
||||
supportedLngs: SUPPORTED_LANGUAGES as unknown as string[],
|
||||
nonExplicitSupportedLngs: true,
|
||||
interpolation: { escapeValue: false },
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'],
|
||||
caches: ['localStorage'],
|
||||
},
|
||||
react: { useSuspense: false },
|
||||
});
|
||||
|
||||
// Ensure the detected language is loaded after init.
|
||||
void loadLocale(i18n.resolvedLanguage ?? 'en');
|
||||
|
||||
/** Lazy-load the target locale, then switch to it (persisted by the detector). */
|
||||
export async function changeLanguage(lng: SupportedLanguage): Promise<void> {
|
||||
await loadLocale(lng);
|
||||
await i18n.changeLanguage(lng);
|
||||
}
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"common": {
|
||||
"appName": "SlpModularCms",
|
||||
"loading": "Loading…",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"retry": "Retry"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
"users": "Users",
|
||||
"cms": "CMS"
|
||||
},
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"subtitle": "Sign in to your SlpModularCms account",
|
||||
"email": "Email",
|
||||
"emailPlaceholder": "you@example.com",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"submitting": "Signing in…",
|
||||
"errors": {
|
||||
"emailRequired": "Email is required",
|
||||
"emailInvalid": "Enter a valid email address",
|
||||
"passwordRequired": "Password is required",
|
||||
"invalidCredentials": "Invalid email or password",
|
||||
"generic": "Something went wrong. Please try again."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"welcome": "Welcome back, {{name}}",
|
||||
"placeholder": "Your dashboard widgets will appear here."
|
||||
},
|
||||
"userMenu": {
|
||||
"language": "Language",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"errors": {
|
||||
"network": "Unable to reach the server. Check your connection and try again."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"common": {
|
||||
"appName": "SlpModularCms",
|
||||
"loading": "Laden…",
|
||||
"cancel": "Annuleren",
|
||||
"save": "Opslaan",
|
||||
"retry": "Opnieuw"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
"users": "Gebruikers",
|
||||
"cms": "CMS"
|
||||
},
|
||||
"login": {
|
||||
"title": "Inloggen",
|
||||
"subtitle": "Log in op je SlpModularCms-account",
|
||||
"email": "E-mail",
|
||||
"emailPlaceholder": "jij@voorbeeld.nl",
|
||||
"password": "Wachtwoord",
|
||||
"submit": "Inloggen",
|
||||
"submitting": "Bezig met inloggen…",
|
||||
"errors": {
|
||||
"emailRequired": "E-mail is verplicht",
|
||||
"emailInvalid": "Voer een geldig e-mailadres in",
|
||||
"passwordRequired": "Wachtwoord is verplicht",
|
||||
"invalidCredentials": "Ongeldige e-mail of wachtwoord",
|
||||
"generic": "Er is iets misgegaan. Probeer het opnieuw."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"welcome": "Welkom terug, {{name}}",
|
||||
"placeholder": "Je dashboard-widgets verschijnen hier."
|
||||
},
|
||||
"userMenu": {
|
||||
"language": "Taal",
|
||||
"logout": "Uitloggen"
|
||||
},
|
||||
"errors": {
|
||||
"network": "Kan de server niet bereiken. Controleer je verbinding en probeer opnieuw."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user