feat: improve language switcher integration across all web apps

- Add language selector to PillNavigation in all apps
- Update LanguageSelector components to use shared-i18n utils
- Simplify AppSlider components
- Add getLanguageDropdownItems and getCurrentLanguageLabel utils

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-30 00:46:13 +01:00
parent 6bb8285349
commit 9a29a8e1ed
16 changed files with 198 additions and 64 deletions

View file

@ -9,6 +9,8 @@ pnpm dev:chat:app
pnpm dev:picture:app
pnpm dev:manacore:app
Übersicht aller wichtigen Befehle zum Starten, Stoppen und Verwalten der Apps.

View file

@ -1,20 +1,19 @@
<script lang="ts">
import { locale } from 'svelte-i18n';
import { LanguageSelector } from '@manacore/shared-i18n';
import { PillDropdown } from '@manacore/shared-ui';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import { theme } from '$lib/stores/theme';
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLabel = $derived(getCurrentLanguageLabel(currentLocale));
</script>
<LanguageSelector
{currentLocale}
{supportedLocales}
onLocaleChange={handleLocaleChange}
isDark={theme.isDark}
primaryColor="#0ea5e9"
/>
<PillDropdown items={languageItems} label={currentLabel} direction="down" />

View file

@ -2,6 +2,7 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { locale } from 'svelte-i18n';
import { authStore } from '$lib/stores/auth.svelte';
import { theme } from '$lib/stores/theme';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
@ -12,6 +13,8 @@
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { getPillAppItems } from '@manacore/shared-branding';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import type { LayoutData } from './$types';
// App switcher items
@ -49,6 +52,16 @@
// Current theme variant label
let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[theme.variant].label);
// Language selector items
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
// Navigation items for Chat (settings moved to user dropdown)
const navItems: PillNavItem[] = [
{ href: '/chat', label: 'Chat', icon: 'home' },
@ -180,7 +193,9 @@
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={false}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={true}
onLogout={handleLogout}
primaryColor="#3b82f6"

View file

@ -1,21 +1,19 @@
<script lang="ts">
import { locale } from 'svelte-i18n';
import { LanguageSelector } from '@manacore/shared-i18n';
import { PillDropdown } from '@manacore/shared-ui';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import { theme } from '$lib/stores/theme';
let isDark = $derived(theme.isDark);
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLabel = $derived(getCurrentLanguageLabel(currentLocale));
</script>
<LanguageSelector
{currentLocale}
{supportedLocales}
onLocaleChange={handleLocaleChange}
{isDark}
primaryColor="#6366f1"
/>
<PillDropdown items={languageItems} label={currentLabel} direction="down" />

View file

@ -3,9 +3,12 @@
import { page } from '$app/stores';
import type { Snippet } from 'svelte';
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/authStore.svelte';
import {
@ -47,6 +50,16 @@
// Current theme variant label
let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[theme.variant].label);
// Language selector items
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
// User email for user dropdown
let userEmail = $derived(authStore.user?.email);
@ -167,7 +180,9 @@
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={false}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={true}
primaryColor="#6366f1"
showAppSwitcher={true}

View file

@ -1,21 +1,19 @@
<script lang="ts">
import { locale } from 'svelte-i18n';
import { LanguageSelector } from '@manacore/shared-i18n';
import { PillDropdown } from '@manacore/shared-ui';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import { theme } from '$lib/stores/theme';
let isDark = $derived(theme.isDark);
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLabel = $derived(getCurrentLanguageLabel(currentLocale));
</script>
<LanguageSelector
{currentLocale}
{supportedLocales}
onLocaleChange={handleLocaleChange}
{isDark}
primaryColor="#8b5cf6"
/>
<PillDropdown items={languageItems} label={currentLabel} direction="down" />

View file

@ -2,6 +2,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { authStore } from '$lib/stores/authStore.svelte';
import { theme } from '$lib/stores/theme';
import {
@ -11,6 +12,8 @@
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
let { children } = $props();
@ -50,6 +53,16 @@
// Current theme variant label
let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[theme.variant].label);
// Language selector items
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
// Navigation shortcuts (Ctrl+1-5)
const navRoutes = navItems.map((item) => item.href);
@ -165,7 +178,9 @@
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={false}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
primaryColor="#6366f1"
/>

View file

@ -1,20 +1,19 @@
<script lang="ts">
import { locale } from 'svelte-i18n';
import { LanguageSelector } from '@manacore/shared-i18n';
import { PillDropdown } from '@manacore/shared-ui';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import { theme } from '$lib/stores/theme';
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLabel = $derived(getCurrentLanguageLabel(currentLocale));
</script>
<LanguageSelector
{currentLocale}
{supportedLocales}
onLocaleChange={handleLocaleChange}
isDark={theme.isDark}
primaryColor="#3b82f6"
/>
<PillDropdown items={languageItems} label={currentLabel} direction="down" />

View file

@ -2,9 +2,12 @@
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { locale } from 'svelte-i18n';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem, PillNavElement, PillDropdownItem } from '@manacore/shared-ui';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import KeyboardShortcutsModal from '$lib/components/ui/KeyboardShortcutsModal.svelte';
import { theme } from '$lib/stores/theme';
import { isUIVisible, toggleUI, showKeyboardShortcuts } from '$lib/stores/ui';
@ -97,6 +100,16 @@
// Current theme variant label
let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[theme.variant].label);
// Language selector items
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
// Elements (divider + view mode tabs)
let elements: PillNavElement[] = $derived([
{ type: 'divider' as const },
@ -209,7 +222,9 @@
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={false}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
primaryColor="#3b82f6"
/>
{/if}

View file

@ -1,20 +1,19 @@
<script lang="ts">
import { locale } from 'svelte-i18n';
import { LanguageSelector } from '@manacore/shared-i18n';
import { PillDropdown } from '@manacore/shared-ui';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import { theme } from '$lib/stores/theme';
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLabel = $derived(getCurrentLanguageLabel(currentLocale));
</script>
<LanguageSelector
{currentLocale}
{supportedLocales}
onLocaleChange={handleLocaleChange}
isDark={theme.isDark}
primaryColor="#f97316"
/>
<PillDropdown items={languageItems} label={currentLabel} direction="down" />

View file

@ -2,6 +2,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { auth } from '$lib/stores/auth.svelte';
@ -11,6 +12,8 @@
isSidebarMode as sidebarModeStore,
isNavCollapsed as collapsedStore,
} from '$lib/stores/navigation';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import '../app.css';
let { children } = $props();
@ -40,6 +43,16 @@
// Current theme variant label
let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[theme.variant].label);
// Language selector items
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
// Navigation items for Presi
const navItems: PillNavItem[] = [
{ href: '/', label: 'Decks', icon: 'document' },
@ -173,7 +186,9 @@
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={false}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={true}
onLogout={handleLogout}
primaryColor="#64748b"

View file

@ -1,20 +1,19 @@
<script lang="ts">
import { locale } from 'svelte-i18n';
import { LanguageSelector } from '@manacore/shared-i18n';
import { PillDropdown } from '@manacore/shared-ui';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import { theme } from '$lib/stores/theme';
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLabel = $derived(getCurrentLanguageLabel(currentLocale));
</script>
<LanguageSelector
{currentLocale}
{supportedLocales}
onLocaleChange={handleLocaleChange}
isDark={theme.isDark}
primaryColor="#f59e0b"
/>
<PillDropdown items={languageItems} label={currentLabel} direction="down" />

View file

@ -2,6 +2,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { theme } from '$lib/stores/theme';
@ -11,6 +12,8 @@
isSidebarMode as sidebarModeStore,
isNavCollapsed as collapsedStore,
} from '$lib/stores/navigation';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import ToastContainer from '$lib/components/ToastContainer.svelte';
import '../app.css';
@ -46,6 +49,16 @@
// Current theme variant label
let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[theme.variant].label);
// Language selector items
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
// Navigation items for Zitare
const navItems: PillNavItem[] = [
{ href: '/', label: 'Start', icon: 'home' },
@ -175,7 +188,9 @@
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={false}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
primaryColor="#f59e0b"

View file

@ -34,6 +34,10 @@ export {
formatRelativeTime,
getPluralCategory,
interpolate,
// PillDropdown language helpers
type LanguageDropdownItem,
getLanguageDropdownItems,
getCurrentLanguageLabel,
} from './utils';
// Common translations

View file

@ -2,7 +2,7 @@
* i18n utility functions
*/
import { type LanguageCode, isLanguageSupported } from './languages';
import { type LanguageCode, isLanguageSupported, getLanguageInfo } from './languages';
/**
* Detect user's preferred locale from browser
@ -241,3 +241,50 @@ export function interpolate(text: string, values: Record<string, string | number
return key in values ? String(values[key]) : match;
});
}
/**
* Interface for PillDropdown items (matches shared-ui type)
*/
export interface LanguageDropdownItem {
id: string;
label: string;
icon?: string;
onClick: () => void;
active?: boolean;
}
/**
* Create PillDropdown items for language selection
* @param supportedLocales - Array of supported locale codes (e.g., ['de', 'en', 'it', 'fr', 'es'])
* @param currentLocale - Currently selected locale
* @param onLocaleChange - Callback when locale changes
* @returns Array of items compatible with PillDropdown
*/
export function getLanguageDropdownItems(
supportedLocales: readonly string[],
currentLocale: string,
onLocaleChange: (locale: string) => void
): LanguageDropdownItem[] {
return supportedLocales.map((locale) => {
const info = getLanguageInfo(locale);
return {
id: locale,
label: info ? `${info.emoji} ${info.nativeName}` : locale.toUpperCase(),
onClick: () => onLocaleChange(locale),
active: currentLocale === locale,
};
});
}
/**
* Get current language label for PillDropdown trigger
* @param currentLocale - Currently selected locale
* @returns Label with flag emoji and language code (e.g., "🇩🇪 DE")
*/
export function getCurrentLanguageLabel(currentLocale: string): string {
const info = getLanguageInfo(currentLocale);
if (info) {
return `${info.emoji} ${currentLocale.toUpperCase()}`;
}
return currentLocale.toUpperCase();
}

View file

@ -338,7 +338,6 @@
items={languageItems}
direction="down"
label={currentLanguageLabel}
icon="globe"
/>
{/if}