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} -
-
-

Was möchten Sie uns mitteilen?

- -
- - - -
- -
-
- - -
- -
- - -

- Falls wir Rückfragen haben oder Sie über Updates informieren möchten -

-
-
-
- - {#if error} -
{error}
- {/if} - -
- -
-
- {/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"] +}