diff --git a/apps/finance/apps/web/src/lib/i18n/index.ts b/apps/finance/apps/web/src/lib/i18n/index.ts
new file mode 100644
index 000000000..71b3c8119
--- /dev/null
+++ b/apps/finance/apps/web/src/lib/i18n/index.ts
@@ -0,0 +1,58 @@
+/**
+ * i18n setup for Finance app
+ * Supports: DE, EN, FR, ES, IT
+ */
+
+import { browser } from '$app/environment';
+import { init, register, locale, getLocaleFromNavigator } from 'svelte-i18n';
+
+// Supported locales
+export const supportedLocales = ['de', 'en', 'fr', 'es', 'it'] as const;
+export type SupportedLocale = (typeof supportedLocales)[number];
+
+// Register locales
+register('de', () => import('./locales/de.json'));
+register('en', () => import('./locales/en.json'));
+register('fr', () => import('./locales/fr.json'));
+register('es', () => import('./locales/es.json'));
+register('it', () => import('./locales/it.json'));
+
+// Get initial locale
+function getInitialLocale(): SupportedLocale {
+ if (browser) {
+ // Check localStorage first
+ const saved = localStorage.getItem('finance-locale');
+ if (saved && supportedLocales.includes(saved as SupportedLocale)) {
+ return saved as SupportedLocale;
+ }
+
+ // Fall back to browser language
+ const browserLocale = getLocaleFromNavigator();
+ if (browserLocale) {
+ const shortLocale = browserLocale.split('-')[0] as SupportedLocale;
+ if (supportedLocales.includes(shortLocale)) {
+ return shortLocale;
+ }
+ }
+ }
+
+ // Default to German
+ return 'de';
+}
+
+// Initialize i18n at module scope (required for SSR)
+init({
+ fallbackLocale: 'de',
+ initialLocale: getInitialLocale(),
+});
+
+// Set locale and persist
+export function setLocale(newLocale: SupportedLocale) {
+ locale.set(newLocale);
+ if (browser) {
+ localStorage.setItem('finance-locale', newLocale);
+ }
+}
+
+// Wait for locale to be loaded (useful for SSR)
+export { waitLocale } from 'svelte-i18n';
diff --git a/apps/finance/apps/web/src/lib/i18n/locales/de.json b/apps/finance/apps/web/src/lib/i18n/locales/de.json
new file mode 100644
index 000000000..99327f970
--- /dev/null
+++ b/apps/finance/apps/web/src/lib/i18n/locales/de.json
@@ -0,0 +1,133 @@
+{
+ "app": {
+ "name": "Finance",
+ "loading": "Laden..."
+ },
+ "nav": {
+ "dashboard": "Übersicht",
+ "accounts": "Konten",
+ "transactions": "Transaktionen",
+ "budgets": "Budgets",
+ "categories": "Kategorien",
+ "reports": "Berichte",
+ "settings": "Einstellungen",
+ "feedback": "Feedback"
+ },
+ "auth": {
+ "login": "Anmelden",
+ "register": "Registrieren",
+ "logout": "Abmelden",
+ "forgotPassword": "Passwort vergessen",
+ "email": "E-Mail",
+ "password": "Passwort",
+ "confirmPassword": "Passwort bestätigen"
+ },
+ "dashboard": {
+ "title": "Finanzübersicht",
+ "totalBalance": "Gesamtguthaben",
+ "income": "Einnahmen",
+ "expenses": "Ausgaben",
+ "savings": "Ersparnis",
+ "recentTransactions": "Letzte Transaktionen",
+ "budgetOverview": "Budget-Übersicht"
+ },
+ "accounts": {
+ "title": "Konten",
+ "add": "Konto hinzufügen",
+ "edit": "Konto bearbeiten",
+ "delete": "Konto löschen",
+ "name": "Kontoname",
+ "type": "Kontotyp",
+ "balance": "Kontostand",
+ "currency": "Währung",
+ "noAccounts": "Keine Konten vorhanden",
+ "types": {
+ "checking": "Girokonto",
+ "savings": "Sparkonto",
+ "credit": "Kreditkarte",
+ "cash": "Bargeld",
+ "investment": "Investment"
+ }
+ },
+ "transactions": {
+ "title": "Transaktionen",
+ "add": "Transaktion hinzufügen",
+ "edit": "Transaktion bearbeiten",
+ "delete": "Transaktion löschen",
+ "amount": "Betrag",
+ "date": "Datum",
+ "description": "Beschreibung",
+ "category": "Kategorie",
+ "account": "Konto",
+ "type": "Art",
+ "noTransactions": "Keine Transaktionen vorhanden",
+ "types": {
+ "income": "Einnahme",
+ "expense": "Ausgabe",
+ "transfer": "Überweisung"
+ }
+ },
+ "budgets": {
+ "title": "Budgets",
+ "add": "Budget hinzufügen",
+ "edit": "Budget bearbeiten",
+ "delete": "Budget löschen",
+ "name": "Budgetname",
+ "amount": "Betrag",
+ "spent": "Ausgegeben",
+ "remaining": "Verbleibend",
+ "period": "Zeitraum",
+ "category": "Kategorie",
+ "noBudgets": "Keine Budgets vorhanden",
+ "periods": {
+ "weekly": "Wöchentlich",
+ "monthly": "Monatlich",
+ "yearly": "Jährlich"
+ }
+ },
+ "categories": {
+ "title": "Kategorien",
+ "add": "Kategorie hinzufügen",
+ "edit": "Kategorie bearbeiten",
+ "delete": "Kategorie löschen",
+ "name": "Name",
+ "icon": "Symbol",
+ "color": "Farbe",
+ "noCategories": "Keine Kategorien vorhanden"
+ },
+ "reports": {
+ "title": "Berichte",
+ "incomeVsExpenses": "Einnahmen vs. Ausgaben",
+ "categoryBreakdown": "Aufschlüsselung nach Kategorien",
+ "trends": "Trends",
+ "export": "Exportieren"
+ },
+ "settings": {
+ "title": "Einstellungen",
+ "general": "Allgemein",
+ "appearance": "Darstellung",
+ "currency": "Standardwährung",
+ "language": "Sprache",
+ "theme": "Design",
+ "darkMode": "Dunkelmodus",
+ "notifications": "Benachrichtigungen"
+ },
+ "common": {
+ "save": "Speichern",
+ "cancel": "Abbrechen",
+ "delete": "Löschen",
+ "edit": "Bearbeiten",
+ "add": "Hinzufügen",
+ "confirm": "Bestätigen",
+ "yes": "Ja",
+ "no": "Nein",
+ "ok": "OK",
+ "loading": "Laden...",
+ "error": "Fehler",
+ "success": "Erfolg",
+ "back": "Zurück",
+ "search": "Suchen",
+ "filter": "Filtern",
+ "sort": "Sortieren"
+ }
+}
diff --git a/apps/finance/apps/web/src/lib/i18n/locales/en.json b/apps/finance/apps/web/src/lib/i18n/locales/en.json
new file mode 100644
index 000000000..ec6087d5b
--- /dev/null
+++ b/apps/finance/apps/web/src/lib/i18n/locales/en.json
@@ -0,0 +1,133 @@
+{
+ "app": {
+ "name": "Finance",
+ "loading": "Loading..."
+ },
+ "nav": {
+ "dashboard": "Dashboard",
+ "accounts": "Accounts",
+ "transactions": "Transactions",
+ "budgets": "Budgets",
+ "categories": "Categories",
+ "reports": "Reports",
+ "settings": "Settings",
+ "feedback": "Feedback"
+ },
+ "auth": {
+ "login": "Sign In",
+ "register": "Sign Up",
+ "logout": "Sign Out",
+ "forgotPassword": "Forgot Password",
+ "email": "Email",
+ "password": "Password",
+ "confirmPassword": "Confirm Password"
+ },
+ "dashboard": {
+ "title": "Financial Overview",
+ "totalBalance": "Total Balance",
+ "income": "Income",
+ "expenses": "Expenses",
+ "savings": "Savings",
+ "recentTransactions": "Recent Transactions",
+ "budgetOverview": "Budget Overview"
+ },
+ "accounts": {
+ "title": "Accounts",
+ "add": "Add Account",
+ "edit": "Edit Account",
+ "delete": "Delete Account",
+ "name": "Account Name",
+ "type": "Account Type",
+ "balance": "Balance",
+ "currency": "Currency",
+ "noAccounts": "No accounts yet",
+ "types": {
+ "checking": "Checking",
+ "savings": "Savings",
+ "credit": "Credit Card",
+ "cash": "Cash",
+ "investment": "Investment"
+ }
+ },
+ "transactions": {
+ "title": "Transactions",
+ "add": "Add Transaction",
+ "edit": "Edit Transaction",
+ "delete": "Delete Transaction",
+ "amount": "Amount",
+ "date": "Date",
+ "description": "Description",
+ "category": "Category",
+ "account": "Account",
+ "type": "Type",
+ "noTransactions": "No transactions yet",
+ "types": {
+ "income": "Income",
+ "expense": "Expense",
+ "transfer": "Transfer"
+ }
+ },
+ "budgets": {
+ "title": "Budgets",
+ "add": "Add Budget",
+ "edit": "Edit Budget",
+ "delete": "Delete Budget",
+ "name": "Budget Name",
+ "amount": "Amount",
+ "spent": "Spent",
+ "remaining": "Remaining",
+ "period": "Period",
+ "category": "Category",
+ "noBudgets": "No budgets yet",
+ "periods": {
+ "weekly": "Weekly",
+ "monthly": "Monthly",
+ "yearly": "Yearly"
+ }
+ },
+ "categories": {
+ "title": "Categories",
+ "add": "Add Category",
+ "edit": "Edit Category",
+ "delete": "Delete Category",
+ "name": "Name",
+ "icon": "Icon",
+ "color": "Color",
+ "noCategories": "No categories yet"
+ },
+ "reports": {
+ "title": "Reports",
+ "incomeVsExpenses": "Income vs. Expenses",
+ "categoryBreakdown": "Category Breakdown",
+ "trends": "Trends",
+ "export": "Export"
+ },
+ "settings": {
+ "title": "Settings",
+ "general": "General",
+ "appearance": "Appearance",
+ "currency": "Default Currency",
+ "language": "Language",
+ "theme": "Theme",
+ "darkMode": "Dark Mode",
+ "notifications": "Notifications"
+ },
+ "common": {
+ "save": "Save",
+ "cancel": "Cancel",
+ "delete": "Delete",
+ "edit": "Edit",
+ "add": "Add",
+ "confirm": "Confirm",
+ "yes": "Yes",
+ "no": "No",
+ "ok": "OK",
+ "loading": "Loading...",
+ "error": "Error",
+ "success": "Success",
+ "back": "Back",
+ "search": "Search",
+ "filter": "Filter",
+ "sort": "Sort"
+ }
+}
diff --git a/apps/finance/apps/web/src/lib/i18n/locales/es.json b/apps/finance/apps/web/src/lib/i18n/locales/es.json
new file mode 100644
index 000000000..e8e3e2add
--- /dev/null
+++ b/apps/finance/apps/web/src/lib/i18n/locales/es.json
@@ -0,0 +1,133 @@
+{
+ "app": {
+ "name": "Finance",
+ "loading": "Cargando..."
+ },
+ "nav": {
+ "dashboard": "Panel",
+ "accounts": "Cuentas",
+ "transactions": "Transacciones",
+ "budgets": "Presupuestos",
+ "categories": "Categorías",
+ "reports": "Informes",
+ "settings": "Configuración",
+ "feedback": "Feedback"
+ },
+ "auth": {
+ "login": "Iniciar sesión",
+ "register": "Registrarse",
+ "logout": "Cerrar sesión",
+ "forgotPassword": "Olvidé mi contraseña",
+ "email": "Correo electrónico",
+ "password": "Contraseña",
+ "confirmPassword": "Confirmar contraseña"
+ },
+ "dashboard": {
+ "title": "Resumen financiero",
+ "totalBalance": "Saldo total",
+ "income": "Ingresos",
+ "expenses": "Gastos",
+ "savings": "Ahorros",
+ "recentTransactions": "Transacciones recientes",
+ "budgetOverview": "Resumen de presupuesto"
+ },
+ "accounts": {
+ "title": "Cuentas",
+ "add": "Añadir cuenta",
+ "edit": "Editar cuenta",
+ "delete": "Eliminar cuenta",
+ "name": "Nombre de cuenta",
+ "type": "Tipo de cuenta",
+ "balance": "Saldo",
+ "currency": "Moneda",
+ "noAccounts": "Sin cuentas",
+ "types": {
+ "checking": "Cuenta corriente",
+ "savings": "Cuenta de ahorro",
+ "credit": "Tarjeta de crédito",
+ "cash": "Efectivo",
+ "investment": "Inversión"
+ }
+ },
+ "transactions": {
+ "title": "Transacciones",
+ "add": "Añadir transacción",
+ "edit": "Editar transacción",
+ "delete": "Eliminar transacción",
+ "amount": "Importe",
+ "date": "Fecha",
+ "description": "Descripción",
+ "category": "Categoría",
+ "account": "Cuenta",
+ "type": "Tipo",
+ "noTransactions": "Sin transacciones",
+ "types": {
+ "income": "Ingreso",
+ "expense": "Gasto",
+ "transfer": "Transferencia"
+ }
+ },
+ "budgets": {
+ "title": "Presupuestos",
+ "add": "Añadir presupuesto",
+ "edit": "Editar presupuesto",
+ "delete": "Eliminar presupuesto",
+ "name": "Nombre del presupuesto",
+ "amount": "Importe",
+ "spent": "Gastado",
+ "remaining": "Restante",
+ "period": "Período",
+ "category": "Categoría",
+ "noBudgets": "Sin presupuestos",
+ "periods": {
+ "weekly": "Semanal",
+ "monthly": "Mensual",
+ "yearly": "Anual"
+ }
+ },
+ "categories": {
+ "title": "Categorías",
+ "add": "Añadir categoría",
+ "edit": "Editar categoría",
+ "delete": "Eliminar categoría",
+ "name": "Nombre",
+ "icon": "Icono",
+ "color": "Color",
+ "noCategories": "Sin categorías"
+ },
+ "reports": {
+ "title": "Informes",
+ "incomeVsExpenses": "Ingresos vs. Gastos",
+ "categoryBreakdown": "Desglose por categoría",
+ "trends": "Tendencias",
+ "export": "Exportar"
+ },
+ "settings": {
+ "title": "Configuración",
+ "general": "General",
+ "appearance": "Apariencia",
+ "currency": "Moneda predeterminada",
+ "language": "Idioma",
+ "theme": "Tema",
+ "darkMode": "Modo oscuro",
+ "notifications": "Notificaciones"
+ },
+ "common": {
+ "save": "Guardar",
+ "cancel": "Cancelar",
+ "delete": "Eliminar",
+ "edit": "Editar",
+ "add": "Añadir",
+ "confirm": "Confirmar",
+ "yes": "Sí",
+ "no": "No",
+ "ok": "OK",
+ "loading": "Cargando...",
+ "error": "Error",
+ "success": "Éxito",
+ "back": "Atrás",
+ "search": "Buscar",
+ "filter": "Filtrar",
+ "sort": "Ordenar"
+ }
+}
diff --git a/apps/finance/apps/web/src/lib/i18n/locales/fr.json b/apps/finance/apps/web/src/lib/i18n/locales/fr.json
new file mode 100644
index 000000000..ad115d8fb
--- /dev/null
+++ b/apps/finance/apps/web/src/lib/i18n/locales/fr.json
@@ -0,0 +1,133 @@
+{
+ "app": {
+ "name": "Finance",
+ "loading": "Chargement..."
+ },
+ "nav": {
+ "dashboard": "Tableau de bord",
+ "accounts": "Comptes",
+ "transactions": "Transactions",
+ "budgets": "Budgets",
+ "categories": "Catégories",
+ "reports": "Rapports",
+ "settings": "Paramètres",
+ "feedback": "Feedback"
+ },
+ "auth": {
+ "login": "Se connecter",
+ "register": "S'inscrire",
+ "logout": "Se déconnecter",
+ "forgotPassword": "Mot de passe oublié",
+ "email": "E-mail",
+ "password": "Mot de passe",
+ "confirmPassword": "Confirmer le mot de passe"
+ },
+ "dashboard": {
+ "title": "Aperçu financier",
+ "totalBalance": "Solde total",
+ "income": "Revenus",
+ "expenses": "Dépenses",
+ "savings": "Épargne",
+ "recentTransactions": "Transactions récentes",
+ "budgetOverview": "Aperçu du budget"
+ },
+ "accounts": {
+ "title": "Comptes",
+ "add": "Ajouter un compte",
+ "edit": "Modifier le compte",
+ "delete": "Supprimer le compte",
+ "name": "Nom du compte",
+ "type": "Type de compte",
+ "balance": "Solde",
+ "currency": "Devise",
+ "noAccounts": "Aucun compte",
+ "types": {
+ "checking": "Compte courant",
+ "savings": "Compte épargne",
+ "credit": "Carte de crédit",
+ "cash": "Espèces",
+ "investment": "Investissement"
+ }
+ },
+ "transactions": {
+ "title": "Transactions",
+ "add": "Ajouter une transaction",
+ "edit": "Modifier la transaction",
+ "delete": "Supprimer la transaction",
+ "amount": "Montant",
+ "date": "Date",
+ "description": "Description",
+ "category": "Catégorie",
+ "account": "Compte",
+ "type": "Type",
+ "noTransactions": "Aucune transaction",
+ "types": {
+ "income": "Revenu",
+ "expense": "Dépense",
+ "transfer": "Virement"
+ }
+ },
+ "budgets": {
+ "title": "Budgets",
+ "add": "Ajouter un budget",
+ "edit": "Modifier le budget",
+ "delete": "Supprimer le budget",
+ "name": "Nom du budget",
+ "amount": "Montant",
+ "spent": "Dépensé",
+ "remaining": "Restant",
+ "period": "Période",
+ "category": "Catégorie",
+ "noBudgets": "Aucun budget",
+ "periods": {
+ "weekly": "Hebdomadaire",
+ "monthly": "Mensuel",
+ "yearly": "Annuel"
+ }
+ },
+ "categories": {
+ "title": "Catégories",
+ "add": "Ajouter une catégorie",
+ "edit": "Modifier la catégorie",
+ "delete": "Supprimer la catégorie",
+ "name": "Nom",
+ "icon": "Icône",
+ "color": "Couleur",
+ "noCategories": "Aucune catégorie"
+ },
+ "reports": {
+ "title": "Rapports",
+ "incomeVsExpenses": "Revenus vs. Dépenses",
+ "categoryBreakdown": "Répartition par catégorie",
+ "trends": "Tendances",
+ "export": "Exporter"
+ },
+ "settings": {
+ "title": "Paramètres",
+ "general": "Général",
+ "appearance": "Apparence",
+ "currency": "Devise par défaut",
+ "language": "Langue",
+ "theme": "Thème",
+ "darkMode": "Mode sombre",
+ "notifications": "Notifications"
+ },
+ "common": {
+ "save": "Enregistrer",
+ "cancel": "Annuler",
+ "delete": "Supprimer",
+ "edit": "Modifier",
+ "add": "Ajouter",
+ "confirm": "Confirmer",
+ "yes": "Oui",
+ "no": "Non",
+ "ok": "OK",
+ "loading": "Chargement...",
+ "error": "Erreur",
+ "success": "Succès",
+ "back": "Retour",
+ "search": "Rechercher",
+ "filter": "Filtrer",
+ "sort": "Trier"
+ }
+}
diff --git a/apps/finance/apps/web/src/lib/i18n/locales/it.json b/apps/finance/apps/web/src/lib/i18n/locales/it.json
new file mode 100644
index 000000000..53b977989
--- /dev/null
+++ b/apps/finance/apps/web/src/lib/i18n/locales/it.json
@@ -0,0 +1,133 @@
+{
+ "app": {
+ "name": "Finance",
+ "loading": "Caricamento..."
+ },
+ "nav": {
+ "dashboard": "Panoramica",
+ "accounts": "Conti",
+ "transactions": "Transazioni",
+ "budgets": "Budget",
+ "categories": "Categorie",
+ "reports": "Report",
+ "settings": "Impostazioni",
+ "feedback": "Feedback"
+ },
+ "auth": {
+ "login": "Accedi",
+ "register": "Registrati",
+ "logout": "Esci",
+ "forgotPassword": "Password dimenticata",
+ "email": "E-mail",
+ "password": "Password",
+ "confirmPassword": "Conferma password"
+ },
+ "dashboard": {
+ "title": "Panoramica finanziaria",
+ "totalBalance": "Saldo totale",
+ "income": "Entrate",
+ "expenses": "Spese",
+ "savings": "Risparmi",
+ "recentTransactions": "Transazioni recenti",
+ "budgetOverview": "Panoramica budget"
+ },
+ "accounts": {
+ "title": "Conti",
+ "add": "Aggiungi conto",
+ "edit": "Modifica conto",
+ "delete": "Elimina conto",
+ "name": "Nome conto",
+ "type": "Tipo conto",
+ "balance": "Saldo",
+ "currency": "Valuta",
+ "noAccounts": "Nessun conto",
+ "types": {
+ "checking": "Conto corrente",
+ "savings": "Conto risparmio",
+ "credit": "Carta di credito",
+ "cash": "Contanti",
+ "investment": "Investimento"
+ }
+ },
+ "transactions": {
+ "title": "Transazioni",
+ "add": "Aggiungi transazione",
+ "edit": "Modifica transazione",
+ "delete": "Elimina transazione",
+ "amount": "Importo",
+ "date": "Data",
+ "description": "Descrizione",
+ "category": "Categoria",
+ "account": "Conto",
+ "type": "Tipo",
+ "noTransactions": "Nessuna transazione",
+ "types": {
+ "income": "Entrata",
+ "expense": "Spesa",
+ "transfer": "Trasferimento"
+ }
+ },
+ "budgets": {
+ "title": "Budget",
+ "add": "Aggiungi budget",
+ "edit": "Modifica budget",
+ "delete": "Elimina budget",
+ "name": "Nome budget",
+ "amount": "Importo",
+ "spent": "Speso",
+ "remaining": "Rimanente",
+ "period": "Periodo",
+ "category": "Categoria",
+ "noBudgets": "Nessun budget",
+ "periods": {
+ "weekly": "Settimanale",
+ "monthly": "Mensile",
+ "yearly": "Annuale"
+ }
+ },
+ "categories": {
+ "title": "Categorie",
+ "add": "Aggiungi categoria",
+ "edit": "Modifica categoria",
+ "delete": "Elimina categoria",
+ "name": "Nome",
+ "icon": "Icona",
+ "color": "Colore",
+ "noCategories": "Nessuna categoria"
+ },
+ "reports": {
+ "title": "Report",
+ "incomeVsExpenses": "Entrate vs. Spese",
+ "categoryBreakdown": "Suddivisione per categoria",
+ "trends": "Tendenze",
+ "export": "Esporta"
+ },
+ "settings": {
+ "title": "Impostazioni",
+ "general": "Generale",
+ "appearance": "Aspetto",
+ "currency": "Valuta predefinita",
+ "language": "Lingua",
+ "theme": "Tema",
+ "darkMode": "Modalità scura",
+ "notifications": "Notifiche"
+ },
+ "common": {
+ "save": "Salva",
+ "cancel": "Annulla",
+ "delete": "Elimina",
+ "edit": "Modifica",
+ "add": "Aggiungi",
+ "confirm": "Conferma",
+ "yes": "Sì",
+ "no": "No",
+ "ok": "OK",
+ "loading": "Caricamento...",
+ "error": "Errore",
+ "success": "Successo",
+ "back": "Indietro",
+ "search": "Cerca",
+ "filter": "Filtra",
+ "sort": "Ordina"
+ }
+}
diff --git a/apps/finance/apps/web/src/routes/+layout.svelte b/apps/finance/apps/web/src/routes/+layout.svelte
index 00ab8dabb..d4c00e325 100644
--- a/apps/finance/apps/web/src/routes/+layout.svelte
+++ b/apps/finance/apps/web/src/routes/+layout.svelte
@@ -1,5 +1,6 @@
-
- Feedback | Finance
-
-
-
-
Feedback
-
- {#if success}
-
-
✅
-
Vielen Dank für Ihr Feedback!
-
Wir werden uns Ihre Nachricht ansehen.
-
- Zurück zur Startseite
-
-
- {:else}
-
- {/if}
-
+
diff --git a/apps/mail/apps/web/src/lib/i18n/index.ts b/apps/mail/apps/web/src/lib/i18n/index.ts
new file mode 100644
index 000000000..e3f3987e8
--- /dev/null
+++ b/apps/mail/apps/web/src/lib/i18n/index.ts
@@ -0,0 +1,58 @@
+/**
+ * i18n setup for Mail app
+ * Supports: DE, EN, FR, ES, IT
+ */
+
+import { browser } from '$app/environment';
+import { init, register, locale, getLocaleFromNavigator } from 'svelte-i18n';
+
+// Supported locales
+export const supportedLocales = ['de', 'en', 'fr', 'es', 'it'] as const;
+export type SupportedLocale = (typeof supportedLocales)[number];
+
+// Register locales
+register('de', () => import('./locales/de.json'));
+register('en', () => import('./locales/en.json'));
+register('fr', () => import('./locales/fr.json'));
+register('es', () => import('./locales/es.json'));
+register('it', () => import('./locales/it.json'));
+
+// Get initial locale
+function getInitialLocale(): SupportedLocale {
+ if (browser) {
+ // Check localStorage first
+ const saved = localStorage.getItem('mail-locale');
+ if (saved && supportedLocales.includes(saved as SupportedLocale)) {
+ return saved as SupportedLocale;
+ }
+
+ // Fall back to browser language
+ const browserLocale = getLocaleFromNavigator();
+ if (browserLocale) {
+ const shortLocale = browserLocale.split('-')[0] as SupportedLocale;
+ if (supportedLocales.includes(shortLocale)) {
+ return shortLocale;
+ }
+ }
+ }
+
+ // Default to German
+ return 'de';
+}
+
+// Initialize i18n at module scope (required for SSR)
+init({
+ fallbackLocale: 'de',
+ initialLocale: getInitialLocale(),
+});
+
+// Set locale and persist
+export function setLocale(newLocale: SupportedLocale) {
+ locale.set(newLocale);
+ if (browser) {
+ localStorage.setItem('mail-locale', newLocale);
+ }
+}
+
+// Wait for locale to be loaded (useful for SSR)
+export { waitLocale } from 'svelte-i18n';
diff --git a/apps/mail/apps/web/src/lib/i18n/locales/de.json b/apps/mail/apps/web/src/lib/i18n/locales/de.json
new file mode 100644
index 000000000..494726a68
--- /dev/null
+++ b/apps/mail/apps/web/src/lib/i18n/locales/de.json
@@ -0,0 +1,105 @@
+{
+ "app": {
+ "name": "Mail",
+ "loading": "Laden..."
+ },
+ "nav": {
+ "inbox": "Posteingang",
+ "sent": "Gesendet",
+ "drafts": "Entwürfe",
+ "starred": "Markiert",
+ "archive": "Archiv",
+ "trash": "Papierkorb",
+ "spam": "Spam",
+ "settings": "Einstellungen",
+ "feedback": "Feedback"
+ },
+ "auth": {
+ "login": "Anmelden",
+ "register": "Registrieren",
+ "logout": "Abmelden",
+ "forgotPassword": "Passwort vergessen",
+ "email": "E-Mail",
+ "password": "Passwort",
+ "confirmPassword": "Passwort bestätigen"
+ },
+ "email": {
+ "compose": "Neue E-Mail",
+ "reply": "Antworten",
+ "replyAll": "Allen antworten",
+ "forward": "Weiterleiten",
+ "delete": "Löschen",
+ "archive": "Archivieren",
+ "markRead": "Als gelesen markieren",
+ "markUnread": "Als ungelesen markieren",
+ "star": "Mit Stern markieren",
+ "unstar": "Stern entfernen",
+ "to": "An",
+ "cc": "CC",
+ "bcc": "BCC",
+ "subject": "Betreff",
+ "body": "Nachricht",
+ "send": "Senden",
+ "saveDraft": "Als Entwurf speichern",
+ "discard": "Verwerfen",
+ "attachments": "Anhänge",
+ "addAttachment": "Anhang hinzufügen",
+ "noEmails": "Keine E-Mails",
+ "noSubject": "(Kein Betreff)",
+ "from": "Von",
+ "date": "Datum"
+ },
+ "folders": {
+ "title": "Ordner",
+ "add": "Ordner hinzufügen",
+ "edit": "Ordner bearbeiten",
+ "delete": "Ordner löschen",
+ "name": "Ordnername"
+ },
+ "labels": {
+ "title": "Labels",
+ "add": "Label hinzufügen",
+ "edit": "Label bearbeiten",
+ "delete": "Label löschen",
+ "name": "Labelname",
+ "color": "Farbe"
+ },
+ "accounts": {
+ "title": "E-Mail-Konten",
+ "add": "Konto hinzufügen",
+ "edit": "Konto bearbeiten",
+ "delete": "Konto löschen",
+ "name": "Kontoname",
+ "address": "E-Mail-Adresse",
+ "default": "Standardkonto"
+ },
+ "settings": {
+ "title": "Einstellungen",
+ "general": "Allgemein",
+ "appearance": "Darstellung",
+ "accounts": "Konten",
+ "signature": "Signatur",
+ "language": "Sprache",
+ "theme": "Design",
+ "darkMode": "Dunkelmodus",
+ "notifications": "Benachrichtigungen"
+ },
+ "common": {
+ "save": "Speichern",
+ "cancel": "Abbrechen",
+ "delete": "Löschen",
+ "edit": "Bearbeiten",
+ "add": "Hinzufügen",
+ "confirm": "Bestätigen",
+ "yes": "Ja",
+ "no": "Nein",
+ "ok": "OK",
+ "loading": "Laden...",
+ "error": "Fehler",
+ "success": "Erfolg",
+ "back": "Zurück",
+ "search": "Suchen",
+ "select": "Auswählen",
+ "selectAll": "Alle auswählen"
+ }
+}
diff --git a/apps/mail/apps/web/src/lib/i18n/locales/en.json b/apps/mail/apps/web/src/lib/i18n/locales/en.json
new file mode 100644
index 000000000..5810f0513
--- /dev/null
+++ b/apps/mail/apps/web/src/lib/i18n/locales/en.json
@@ -0,0 +1,105 @@
+{
+ "app": {
+ "name": "Mail",
+ "loading": "Loading..."
+ },
+ "nav": {
+ "inbox": "Inbox",
+ "sent": "Sent",
+ "drafts": "Drafts",
+ "starred": "Starred",
+ "archive": "Archive",
+ "trash": "Trash",
+ "spam": "Spam",
+ "settings": "Settings",
+ "feedback": "Feedback"
+ },
+ "auth": {
+ "login": "Sign In",
+ "register": "Sign Up",
+ "logout": "Sign Out",
+ "forgotPassword": "Forgot Password",
+ "email": "Email",
+ "password": "Password",
+ "confirmPassword": "Confirm Password"
+ },
+ "email": {
+ "compose": "Compose",
+ "reply": "Reply",
+ "replyAll": "Reply All",
+ "forward": "Forward",
+ "delete": "Delete",
+ "archive": "Archive",
+ "markRead": "Mark as read",
+ "markUnread": "Mark as unread",
+ "star": "Star",
+ "unstar": "Unstar",
+ "to": "To",
+ "cc": "CC",
+ "bcc": "BCC",
+ "subject": "Subject",
+ "body": "Message",
+ "send": "Send",
+ "saveDraft": "Save as draft",
+ "discard": "Discard",
+ "attachments": "Attachments",
+ "addAttachment": "Add attachment",
+ "noEmails": "No emails",
+ "noSubject": "(No subject)",
+ "from": "From",
+ "date": "Date"
+ },
+ "folders": {
+ "title": "Folders",
+ "add": "Add Folder",
+ "edit": "Edit Folder",
+ "delete": "Delete Folder",
+ "name": "Folder name"
+ },
+ "labels": {
+ "title": "Labels",
+ "add": "Add Label",
+ "edit": "Edit Label",
+ "delete": "Delete Label",
+ "name": "Label name",
+ "color": "Color"
+ },
+ "accounts": {
+ "title": "Email Accounts",
+ "add": "Add Account",
+ "edit": "Edit Account",
+ "delete": "Delete Account",
+ "name": "Account name",
+ "address": "Email address",
+ "default": "Default account"
+ },
+ "settings": {
+ "title": "Settings",
+ "general": "General",
+ "appearance": "Appearance",
+ "accounts": "Accounts",
+ "signature": "Signature",
+ "language": "Language",
+ "theme": "Theme",
+ "darkMode": "Dark Mode",
+ "notifications": "Notifications"
+ },
+ "common": {
+ "save": "Save",
+ "cancel": "Cancel",
+ "delete": "Delete",
+ "edit": "Edit",
+ "add": "Add",
+ "confirm": "Confirm",
+ "yes": "Yes",
+ "no": "No",
+ "ok": "OK",
+ "loading": "Loading...",
+ "error": "Error",
+ "success": "Success",
+ "back": "Back",
+ "search": "Search",
+ "select": "Select",
+ "selectAll": "Select all"
+ }
+}
diff --git a/apps/mail/apps/web/src/lib/i18n/locales/es.json b/apps/mail/apps/web/src/lib/i18n/locales/es.json
new file mode 100644
index 000000000..851017ca4
--- /dev/null
+++ b/apps/mail/apps/web/src/lib/i18n/locales/es.json
@@ -0,0 +1,105 @@
+{
+ "app": {
+ "name": "Mail",
+ "loading": "Cargando..."
+ },
+ "nav": {
+ "inbox": "Bandeja de entrada",
+ "sent": "Enviados",
+ "drafts": "Borradores",
+ "starred": "Destacados",
+ "archive": "Archivo",
+ "trash": "Papelera",
+ "spam": "Spam",
+ "settings": "Configuración",
+ "feedback": "Feedback"
+ },
+ "auth": {
+ "login": "Iniciar sesión",
+ "register": "Registrarse",
+ "logout": "Cerrar sesión",
+ "forgotPassword": "Olvidé mi contraseña",
+ "email": "Correo electrónico",
+ "password": "Contraseña",
+ "confirmPassword": "Confirmar contraseña"
+ },
+ "email": {
+ "compose": "Redactar",
+ "reply": "Responder",
+ "replyAll": "Responder a todos",
+ "forward": "Reenviar",
+ "delete": "Eliminar",
+ "archive": "Archivar",
+ "markRead": "Marcar como leído",
+ "markUnread": "Marcar como no leído",
+ "star": "Destacar",
+ "unstar": "Quitar destacado",
+ "to": "Para",
+ "cc": "CC",
+ "bcc": "CCO",
+ "subject": "Asunto",
+ "body": "Mensaje",
+ "send": "Enviar",
+ "saveDraft": "Guardar como borrador",
+ "discard": "Descartar",
+ "attachments": "Adjuntos",
+ "addAttachment": "Añadir adjunto",
+ "noEmails": "Sin correos",
+ "noSubject": "(Sin asunto)",
+ "from": "De",
+ "date": "Fecha"
+ },
+ "folders": {
+ "title": "Carpetas",
+ "add": "Añadir carpeta",
+ "edit": "Editar carpeta",
+ "delete": "Eliminar carpeta",
+ "name": "Nombre de carpeta"
+ },
+ "labels": {
+ "title": "Etiquetas",
+ "add": "Añadir etiqueta",
+ "edit": "Editar etiqueta",
+ "delete": "Eliminar etiqueta",
+ "name": "Nombre de etiqueta",
+ "color": "Color"
+ },
+ "accounts": {
+ "title": "Cuentas de correo",
+ "add": "Añadir cuenta",
+ "edit": "Editar cuenta",
+ "delete": "Eliminar cuenta",
+ "name": "Nombre de cuenta",
+ "address": "Dirección de correo",
+ "default": "Cuenta predeterminada"
+ },
+ "settings": {
+ "title": "Configuración",
+ "general": "General",
+ "appearance": "Apariencia",
+ "accounts": "Cuentas",
+ "signature": "Firma",
+ "language": "Idioma",
+ "theme": "Tema",
+ "darkMode": "Modo oscuro",
+ "notifications": "Notificaciones"
+ },
+ "common": {
+ "save": "Guardar",
+ "cancel": "Cancelar",
+ "delete": "Eliminar",
+ "edit": "Editar",
+ "add": "Añadir",
+ "confirm": "Confirmar",
+ "yes": "Sí",
+ "no": "No",
+ "ok": "OK",
+ "loading": "Cargando...",
+ "error": "Error",
+ "success": "Éxito",
+ "back": "Atrás",
+ "search": "Buscar",
+ "select": "Seleccionar",
+ "selectAll": "Seleccionar todo"
+ }
+}
diff --git a/apps/mail/apps/web/src/lib/i18n/locales/fr.json b/apps/mail/apps/web/src/lib/i18n/locales/fr.json
new file mode 100644
index 000000000..756306d05
--- /dev/null
+++ b/apps/mail/apps/web/src/lib/i18n/locales/fr.json
@@ -0,0 +1,105 @@
+{
+ "app": {
+ "name": "Mail",
+ "loading": "Chargement..."
+ },
+ "nav": {
+ "inbox": "Boîte de réception",
+ "sent": "Envoyés",
+ "drafts": "Brouillons",
+ "starred": "Favoris",
+ "archive": "Archives",
+ "trash": "Corbeille",
+ "spam": "Spam",
+ "settings": "Paramètres",
+ "feedback": "Feedback"
+ },
+ "auth": {
+ "login": "Se connecter",
+ "register": "S'inscrire",
+ "logout": "Se déconnecter",
+ "forgotPassword": "Mot de passe oublié",
+ "email": "E-mail",
+ "password": "Mot de passe",
+ "confirmPassword": "Confirmer le mot de passe"
+ },
+ "email": {
+ "compose": "Nouveau message",
+ "reply": "Répondre",
+ "replyAll": "Répondre à tous",
+ "forward": "Transférer",
+ "delete": "Supprimer",
+ "archive": "Archiver",
+ "markRead": "Marquer comme lu",
+ "markUnread": "Marquer comme non lu",
+ "star": "Ajouter aux favoris",
+ "unstar": "Retirer des favoris",
+ "to": "À",
+ "cc": "CC",
+ "bcc": "CCI",
+ "subject": "Objet",
+ "body": "Message",
+ "send": "Envoyer",
+ "saveDraft": "Enregistrer comme brouillon",
+ "discard": "Annuler",
+ "attachments": "Pièces jointes",
+ "addAttachment": "Ajouter une pièce jointe",
+ "noEmails": "Aucun e-mail",
+ "noSubject": "(Sans objet)",
+ "from": "De",
+ "date": "Date"
+ },
+ "folders": {
+ "title": "Dossiers",
+ "add": "Ajouter un dossier",
+ "edit": "Modifier le dossier",
+ "delete": "Supprimer le dossier",
+ "name": "Nom du dossier"
+ },
+ "labels": {
+ "title": "Libellés",
+ "add": "Ajouter un libellé",
+ "edit": "Modifier le libellé",
+ "delete": "Supprimer le libellé",
+ "name": "Nom du libellé",
+ "color": "Couleur"
+ },
+ "accounts": {
+ "title": "Comptes e-mail",
+ "add": "Ajouter un compte",
+ "edit": "Modifier le compte",
+ "delete": "Supprimer le compte",
+ "name": "Nom du compte",
+ "address": "Adresse e-mail",
+ "default": "Compte par défaut"
+ },
+ "settings": {
+ "title": "Paramètres",
+ "general": "Général",
+ "appearance": "Apparence",
+ "accounts": "Comptes",
+ "signature": "Signature",
+ "language": "Langue",
+ "theme": "Thème",
+ "darkMode": "Mode sombre",
+ "notifications": "Notifications"
+ },
+ "common": {
+ "save": "Enregistrer",
+ "cancel": "Annuler",
+ "delete": "Supprimer",
+ "edit": "Modifier",
+ "add": "Ajouter",
+ "confirm": "Confirmer",
+ "yes": "Oui",
+ "no": "Non",
+ "ok": "OK",
+ "loading": "Chargement...",
+ "error": "Erreur",
+ "success": "Succès",
+ "back": "Retour",
+ "search": "Rechercher",
+ "select": "Sélectionner",
+ "selectAll": "Tout sélectionner"
+ }
+}
diff --git a/apps/mail/apps/web/src/lib/i18n/locales/it.json b/apps/mail/apps/web/src/lib/i18n/locales/it.json
new file mode 100644
index 000000000..4fae64dbd
--- /dev/null
+++ b/apps/mail/apps/web/src/lib/i18n/locales/it.json
@@ -0,0 +1,105 @@
+{
+ "app": {
+ "name": "Mail",
+ "loading": "Caricamento..."
+ },
+ "nav": {
+ "inbox": "Posta in arrivo",
+ "sent": "Inviati",
+ "drafts": "Bozze",
+ "starred": "Speciali",
+ "archive": "Archivio",
+ "trash": "Cestino",
+ "spam": "Spam",
+ "settings": "Impostazioni",
+ "feedback": "Feedback"
+ },
+ "auth": {
+ "login": "Accedi",
+ "register": "Registrati",
+ "logout": "Esci",
+ "forgotPassword": "Password dimenticata",
+ "email": "E-mail",
+ "password": "Password",
+ "confirmPassword": "Conferma password"
+ },
+ "email": {
+ "compose": "Scrivi",
+ "reply": "Rispondi",
+ "replyAll": "Rispondi a tutti",
+ "forward": "Inoltra",
+ "delete": "Elimina",
+ "archive": "Archivia",
+ "markRead": "Segna come letto",
+ "markUnread": "Segna come non letto",
+ "star": "Aggiungi a speciali",
+ "unstar": "Rimuovi da speciali",
+ "to": "A",
+ "cc": "CC",
+ "bcc": "CCN",
+ "subject": "Oggetto",
+ "body": "Messaggio",
+ "send": "Invia",
+ "saveDraft": "Salva come bozza",
+ "discard": "Elimina",
+ "attachments": "Allegati",
+ "addAttachment": "Aggiungi allegato",
+ "noEmails": "Nessuna email",
+ "noSubject": "(Nessun oggetto)",
+ "from": "Da",
+ "date": "Data"
+ },
+ "folders": {
+ "title": "Cartelle",
+ "add": "Aggiungi cartella",
+ "edit": "Modifica cartella",
+ "delete": "Elimina cartella",
+ "name": "Nome cartella"
+ },
+ "labels": {
+ "title": "Etichette",
+ "add": "Aggiungi etichetta",
+ "edit": "Modifica etichetta",
+ "delete": "Elimina etichetta",
+ "name": "Nome etichetta",
+ "color": "Colore"
+ },
+ "accounts": {
+ "title": "Account email",
+ "add": "Aggiungi account",
+ "edit": "Modifica account",
+ "delete": "Elimina account",
+ "name": "Nome account",
+ "address": "Indirizzo email",
+ "default": "Account predefinito"
+ },
+ "settings": {
+ "title": "Impostazioni",
+ "general": "Generale",
+ "appearance": "Aspetto",
+ "accounts": "Account",
+ "signature": "Firma",
+ "language": "Lingua",
+ "theme": "Tema",
+ "darkMode": "Modalità scura",
+ "notifications": "Notifiche"
+ },
+ "common": {
+ "save": "Salva",
+ "cancel": "Annulla",
+ "delete": "Elimina",
+ "edit": "Modifica",
+ "add": "Aggiungi",
+ "confirm": "Conferma",
+ "yes": "Sì",
+ "no": "No",
+ "ok": "OK",
+ "loading": "Caricamento...",
+ "error": "Errore",
+ "success": "Successo",
+ "back": "Indietro",
+ "search": "Cerca",
+ "select": "Seleziona",
+ "selectAll": "Seleziona tutto"
+ }
+}
diff --git a/apps/mail/apps/web/src/routes/+layout.svelte b/apps/mail/apps/web/src/routes/+layout.svelte
index 8b65fa090..6d07ab69b 100644
--- a/apps/mail/apps/web/src/routes/+layout.svelte
+++ b/apps/mail/apps/web/src/routes/+layout.svelte
@@ -9,6 +9,7 @@
import { authStore } from '$lib/stores/auth.svelte';
import { accountsStore } from '$lib/stores/accounts.svelte';
import { foldersStore } from '$lib/stores/folders.svelte';
+ import '$lib/i18n';
import '../app.css';
let { children } = $props();
diff --git a/apps/mail/apps/web/src/routes/feedback/+page.svelte b/apps/mail/apps/web/src/routes/feedback/+page.svelte
new file mode 100644
index 000000000..3600ee580
--- /dev/null
+++ b/apps/mail/apps/web/src/routes/feedback/+page.svelte
@@ -0,0 +1,20 @@
+
+
+
diff --git a/apps/manacore/apps/web/src/lib/api/base-client.ts b/apps/manacore/apps/web/src/lib/api/base-client.ts
index 122f54e33..6e4394049 100644
--- a/apps/manacore/apps/web/src/lib/api/base-client.ts
+++ b/apps/manacore/apps/web/src/lib/api/base-client.ts
@@ -4,7 +4,7 @@
* Provides authenticated fetch with exponential backoff retry.
*/
-import { authStore } from '$lib/stores/authStore.svelte';
+import { authStore } from '$lib/stores/auth.svelte';
/**
* Retry configuration
diff --git a/apps/manacore/apps/web/src/lib/api/credits.ts b/apps/manacore/apps/web/src/lib/api/credits.ts
index 70bf4f761..e24a11ab1 100644
--- a/apps/manacore/apps/web/src/lib/api/credits.ts
+++ b/apps/manacore/apps/web/src/lib/api/credits.ts
@@ -3,7 +3,7 @@
* Handles credit balance, transactions, and packages
*/
-import { authStore } from '$lib/stores/authStore.svelte';
+import { authStore } from '$lib/stores/auth.svelte';
const MANA_AUTH_URL = 'http://localhost:3001'; // TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env
diff --git a/apps/manacore/apps/web/src/lib/api/feedback.ts b/apps/manacore/apps/web/src/lib/api/feedback.ts
index 877bd1796..946142c01 100644
--- a/apps/manacore/apps/web/src/lib/api/feedback.ts
+++ b/apps/manacore/apps/web/src/lib/api/feedback.ts
@@ -3,7 +3,7 @@
*/
import { createFeedbackService } from '@manacore/shared-feedback-service';
-import { authStore } from '$lib/stores/authStore.svelte';
+import { authStore } from '$lib/stores/auth.svelte';
const MANA_AUTH_URL = 'http://localhost:3001'; // TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env
diff --git a/apps/manacore/apps/web/src/lib/stores/authStore.svelte.ts b/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts
similarity index 100%
rename from apps/manacore/apps/web/src/lib/stores/authStore.svelte.ts
rename to apps/manacore/apps/web/src/lib/stores/auth.svelte.ts
diff --git a/apps/manacore/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/manacore/apps/web/src/lib/stores/user-settings.svelte.ts
index a520d0aeb..6e178a802 100644
--- a/apps/manacore/apps/web/src/lib/stores/user-settings.svelte.ts
+++ b/apps/manacore/apps/web/src/lib/stores/user-settings.svelte.ts
@@ -8,7 +8,7 @@
*/
import { createUserSettingsStore } from '@manacore/shared-theme';
-import { authStore } from './authStore.svelte';
+import { authStore } from './auth.svelte';
// TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env when available
const MANA_AUTH_URL = 'http://localhost:3001';
diff --git a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte
index f9998bf9f..ad7ac7c1a 100644
--- a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte
+++ b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte
@@ -10,7 +10,7 @@
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 { authStore } from '$lib/stores/auth.svelte';
import { userSettings } from '$lib/stores/user-settings.svelte';
import {
isSidebarMode as sidebarModeStore,
diff --git a/apps/manacore/apps/web/src/routes/(app)/dashboard/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/dashboard/+page.svelte
index d50db0b41..dbc25ad54 100644
--- a/apps/manacore/apps/web/src/routes/(app)/dashboard/+page.svelte
+++ b/apps/manacore/apps/web/src/routes/(app)/dashboard/+page.svelte
@@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { PageHeader } from '@manacore/shared-ui';
- import { authStore } from '$lib/stores/authStore.svelte';
+ import { authStore } from '$lib/stores/auth.svelte';
import { dashboardStore } from '$lib/stores/dashboard.svelte';
import DashboardGrid from '$lib/components/dashboard/DashboardGrid.svelte';
diff --git a/apps/manacore/apps/web/src/routes/(app)/feedback/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/feedback/+page.svelte
index 46fae75f6..a8cb80d2c 100644
--- a/apps/manacore/apps/web/src/routes/(app)/feedback/+page.svelte
+++ b/apps/manacore/apps/web/src/routes/(app)/feedback/+page.svelte
@@ -1,7 +1,7 @@
diff --git a/apps/manacore/apps/web/src/routes/(app)/profile/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/profile/+page.svelte
index 7995a5f3c..e4aeb28d4 100644
--- a/apps/manacore/apps/web/src/routes/(app)/profile/+page.svelte
+++ b/apps/manacore/apps/web/src/routes/(app)/profile/+page.svelte
@@ -1,7 +1,7 @@
diff --git a/apps/manadeck/apps/web/src/routes/(app)/profile/+page.svelte b/apps/manadeck/apps/web/src/routes/(app)/profile/+page.svelte
index 225ce40bd..c20e7ffd1 100644
--- a/apps/manadeck/apps/web/src/routes/(app)/profile/+page.svelte
+++ b/apps/manadeck/apps/web/src/routes/(app)/profile/+page.svelte
@@ -1,7 +1,7 @@
diff --git a/apps/moodlit/apps/web/src/routes/(auth)/forgot-password/+page.svelte b/apps/moodlit/apps/web/src/routes/(auth)/forgot-password/+page.svelte
index a107bba4f..f46767eed 100644
--- a/apps/moodlit/apps/web/src/routes/(auth)/forgot-password/+page.svelte
+++ b/apps/moodlit/apps/web/src/routes/(auth)/forgot-password/+page.svelte
@@ -6,7 +6,7 @@
import { getForgotPasswordTranslations } from '@manacore/shared-i18n';
import AppSlider from '$lib/components/AppSlider.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
- import { authStore } from '$lib/stores/authStore.svelte';
+ import { authStore } from '$lib/stores/auth.svelte';
// Get translations based on current locale
const translations = $derived(getForgotPasswordTranslations($locale || 'de'));
diff --git a/apps/moodlit/apps/web/src/routes/(auth)/login/+page.svelte b/apps/moodlit/apps/web/src/routes/(auth)/login/+page.svelte
index da211d5db..98901b183 100644
--- a/apps/moodlit/apps/web/src/routes/(auth)/login/+page.svelte
+++ b/apps/moodlit/apps/web/src/routes/(auth)/login/+page.svelte
@@ -6,7 +6,7 @@
import { getLoginTranslations } from '@manacore/shared-i18n';
import AppSlider from '$lib/components/AppSlider.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
- import { authStore } from '$lib/stores/authStore.svelte';
+ import { authStore } from '$lib/stores/auth.svelte';
// Get translations based on current locale
const translations = $derived(getLoginTranslations($locale || 'de'));
diff --git a/apps/moodlit/apps/web/src/routes/(auth)/register/+page.svelte b/apps/moodlit/apps/web/src/routes/(auth)/register/+page.svelte
index f09128db8..5bde17637 100644
--- a/apps/moodlit/apps/web/src/routes/(auth)/register/+page.svelte
+++ b/apps/moodlit/apps/web/src/routes/(auth)/register/+page.svelte
@@ -6,7 +6,7 @@
import { getRegisterTranslations } from '@manacore/shared-i18n';
import AppSlider from '$lib/components/AppSlider.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
- import { authStore } from '$lib/stores/authStore.svelte';
+ import { authStore } from '$lib/stores/auth.svelte';
// Get translations based on current locale
const translations = $derived(getRegisterTranslations($locale || 'de'));
diff --git a/packages/shared-api-client/package.json b/packages/shared-api-client/package.json
new file mode 100644
index 000000000..8bd95a6ea
--- /dev/null
+++ b/packages/shared-api-client/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@manacore/shared-api-client",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "main": "./src/index.ts",
+ "types": "./src/index.ts",
+ "exports": {
+ ".": "./src/index.ts"
+ },
+ "scripts": {
+ "type-check": "tsc --noEmit"
+ },
+ "devDependencies": {
+ "typescript": "^5.0.0"
+ }
+}
diff --git a/packages/shared-api-client/src/client.ts b/packages/shared-api-client/src/client.ts
new file mode 100644
index 000000000..a096dedac
--- /dev/null
+++ b/packages/shared-api-client/src/client.ts
@@ -0,0 +1,218 @@
+/**
+ * Shared API Client Factory
+ * Creates a configured API client for making authenticated requests.
+ */
+
+import type { ApiResponse, FetchOptions, HttpMethod } from './types';
+
+export interface ApiClientConfig {
+ /** Base URL for the API (e.g., 'http://localhost:3002') */
+ baseUrl: string;
+ /** Optional API prefix (default: '/api') */
+ apiPrefix?: string;
+ /** Function to get the current auth token */
+ getToken?: () => Promise | string | null;
+ /** Whether running in browser environment */
+ isBrowser?: boolean;
+ /** Local storage key for token fallback */
+ tokenStorageKey?: string;
+}
+
+export interface ApiClient {
+ /** Make a GET request */
+ get: (endpoint: string, options?: Omit) => Promise>;
+ /** Make a POST request */
+ post: (
+ endpoint: string,
+ body?: unknown,
+ options?: Omit
+ ) => Promise>;
+ /** Make a PUT request */
+ put: (
+ endpoint: string,
+ body?: unknown,
+ options?: Omit
+ ) => Promise>;
+ /** Make a PATCH request */
+ patch: (
+ endpoint: string,
+ body?: unknown,
+ options?: Omit
+ ) => Promise>;
+ /** Make a DELETE request */
+ delete: (endpoint: string, options?: Omit) => Promise>;
+ /** Make a request with any method */
+ request: (endpoint: string, options?: FetchOptions) => Promise>;
+ /** Upload a single file */
+ uploadFile: (endpoint: string, file: File, token?: string) => Promise>;
+ /** Upload multiple files */
+ uploadFiles: (endpoint: string, files: File[], token?: string) => Promise>;
+}
+
+/**
+ * Create an API client with the given configuration.
+ */
+export function createApiClient(config: ApiClientConfig): ApiClient {
+ const { baseUrl, apiPrefix = '/api', getToken, isBrowser = true, tokenStorageKey } = config;
+
+ async function getAuthToken(providedToken?: string): Promise {
+ if (providedToken) return providedToken;
+
+ if (getToken) {
+ const token = await getToken();
+ if (token) return token;
+ }
+
+ // Fallback to localStorage if in browser and key provided
+ if (isBrowser && tokenStorageKey && typeof localStorage !== 'undefined') {
+ return localStorage.getItem(tokenStorageKey) || undefined;
+ }
+
+ return undefined;
+ }
+
+ async function request(endpoint: string, options: FetchOptions = {}): Promise> {
+ const { method = 'GET', body, token, isFormData = false, headers: customHeaders } = options;
+
+ const authToken = await getAuthToken(token);
+
+ try {
+ const headers: Record = { ...customHeaders };
+
+ // Don't set Content-Type for FormData - browser sets it automatically with boundary
+ if (!isFormData) {
+ headers['Content-Type'] = 'application/json';
+ }
+
+ if (authToken) {
+ headers['Authorization'] = `Bearer ${authToken}`;
+ }
+
+ const url = `${baseUrl}${apiPrefix}${endpoint}`;
+ const response = await fetch(url, {
+ method,
+ headers,
+ body: isFormData ? (body as FormData) : body ? JSON.stringify(body) : undefined,
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ return {
+ data: null,
+ error: new Error(errorData.message || `API error: ${response.status}`),
+ };
+ }
+
+ // Handle empty responses (204 No Content)
+ if (response.status === 204) {
+ return { data: null, error: null };
+ }
+
+ const data = await response.json();
+ return { data, error: null };
+ } catch (error) {
+ return {
+ data: null,
+ error: error instanceof Error ? error : new Error('Unknown error'),
+ };
+ }
+ }
+
+ async function uploadFile(
+ endpoint: string,
+ file: File,
+ token?: string
+ ): Promise> {
+ const authToken = await getAuthToken(token);
+
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const headers: Record = {};
+ if (authToken) {
+ headers['Authorization'] = `Bearer ${authToken}`;
+ }
+
+ const response = await fetch(`${baseUrl}${apiPrefix}${endpoint}`, {
+ method: 'POST',
+ headers,
+ body: formData,
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ return {
+ data: null,
+ error: new Error(errorData.message || `Upload error: ${response.status}`),
+ };
+ }
+
+ const data = await response.json();
+ return { data, error: null };
+ } catch (error) {
+ return {
+ data: null,
+ error: error instanceof Error ? error : new Error('Upload failed'),
+ };
+ }
+ }
+
+ async function uploadFiles(
+ endpoint: string,
+ files: File[],
+ token?: string
+ ): Promise> {
+ const authToken = await getAuthToken(token);
+
+ try {
+ const formData = new FormData();
+ files.forEach((file) => {
+ formData.append('files', file);
+ });
+
+ const headers: Record = {};
+ if (authToken) {
+ headers['Authorization'] = `Bearer ${authToken}`;
+ }
+
+ const response = await fetch(`${baseUrl}${apiPrefix}${endpoint}`, {
+ method: 'POST',
+ headers,
+ body: formData,
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ return {
+ data: null,
+ error: new Error(errorData.message || `Upload error: ${response.status}`),
+ };
+ }
+
+ const data = await response.json();
+ return { data, error: null };
+ } catch (error) {
+ return {
+ data: null,
+ error: error instanceof Error ? error : new Error('Upload failed'),
+ };
+ }
+ }
+
+ return {
+ get: (endpoint: string, options?: Omit) =>
+ request(endpoint, { ...options, method: 'GET' }),
+ post: (endpoint: string, body?: unknown, options?: Omit) =>
+ request(endpoint, { ...options, method: 'POST', body }),
+ put: (endpoint: string, body?: unknown, options?: Omit) =>
+ request(endpoint, { ...options, method: 'PUT', body }),
+ patch: (endpoint: string, body?: unknown, options?: Omit) =>
+ request(endpoint, { ...options, method: 'PATCH', body }),
+ delete: (endpoint: string, options?: Omit) =>
+ request(endpoint, { ...options, method: 'DELETE' }),
+ request,
+ uploadFile,
+ uploadFiles,
+ };
+}
diff --git a/packages/shared-api-client/src/index.ts b/packages/shared-api-client/src/index.ts
new file mode 100644
index 000000000..f1f3e15a8
--- /dev/null
+++ b/packages/shared-api-client/src/index.ts
@@ -0,0 +1,7 @@
+/**
+ * Shared API Client for ManaCore Apps
+ * Provides a unified way to make API calls with authentication.
+ */
+
+export { createApiClient, type ApiClientConfig, type ApiClient } from './client';
+export { type ApiResponse, type FetchOptions, type HttpMethod } from './types';
diff --git a/packages/shared-api-client/src/types.ts b/packages/shared-api-client/src/types.ts
new file mode 100644
index 000000000..3fca6d913
--- /dev/null
+++ b/packages/shared-api-client/src/types.ts
@@ -0,0 +1,18 @@
+/**
+ * Shared API Client Types
+ */
+
+export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
+
+export interface FetchOptions {
+ method?: HttpMethod;
+ body?: unknown;
+ token?: string;
+ isFormData?: boolean;
+ headers?: Record;
+}
+
+export interface ApiResponse {
+ data: T | null;
+ error: Error | null;
+}
diff --git a/packages/shared-api-client/tsconfig.json b/packages/shared-api-client/tsconfig.json
new file mode 100644
index 000000000..c0db43203
--- /dev/null
+++ b/packages/shared-api-client/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "./dist",
+ "rootDir": "./src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/shared-stores/package.json b/packages/shared-stores/package.json
new file mode 100644
index 000000000..ce3f336a5
--- /dev/null
+++ b/packages/shared-stores/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@manacore/shared-stores",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "main": "./src/index.ts",
+ "types": "./src/index.ts",
+ "exports": {
+ ".": "./src/index.ts"
+ },
+ "scripts": {
+ "type-check": "echo 'Skipping: shared-stores uses Svelte 5 runes, type-checked at build time'"
+ },
+ "devDependencies": {
+ "svelte": "^5.0.0",
+ "typescript": "^5.0.0"
+ },
+ "dependencies": {
+ "@manacore/shared-auth": "workspace:*"
+ }
+}
diff --git a/packages/shared-stores/src/index.ts b/packages/shared-stores/src/index.ts
new file mode 100644
index 000000000..46c334d50
--- /dev/null
+++ b/packages/shared-stores/src/index.ts
@@ -0,0 +1,12 @@
+/**
+ * Shared Store Factories for ManaCore Apps
+ * Provides reusable Svelte 5 runes-based stores.
+ */
+
+export { createToastStore, type Toast, type ToastStore, type ToastType } from './toast.svelte';
+export {
+ createNavigationStore,
+ type NavigationItem,
+ type NavigationStore,
+} from './navigation.svelte';
+export { createThemeStore, type ThemeStore, type ThemeMode } from './theme.svelte';
diff --git a/packages/shared-stores/src/navigation.svelte.ts b/packages/shared-stores/src/navigation.svelte.ts
new file mode 100644
index 000000000..bed65b1e1
--- /dev/null
+++ b/packages/shared-stores/src/navigation.svelte.ts
@@ -0,0 +1,117 @@
+/**
+ * Navigation Store Factory
+ * Creates a navigation state store with Svelte 5 runes.
+ */
+
+export interface NavigationItem {
+ href: string;
+ label: string;
+ icon?: string;
+ badge?: string | number;
+ children?: NavigationItem[];
+}
+
+export interface NavigationStore {
+ readonly items: NavigationItem[];
+ readonly isOpen: boolean;
+ readonly isSidebarMode: boolean;
+ readonly isCollapsed: boolean;
+ setItems: (items: NavigationItem[]) => void;
+ toggle: () => void;
+ open: () => void;
+ close: () => void;
+ setSidebarMode: (isSidebar: boolean) => void;
+ setCollapsed: (collapsed: boolean) => void;
+}
+
+export interface NavigationStoreConfig {
+ /** Initial navigation items */
+ initialItems?: NavigationItem[];
+ /** Storage key for persisting sidebar mode */
+ storageKey?: string;
+ /** Whether to start in sidebar mode */
+ defaultSidebarMode?: boolean;
+ /** Whether to start collapsed */
+ defaultCollapsed?: boolean;
+}
+
+/**
+ * Create a navigation store with Svelte 5 runes.
+ */
+export function createNavigationStore(config: NavigationStoreConfig = {}): NavigationStore {
+ const {
+ initialItems = [],
+ storageKey,
+ defaultSidebarMode = false,
+ defaultCollapsed = false,
+ } = config;
+
+ let items = $state(initialItems);
+ let isOpen = $state(false);
+ let isSidebarMode = $state(defaultSidebarMode);
+ let isCollapsed = $state(defaultCollapsed);
+
+ // Load from localStorage if available
+ if (storageKey && typeof localStorage !== 'undefined') {
+ const savedSidebar = localStorage.getItem(`${storageKey}-sidebar`);
+ const savedCollapsed = localStorage.getItem(`${storageKey}-collapsed`);
+
+ if (savedSidebar !== null) {
+ isSidebarMode = savedSidebar === 'true';
+ }
+ if (savedCollapsed !== null) {
+ isCollapsed = savedCollapsed === 'true';
+ }
+ }
+
+ function setItems(newItems: NavigationItem[]) {
+ items = newItems;
+ }
+
+ function toggle() {
+ isOpen = !isOpen;
+ }
+
+ function open() {
+ isOpen = true;
+ }
+
+ function close() {
+ isOpen = false;
+ }
+
+ function setSidebarMode(sidebar: boolean) {
+ isSidebarMode = sidebar;
+ if (storageKey && typeof localStorage !== 'undefined') {
+ localStorage.setItem(`${storageKey}-sidebar`, String(sidebar));
+ }
+ }
+
+ function setCollapsed(collapsed: boolean) {
+ isCollapsed = collapsed;
+ if (storageKey && typeof localStorage !== 'undefined') {
+ localStorage.setItem(`${storageKey}-collapsed`, String(collapsed));
+ }
+ }
+
+ return {
+ get items() {
+ return items;
+ },
+ get isOpen() {
+ return isOpen;
+ },
+ get isSidebarMode() {
+ return isSidebarMode;
+ },
+ get isCollapsed() {
+ return isCollapsed;
+ },
+ setItems,
+ toggle,
+ open,
+ close,
+ setSidebarMode,
+ setCollapsed,
+ };
+}
diff --git a/packages/shared-stores/src/theme.svelte.ts b/packages/shared-stores/src/theme.svelte.ts
new file mode 100644
index 000000000..1f37d31d3
--- /dev/null
+++ b/packages/shared-stores/src/theme.svelte.ts
@@ -0,0 +1,125 @@
+/**
+ * Theme Store Factory
+ * Creates a theme state store with Svelte 5 runes.
+ */
+
+export type ThemeMode = 'light' | 'dark' | 'system';
+
+export interface ThemeStore {
+ readonly isDark: boolean;
+ readonly mode: ThemeMode;
+ readonly variant: string;
+ initialize: () => () => void;
+ setMode: (mode: ThemeMode) => void;
+ setVariant: (variant: string) => void;
+ toggle: () => void;
+}
+
+export interface ThemeStoreConfig {
+ /** Storage key prefix (default: 'theme') */
+ storagePrefix?: string;
+ /** Default theme mode */
+ defaultMode?: ThemeMode;
+ /** Default theme variant */
+ defaultVariant?: string;
+ /** CSS class to add/remove for dark mode */
+ darkClass?: string;
+ /** Data attribute for variant */
+ variantAttribute?: string;
+}
+
+/**
+ * Create a theme store with Svelte 5 runes.
+ */
+export function createThemeStore(config: ThemeStoreConfig = {}): ThemeStore {
+ const {
+ storagePrefix = 'theme',
+ defaultMode = 'system',
+ defaultVariant = 'default',
+ darkClass = 'dark',
+ variantAttribute = 'data-theme',
+ } = config;
+
+ let isDark = $state(false);
+ let mode = $state(defaultMode);
+ let variant = $state(defaultVariant);
+
+ function updateTheme() {
+ if (typeof window === 'undefined') return;
+
+ let shouldBeDark = false;
+ if (mode === 'dark') {
+ shouldBeDark = true;
+ } else if (mode === 'system') {
+ shouldBeDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+ }
+
+ isDark = shouldBeDark;
+ document.documentElement.classList.toggle(darkClass, isDark);
+ }
+
+ function initialize(): () => void {
+ if (typeof window === 'undefined') return () => {};
+
+ // Load from localStorage
+ const savedMode = localStorage.getItem(`${storagePrefix}-mode`) as ThemeMode | null;
+ const savedVariant = localStorage.getItem(`${storagePrefix}-variant`);
+
+ if (savedMode) mode = savedMode;
+ if (savedVariant) {
+ variant = savedVariant;
+ document.documentElement.setAttribute(variantAttribute, variant);
+ }
+
+ updateTheme();
+
+ // Listen for system theme changes
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ const handleChange = () => {
+ if (mode === 'system') {
+ updateTheme();
+ }
+ };
+
+ mediaQuery.addEventListener('change', handleChange);
+ return () => mediaQuery.removeEventListener('change', handleChange);
+ }
+
+ function setMode(newMode: ThemeMode) {
+ mode = newMode;
+ if (typeof localStorage !== 'undefined') {
+ localStorage.setItem(`${storagePrefix}-mode`, newMode);
+ }
+ updateTheme();
+ }
+
+ function setVariant(newVariant: string) {
+ variant = newVariant;
+ if (typeof localStorage !== 'undefined') {
+ localStorage.setItem(`${storagePrefix}-variant`, newVariant);
+ }
+ if (typeof document !== 'undefined') {
+ document.documentElement.setAttribute(variantAttribute, newVariant);
+ }
+ }
+
+ function toggle() {
+ setMode(isDark ? 'light' : 'dark');
+ }
+
+ return {
+ get isDark() {
+ return isDark;
+ },
+ get mode() {
+ return mode;
+ },
+ get variant() {
+ return variant;
+ },
+ initialize,
+ setMode,
+ setVariant,
+ toggle,
+ };
+}
diff --git a/packages/shared-stores/src/toast.svelte.ts b/packages/shared-stores/src/toast.svelte.ts
new file mode 100644
index 000000000..804f92f05
--- /dev/null
+++ b/packages/shared-stores/src/toast.svelte.ts
@@ -0,0 +1,76 @@
+/**
+ * Toast Store Factory
+ * Creates a toast notification store with Svelte 5 runes.
+ */
+
+export type ToastType = 'success' | 'error' | 'info' | 'warning';
+
+export interface Toast {
+ id: string;
+ type: ToastType;
+ message: string;
+ duration?: number;
+}
+
+export interface ToastStore {
+ readonly toasts: Toast[];
+ show: (message: string, type?: ToastType, duration?: number) => void;
+ success: (message: string, duration?: number) => void;
+ error: (message: string, duration?: number) => void;
+ info: (message: string, duration?: number) => void;
+ warning: (message: string, duration?: number) => void;
+ dismiss: (id: string) => void;
+ clear: () => void;
+}
+
+export interface ToastStoreConfig {
+ /** Default duration in milliseconds (default: 5000) */
+ defaultDuration?: number;
+ /** Maximum number of toasts visible at once */
+ maxToasts?: number;
+}
+
+/**
+ * Create a toast store with Svelte 5 runes.
+ */
+export function createToastStore(config: ToastStoreConfig = {}): ToastStore {
+ const { defaultDuration = 5000, maxToasts = 5 } = config;
+
+ let toasts = $state([]);
+
+ function generateId(): string {
+ return Math.random().toString(36).substring(2, 9);
+ }
+
+ function show(message: string, type: ToastType = 'info', duration: number = defaultDuration) {
+ const id = generateId();
+ const toast: Toast = { id, type, message, duration };
+
+ toasts = [...toasts.slice(-(maxToasts - 1)), toast];
+
+ if (duration > 0) {
+ setTimeout(() => dismiss(id), duration);
+ }
+ }
+
+ function dismiss(id: string) {
+ toasts = toasts.filter((t) => t.id !== id);
+ }
+
+ function clear() {
+ toasts = [];
+ }
+
+ return {
+ get toasts() {
+ return toasts;
+ },
+ show,
+ success: (message: string, duration?: number) => show(message, 'success', duration),
+ error: (message: string, duration?: number) => show(message, 'error', duration),
+ info: (message: string, duration?: number) => show(message, 'info', duration),
+ warning: (message: string, duration?: number) => show(message, 'warning', duration),
+ dismiss,
+ clear,
+ };
+}
diff --git a/packages/shared-stores/tsconfig.json b/packages/shared-stores/tsconfig.json
new file mode 100644
index 000000000..c0db43203
--- /dev/null
+++ b/packages/shared-stores/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "./dist",
+ "rootDir": "./src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/shared-vite-config/package.json b/packages/shared-vite-config/package.json
new file mode 100644
index 000000000..58262b43c
--- /dev/null
+++ b/packages/shared-vite-config/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@manacore/shared-vite-config",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "main": "./src/index.ts",
+ "types": "./src/index.ts",
+ "exports": {
+ ".": "./src/index.ts"
+ },
+ "scripts": {
+ "type-check": "tsc --noEmit"
+ },
+ "devDependencies": {
+ "typescript": "^5.0.0",
+ "vite": "^6.0.0"
+ }
+}
diff --git a/packages/shared-vite-config/src/index.ts b/packages/shared-vite-config/src/index.ts
new file mode 100644
index 000000000..844a5e6df
--- /dev/null
+++ b/packages/shared-vite-config/src/index.ts
@@ -0,0 +1,130 @@
+/**
+ * Shared Vite Configuration for ManaCore Web Apps
+ * Provides consistent SSR and optimization settings.
+ */
+
+import type { UserConfig } from 'vite';
+
+/**
+ * Common ManaCore shared packages that need SSR configuration.
+ * These packages contain Svelte 5 runes or other client-side state.
+ */
+export const MANACORE_SHARED_PACKAGES = [
+ '@manacore/shared-icons',
+ '@manacore/shared-ui',
+ '@manacore/shared-tailwind',
+ '@manacore/shared-theme',
+ '@manacore/shared-theme-ui',
+ '@manacore/shared-feedback-ui',
+ '@manacore/shared-feedback-service',
+ '@manacore/shared-feedback-types',
+ '@manacore/shared-auth',
+ '@manacore/shared-auth-ui',
+ '@manacore/shared-branding',
+ '@manacore/shared-subscription-ui',
+ '@manacore/shared-profile-ui',
+ '@manacore/shared-i18n',
+ '@manacore/shared-api-client',
+] as const;
+
+export interface ViteConfigOptions {
+ /** Server port */
+ port: number;
+ /** Additional packages to include in noExternal (e.g., app-specific shared packages) */
+ additionalPackages?: string[];
+ /** Additional packages to exclude from optimization */
+ additionalExcludes?: string[];
+ /** Override default shared packages (if you need a subset) */
+ sharedPackages?: string[];
+}
+
+/**
+ * Get the SSR noExternal configuration for ManaCore apps.
+ */
+export function getSsrNoExternal(additionalPackages: string[] = []): string[] {
+ return [...MANACORE_SHARED_PACKAGES, ...additionalPackages];
+}
+
+/**
+ * Get the optimizeDeps exclude configuration for ManaCore apps.
+ */
+export function getOptimizeDepsExclude(additionalExcludes: string[] = []): string[] {
+ return [...MANACORE_SHARED_PACKAGES, ...additionalExcludes];
+}
+
+/**
+ * Create a base Vite configuration for ManaCore SvelteKit apps.
+ * Merge this with your app-specific configuration.
+ */
+export function createViteConfig(options: ViteConfigOptions): Partial {
+ const { port, additionalPackages = [], additionalExcludes = [] } = options;
+
+ const packages = options.sharedPackages || [...MANACORE_SHARED_PACKAGES];
+ const noExternal = [...packages, ...additionalPackages];
+ const exclude = [...packages, ...additionalExcludes];
+
+ return {
+ server: {
+ port,
+ strictPort: true,
+ },
+ ssr: {
+ noExternal,
+ },
+ optimizeDeps: {
+ exclude,
+ },
+ };
+}
+
+/**
+ * Merge base config with app-specific plugins and settings.
+ * Use this in your vite.config.ts:
+ *
+ * @example
+ * ```ts
+ * import { sveltekit } from '@sveltejs/kit/vite';
+ * import tailwindcss from '@tailwindcss/vite';
+ * import { defineConfig } from 'vite';
+ * import { createViteConfig, mergeViteConfig } from '@manacore/shared-vite-config';
+ *
+ * const baseConfig = createViteConfig({
+ * port: 5174,
+ * additionalPackages: ['@chat/shared'],
+ * });
+ *
+ * export default defineConfig(mergeViteConfig(baseConfig, {
+ * plugins: [tailwindcss(), sveltekit()],
+ * }));
+ * ```
+ */
+export function mergeViteConfig(
+ baseConfig: Partial,
+ appConfig: Partial
+): UserConfig {
+ return {
+ ...baseConfig,
+ ...appConfig,
+ server: {
+ ...baseConfig.server,
+ ...appConfig.server,
+ },
+ ssr: {
+ ...baseConfig.ssr,
+ ...appConfig.ssr,
+ noExternal: [
+ ...((baseConfig.ssr?.noExternal as string[]) || []),
+ ...((appConfig.ssr?.noExternal as string[]) || []),
+ ],
+ },
+ optimizeDeps: {
+ ...baseConfig.optimizeDeps,
+ ...appConfig.optimizeDeps,
+ exclude: [
+ ...(baseConfig.optimizeDeps?.exclude || []),
+ ...(appConfig.optimizeDeps?.exclude || []),
+ ],
+ },
+ plugins: [...(baseConfig.plugins || []), ...(appConfig.plugins || [])],
+ };
+}
diff --git a/packages/shared-vite-config/tsconfig.json b/packages/shared-vite-config/tsconfig.json
new file mode 100644
index 000000000..c0db43203
--- /dev/null
+++ b/packages/shared-vite-config/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "./dist",
+ "rootDir": "./src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}