From 2b1556e392cacb16c61a2e8856acf9b34cc02b2e Mon Sep 17 00:00:00 2001 From: marks Date: Thu, 21 Aug 2025 16:33:00 -0500 Subject: [PATCH] Localize the web and tauri apps This sets up the app to handle new translations that are put into `frontend/public/locales/`. When adding a new language, the Info.plist at `src-tauri/gen/apple/maple_iOS/Info.plist` also needs to have a new entry in the `CFBundleLocalizations` list. Next steps: 1. Convert all hardcoded strings in the app to use the localized versions 2. Add more translations This version has English, Spanish, French, and Portuguese. However, only a few of the strings in the UI are being pulled from the localized strings. See step 1 in "Next Steps" --- frontend/README-localization.md | 196 ++++++++++++++++++ frontend/bun.lockb | Bin 239857 -> 250346 bytes frontend/package.json | 2 + frontend/public/locales/en.json | 178 ++++++++++++++++ frontend/public/locales/es.json | 178 ++++++++++++++++ frontend/public/locales/fr.json | 178 ++++++++++++++++ frontend/public/locales/pt.json | 178 ++++++++++++++++ frontend/src-tauri/Cargo.toml | 1 + .../src-tauri/gen/apple/maple_iOS/Info.plist | 7 + frontend/src-tauri/src/lib.rs | 4 +- frontend/src-tauri/tauri.conf.json | 7 + frontend/src/components/TopNav.tsx | 14 +- frontend/src/main.tsx | 39 +++- frontend/src/routes/index.tsx | 4 +- frontend/src/utils/i18n.ts | 83 ++++++++ 15 files changed, 1051 insertions(+), 18 deletions(-) create mode 100644 frontend/README-localization.md create mode 100644 frontend/public/locales/en.json create mode 100644 frontend/public/locales/es.json create mode 100644 frontend/public/locales/fr.json create mode 100644 frontend/public/locales/pt.json create mode 100644 frontend/src/utils/i18n.ts diff --git a/frontend/README-localization.md b/frontend/README-localization.md new file mode 100644 index 00000000..a8290e00 --- /dev/null +++ b/frontend/README-localization.md @@ -0,0 +1,196 @@ +# Localization (i18n) Guide for Maple + +This document explains how the automatic UI localization system works in Maple and how to add new languages. + +## How It Works + +Maple automatically detects the user's operating system language and displays the UI in that language: + +1. **Locale Detection**: Uses `tauri-plugin-localization` to get the native OS locale +2. **Fallback**: Falls back to browser language (`navigator.language`) if native detection fails +3. **Translation Loading**: Dynamically loads the appropriate JSON translation file +4. **UI Rendering**: React components use `useTranslation()` hook to display localized strings + +## Current Supported Languages + +- **English** (`en`) - Default and fallback language +- **French** (`fr`) - Complete translations +- **Spanish** (`es`) - Complete translations + +## File Structure + +``` +frontend/ +├── public/locales/ # Translation files +│ ├── en.json # English (default) +│ ├── fr.json # French +│ └── es.json # Spanish +├── src/ +│ ├── utils/i18n.ts # i18n configuration +│ ├── main.tsx # i18n initialization +│ └── components/ # Components using translations +└── src-tauri/ + ├── Cargo.toml # Rust dependencies + ├── src/lib.rs # Plugin registration + ├── tauri.conf.json # Asset protocol config + └── gen/apple/maple_iOS/Info.plist # iOS language declarations +``` + +## Adding a New Language + +### 1. Create Translation File + +1. Copy `public/locales/en.json` to `public/locales/{code}.json` (e.g., `de.json` for German) +2. Translate all the strings while keeping the same key structure: + +```json +{ + "app": { + "title": "Maple - Private KI-Chat", + "welcome": "Willkommen bei Maple", + "description": "Private KI-Chat mit vertraulicher Datenverarbeitung" + }, + "auth": { + "signIn": "Anmelden", + "signOut": "Abmelden", + "email": "E-Mail", + "password": "Passwort" + } + // ... continue with all keys +} +``` + +### 2. Update iOS Configuration + +Edit `src-tauri/gen/apple/maple_iOS/Info.plist` and add your language code: + +```xml +CFBundleLocalizations + + en + fr + es + de + +``` + +### 3. Test the Implementation + +1. **Development**: `bun tauri dev` + - Change your OS language settings + - Restart the app to see the new language + +2. **iOS**: `bun tauri build --target ios` + - Build and run in iOS Simulator + - Change device language in Settings app + - Test the localized UI + +## Using Translations in Components + +### Basic Usage + +```tsx +import { useTranslation } from 'react-i18next'; + +function MyComponent() { + const { t } = useTranslation(); + + return ( +
+

{t('app.title')}

+ +
+ ); +} +``` + +### With Variables + +```tsx +// Translation with interpolation +const message = t('auth.welcome', { name: 'John' }); + +// In en.json: +// "auth": { "welcome": "Welcome, {{name}}!" } +``` + +### Language Switching (Optional) + +```tsx +const { i18n } = useTranslation(); + +// Manually change language (for testing/admin purposes) +i18n.changeLanguage('fr'); +``` + +## Technical Details + +### Dependencies + +- **Frontend**: `i18next`, `react-i18next` +- **Backend**: `tauri-plugin-localization` + +### Initialization Flow + +1. `main.tsx` calls `initI18n()` before rendering +2. `i18n.ts` resolves the locale using Tauri plugin +3. Appropriate JSON file is loaded dynamically +4. i18next is initialized with the translations +5. React app renders with localized strings + +### Fallback Strategy + +1. Try native OS locale (e.g., `en-US`) +2. Extract language code (`en-US` → `en`) +3. Load matching JSON file (`en.json`) +4. If not found, fall back to English +5. If English fails, use empty translations + +## Platform Support + +| Platform | Locale Detection | Status | +|----------|------------------|--------| +| **Desktop** (Windows/macOS/Linux) | ✅ Native OS locale | Fully supported | +| **iOS** | ✅ Device language | Fully supported | +| **Web** | ✅ Browser language | Fallback only | + +## Troubleshooting + +### Language Not Changing + +1. Check that the JSON file exists in `public/locales/` +2. Verify iOS `Info.plist` includes the language code +3. Restart the app after changing OS language +4. Check browser console for i18n loading errors + +### Missing Translations + +1. Compare your JSON structure with `en.json` +2. Ensure all keys match exactly (case-sensitive) +3. Check for syntax errors in JSON files +4. Use the `t()` function's fallback: `t('key', { defaultValue: 'fallback' })` + +### iOS Build Issues + +1. Ensure Xcode project is regenerated: `bun tauri build --target ios` +2. Check that `CFBundleLocalizations` is properly formatted +3. Clean build folder if needed + +## Performance Notes + +- Translation files are loaded asynchronously on startup +- Only the detected language file is loaded (not all languages) +- Vite's `import.meta.glob` ensures efficient bundling +- First render waits for i18n initialization to prevent FOUC + +## Future Enhancements + +- [ ] Add more languages (German, Italian, Portuguese, Japanese, etc.) +- [ ] Implement plural forms for complex languages +- [ ] Add context-aware translations +- [ ] Create translation management workflow +- [ ] Add RTL language support (Arabic, Hebrew) + +--- + +For questions or issues with localization, please check the main README or open an issue on GitHub. diff --git a/frontend/bun.lockb b/frontend/bun.lockb index d43d39d7b68981fe3540ca4a5894b788ec99974b..7335340e895a1c6c459989cc59a0ecf67a73be63 100755 GIT binary patch delta 54685 zcmeFacR&?a`z?IV$We|8Vh06#0TmTQI>%lRvBUz{5l~Q?fC?xc?4n}Hs9RL5F%e^r z6=SS1lGxL1s8JJ3?6G3IYt76dyzkHVcklhad%y3WOtM(}S^L@T*?acPnZw|dbGG-^ zRans6CFQ9{oA>Vyo@`jjJETF2p@q4JqRySZGxgVbPv=E#UXVU^z`0U79)lN#)HcWT zmlQ*%iwjSTjY4|t*qBI4N${$y(`7p8b-Hq3TZ2wl9XtVS3yw%mj!KTu>E2kWe(7K% z^tAA}SeE}2wjJz3F#Qt~l2cGdH>r$HX9HV6FKb33K0H2wO`g#llcJInV#h`4jHs

p3E1?rMK9QqVfexPsha;7FzvXgq>)kX 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 diff --git a/frontend/package.json b/frontend/package.json index 02a0c42b..d050d438 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,8 @@ }, "dependencies": { "@opensecret/react": "1.3.8", + "i18next": "^23.6.0", + "react-i18next": "^13.0.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", diff --git a/frontend/public/locales/en.json b/frontend/public/locales/en.json new file mode 100644 index 00000000..e51b0082 --- /dev/null +++ b/frontend/public/locales/en.json @@ -0,0 +1,178 @@ +{ + "app": { + "title": "Maple - Private AI Chat", + "welcome": "Welcome to Maple", + "description": "Private AI Chat" + }, + "navigation": { + "home": "Home", + "chat": "Chat", + "teams": "Teams", + "pricing": "Pricing", + "downloads": "Downloads", + "about": "About" + }, + "auth": { + "signIn": "Sign In", + "signOut": "Sign Out", + "signUp": "Sign Up", + "login": "Login", + "logout": "Logout", + "email": "Email", + "password": "Password", + "confirmPassword": "Confirm Password", + "forgotPassword": "Forgot Password?", + "resetPassword": "Reset Password", + "createAccount": "Create Account", + "alreadyHaveAccount": "Already have an account?", + "dontHaveAccount": "Don't have an account?", + "signInWithApple": "Sign in with Apple", + "signInWithGoogle": "Sign in with Google", + "verification": "Verification", + "verifyEmail": "Verify your email address", + "checkEmail": "Please check your email for a verification link" + }, + "chat": { + "newChat": "New Chat", + "chatHistory": "Chat History", + "typeMessage": "Type your message...", + "send": "Send", + "thinking": "Thinking...", + "selectModel": "Select Model", + "systemPrompt": "System Prompt", + "attachFile": "Attach File", + "deleteChat": "Delete Chat", + "renameChat": "Rename Chat", + "confirmDelete": "Are you sure you want to delete this chat?", + "enterChatName": "Enter chat name", + "untitledChat": "Untitled Chat" + }, + "teams": { + "createTeam": "Create Team", + "joinTeam": "Join Team", + "teamName": "Team Name", + "teamMembers": "Team Members", + "inviteMembers": "Invite Members", + "manageTeam": "Manage Team", + "leaveTeam": "Leave Team", + "deleteTeam": "Delete Team", + "teamSettings": "Team Settings", + "pendingInvites": "Pending Invites", + "teamAdmin": "Team Admin", + "teamMember": "Team Member" + }, + "billing": { + "subscription": "Subscription", + "billing": "Billing", + "usage": "Usage", + "credits": "Credits", + "upgrade": "Upgrade", + "downgrade": "Downgrade", + "cancel": "Cancel", + "paymentMethod": "Payment Method", + "billingHistory": "Billing History", + "currentPlan": "Current Plan", + "freePlan": "Free Plan", + "proPlan": "Pro Plan", + "enterprisePlan": "Enterprise Plan" + }, + "button": { + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "create": "Create", + "update": "Update", + "confirm": "Confirm", + "close": "Close", + "next": "Next", + "previous": "Previous", + "submit": "Submit", + "retry": "Retry", + "refresh": "Refresh", + "loading": "Loading...", + "copy": "Copy", + "copied": "Copied!", + "download": "Download", + "upload": "Upload" + }, + "settings": { + "settings": "Settings", + "account": "Account", + "profile": "Profile", + "preferences": "Preferences", + "notifications": "Notifications", + "privacy": "Privacy", + "security": "Security", + "appearance": "Appearance", + "language": "Language", + "theme": "Theme", + "darkMode": "Dark Mode", + "lightMode": "Light Mode", + "systemDefault": "System Default" + }, + "error": { + "general": "An error occurred", + "network": "Network error - please try again later", + "unauthorized": "You are not authorized to perform this action", + "notFound": "The requested resource was not found", + "serverError": "Internal server error", + "validationError": "Please check your input and try again", + "timeout": "Request timed out", + "unknown": "An unknown error occurred", + "sessionExpired": "Your session has expired. Please sign in again.", + "invalidCredentials": "Invalid email or password", + "emailRequired": "Email is required", + "passwordRequired": "Password is required", + "emailInvalid": "Please enter a valid email address", + "passwordTooShort": "Password must be at least 8 characters long", + "passwordsDontMatch": "Passwords don't match" + }, + "success": { + "saved": "Successfully saved", + "deleted": "Successfully deleted", + "updated": "Successfully updated", + "created": "Successfully created", + "sent": "Successfully sent", + "copied": "Copied to clipboard", + "emailSent": "Email sent successfully", + "passwordReset": "Password reset successfully", + "accountCreated": "Account created successfully", + "signedIn": "Signed in successfully", + "signedOut": "Signed out successfully" + }, + "common": { + "yes": "Yes", + "no": "No", + "ok": "OK", + "or": "or", + "and": "and", + "optional": "Optional", + "required": "Required", + "search": "Search", + "filter": "Filter", + "sort": "Sort", + "name": "Name", + "email": "Email", + "date": "Date", + "time": "Time", + "status": "Status", + "active": "Active", + "inactive": "Inactive", + "enabled": "Enabled", + "disabled": "Disabled", + "public": "Public", + "private": "Private", + "examples": "Examples" + }, + "footer": { + "privacy": "Privacy Policy", + "terms": "Terms of Service", + "contact": "Contact", + "support": "Support", + "documentation": "Documentation", + "status": "Status", + "changelog": "Changelog", + "copyright": "© {{year}} OpenSecret. All rights reserved." + } +} diff --git a/frontend/public/locales/es.json b/frontend/public/locales/es.json new file mode 100644 index 00000000..ba146981 --- /dev/null +++ b/frontend/public/locales/es.json @@ -0,0 +1,178 @@ +{ + "app": { + "title": "Maple - Chat con IA Privada", + "welcome": "Bienvenido a Maple", + "description": "Chat con IA Privada" + }, + "navigation": { + "home": "Inicio", + "chat": "Chat", + "teams": "Equipos", + "pricing": "Precios", + "downloads": "Descargas", + "about": "Acerca de" + }, + "auth": { + "signIn": "Iniciar sesión", + "signOut": "Cerrar sesión", + "signUp": "Registrarse", + "login": "Iniciar sesión", + "logout": "Cerrar sesión", + "email": "Correo electrónico", + "password": "Contraseña", + "confirmPassword": "Confirmar contraseña", + "forgotPassword": "¿Olvidaste tu contraseña?", + "resetPassword": "Restablecer contraseña", + "createAccount": "Crear cuenta", + "alreadyHaveAccount": "¿Ya tienes una cuenta?", + "dontHaveAccount": "¿No tienes una cuenta?", + "signInWithApple": "Iniciar sesión con Apple", + "signInWithGoogle": "Iniciar sesión con Google", + "verification": "Verificación", + "verifyEmail": "Verifica tu dirección de correo", + "checkEmail": "Por favor revisa tu correo para el enlace de verificación" + }, + "chat": { + "newChat": "Chat nuevo", + "chatHistory": "Historial de chat", + "typeMessage": "Escribe tu mensaje...", + "send": "Enviar", + "thinking": "Pensando...", + "selectModel": "Seleccionar modelo", + "systemPrompt": "Prompt del sistema", + "attachFile": "Adjuntar archivo", + "deleteChat": "Eliminar chat", + "renameChat": "Renombrar chat", + "confirmDelete": "¿Seguro que quieres eliminar este chat?", + "enterChatName": "Ingresa un nombre para el chat", + "untitledChat": "Chat sin título" + }, + "teams": { + "createTeam": "Crear equipo", + "joinTeam": "Unirse a equipo", + "teamName": "Nombre del equipo", + "teamMembers": "Miembros del equipo", + "inviteMembers": "Invitar miembros", + "manageTeam": "Administrar equipo", + "leaveTeam": "Abandonar equipo", + "deleteTeam": "Eliminar equipo", + "teamSettings": "Configuración del equipo", + "pendingInvites": "Invitaciones pendientes", + "teamAdmin": "Administrador de equipo", + "teamMember": "Miembro de equipo" + }, + "billing": { + "subscription": "Suscripción", + "billing": "Facturación", + "usage": "Uso", + "credits": "Créditos", + "upgrade": "Mejorar plan", + "downgrade": "Reducir plan", + "cancel": "Cancelar", + "paymentMethod": "Método de pago", + "billingHistory": "Historial de facturación", + "currentPlan": "Plan actual", + "freePlan": "Plan gratuito", + "proPlan": "Plan Pro", + "enterprisePlan": "Plan Empresa" + }, + "button": { + "save": "Guardar", + "cancel": "Cancelar", + "delete": "Eliminar", + "edit": "Editar", + "create": "Crear", + "update": "Actualizar", + "confirm": "Confirmar", + "close": "Cerrar", + "next": "Siguiente", + "previous": "Anterior", + "submit": "Enviar", + "retry": "Reintentar", + "refresh": "Actualizar", + "loading": "Cargando...", + "copy": "Copiar", + "copied": "¡Copiado!", + "download": "Descargar", + "upload": "Subir" + }, + "settings": { + "settings": "Configuración", + "account": "Cuenta", + "profile": "Perfil", + "preferences": "Preferencias", + "notifications": "Notificaciones", + "privacy": "Privacidad", + "security": "Seguridad", + "appearance": "Apariencia", + "language": "Idioma", + "theme": "Tema", + "darkMode": "Modo oscuro", + "lightMode": "Modo claro", + "systemDefault": "Predeterminado del sistema" + }, + "error": { + "general": "Ocurrió un error", + "network": "Error de red - inténtalo más tarde", + "unauthorized": "No estás autorizado para esta acción", + "notFound": "Recurso no encontrado", + "serverError": "Error interno del servidor", + "validationError": "Verifica los datos e inténtalo de nuevo", + "timeout": "Tiempo de espera agotado", + "unknown": "Error desconocido", + "sessionExpired": "Sesión expirada. Inicia sesión de nuevo.", + "invalidCredentials": "Correo o contraseña inválidos", + "emailRequired": "Correo electrónico requerido", + "passwordRequired": "Contraseña requerida", + "emailInvalid": "Ingresa un correo válido", + "passwordTooShort": "La contraseña debe tener al menos 8 caracteres", + "passwordsDontMatch": "Las contraseñas no coinciden" + }, + "success": { + "saved": "Guardado exitosamente", + "deleted": "Eliminado exitosamente", + "updated": "Actualizado exitosamente", + "created": "Creado exitosamente", + "sent": "Enviado exitosamente", + "copied": "Copiado al portapapeles", + "emailSent": "Correo enviado exitosamente", + "passwordReset": "Contraseña restablecida exitosamente", + "accountCreated": "Cuenta creada exitosamente", + "signedIn": "Sesión iniciada exitosamente", + "signedOut": "Sesión cerrada exitosamente" + }, + "common": { + "yes": "Sí", + "no": "No", + "ok": "Aceptar", + "or": "o", + "and": "y", + "optional": "Opcional", + "required": "Requerido", + "search": "Buscar", + "filter": "Filtrar", + "sort": "Ordenar", + "name": "Nombre", + "email": "Correo electrónico", + "date": "Fecha", + "time": "Hora", + "status": "Estado", + "active": "Activo", + "inactive": "Inactivo", + "enabled": "Habilitado", + "disabled": "Deshabilitado", + "public": "Público", + "private": "Privado", + "examples": "Ejemplos" + }, + "footer": { + "privacy": "Política de privacidad", + "terms": "Términos del servicio", + "contact": "Contacto", + "support": "Soporte", + "documentation": "Documentación", + "status": "Estado", + "changelog": "Registro de cambios", + "copyright": "© {{year}} OpenSecret. Todos los derechos reservados." + } +} diff --git a/frontend/public/locales/fr.json b/frontend/public/locales/fr.json new file mode 100644 index 00000000..15a940ea --- /dev/null +++ b/frontend/public/locales/fr.json @@ -0,0 +1,178 @@ +{ + "app": { + "title": "Maple - Chat IA Privé", + "welcome": "Bienvenue sur Maple", + "description": "Chat IA Privé" + }, + "navigation": { + "home": "Accueil", + "chat": "Chat", + "teams": "Équipes", + "pricing": "Tarifs", + "downloads": "Téléchargements", + "about": "À propos" + }, + "auth": { + "signIn": "Se connecter", + "signOut": "Se déconnecter", + "signUp": "S'inscrire", + "login": "Connexion", + "logout": "Déconnexion", + "email": "E-mail", + "password": "Mot de passe", + "confirmPassword": "Confirmer le mot de passe", + "forgotPassword": "Mot de passe oublié ?", + "resetPassword": "Réinitialiser le mot de passe", + "createAccount": "Créer un compte", + "alreadyHaveAccount": "Vous avez déjà un compte ?", + "dontHaveAccount": "Vous n'avez pas de compte ?", + "signInWithApple": "Se connecter avec Apple", + "signInWithGoogle": "Se connecter avec Google", + "verification": "Vérification", + "verifyEmail": "Vérifiez votre adresse e-mail", + "checkEmail": "Veuillez vérifier vos e-mails pour le lien de vérification" + }, + "chat": { + "newChat": "Nouveau chat", + "chatHistory": "Historique des chats", + "typeMessage": "Saisissez votre message...", + "send": "Envoyer", + "thinking": "Réflexion...", + "selectModel": "Sélectionner un modèle", + "systemPrompt": "Prompt système", + "attachFile": "Joindre un fichier", + "deleteChat": "Supprimer le chat", + "renameChat": "Renommer le chat", + "confirmDelete": "Confirmez-vous la suppression de ce chat ?", + "enterChatName": "Entrez un nom de chat", + "untitledChat": "Chat sans titre" + }, + "teams": { + "createTeam": "Créer une équipe", + "joinTeam": "Rejoindre une équipe", + "teamName": "Nom de l'équipe", + "teamMembers": "Membres de l'équipe", + "inviteMembers": "Inviter des membres", + "manageTeam": "Gérer l'équipe", + "leaveTeam": "Quitter l'équipe", + "deleteTeam": "Supprimer l'équipe", + "teamSettings": "Paramètres de l'équipe", + "pendingInvites": "Invitations en attente", + "teamAdmin": "Administrateur d'équipe", + "teamMember": "Membre d'équipe" + }, + "billing": { + "subscription": "Abonnement", + "billing": "Facturation", + "usage": "Utilisation", + "credits": "Crédits", + "upgrade": "Améliorer", + "downgrade": "Rétrograder", + "cancel": "Annuler", + "paymentMethod": "Moyen de paiement", + "billingHistory": "Historique de facturation", + "currentPlan": "Formule actuelle", + "freePlan": "Formule gratuite", + "proPlan": "Formule Pro", + "enterprisePlan": "Formule Entreprise" + }, + "button": { + "save": "Enregistrer", + "cancel": "Annuler", + "delete": "Supprimer", + "edit": "Modifier", + "create": "Créer", + "update": "Mettre à jour", + "confirm": "Confirmer", + "close": "Fermer", + "next": "Suivant", + "previous": "Précédent", + "submit": "Soumettre", + "retry": "Réessayer", + "refresh": "Actualiser", + "loading": "Chargement...", + "copy": "Copier", + "copied": "Copié !", + "download": "Télécharger", + "upload": "Téléverser" + }, + "settings": { + "settings": "Paramètres", + "account": "Compte", + "profile": "Profil", + "preferences": "Préférences", + "notifications": "Notifications", + "privacy": "Confidentialité", + "security": "Sécurité", + "appearance": "Apparence", + "language": "Langue", + "theme": "Thème", + "darkMode": "Mode sombre", + "lightMode": "Mode clair", + "systemDefault": "Par défaut du système" + }, + "error": { + "general": "Une erreur est survenue", + "network": "Erreur réseau - veuillez réessayer plus tard", + "unauthorized": "Action non autorisée", + "notFound": "Ressource introuvable", + "serverError": "Erreur interne du serveur", + "validationError": "Veuillez vérifier vos informations et réessayer", + "timeout": "Délai expiré", + "unknown": "Erreur inconnue", + "sessionExpired": "Session expirée. Veuillez vous reconnecter.", + "invalidCredentials": "E-mail ou mot de passe invalide", + "emailRequired": "E-mail requis", + "passwordRequired": "Mot de passe requis", + "emailInvalid": "Veuillez saisir un e-mail valide", + "passwordTooShort": "Le mot de passe doit contenir au moins 8 caractères", + "passwordsDontMatch": "Les mots de passe ne correspondent pas" + }, + "success": { + "saved": "Enregistré avec succès", + "deleted": "Supprimé avec succès", + "updated": "Mis à jour avec succès", + "created": "Créé avec succès", + "sent": "Envoyé avec succès", + "copied": "Copié dans le presse-papiers", + "emailSent": "E-mail envoyé avec succès", + "passwordReset": "Mot de passe réinitialisé avec succès", + "accountCreated": "Compte créé avec succès", + "signedIn": "Connexion réussie", + "signedOut": "Déconnexion réussie" + }, + "common": { + "yes": "Oui", + "no": "Non", + "ok": "OK", + "or": "ou", + "and": "et", + "optional": "Optionnel", + "required": "Requis", + "search": "Rechercher", + "filter": "Filtrer", + "sort": "Trier", + "name": "Nom", + "email": "E-mail", + "date": "Date", + "time": "Heure", + "status": "Statut", + "active": "Actif", + "inactive": "Inactif", + "enabled": "Activé", + "disabled": "Désactivé", + "public": "Public", + "private": "Privé", + "examples": "Exemples" + }, + "footer": { + "privacy": "Politique de confidentialité", + "terms": "Conditions d'utilisation", + "contact": "Contact", + "support": "Support", + "documentation": "Documentation", + "status": "État", + "changelog": "Journal des modifications", + "copyright": "© {{year}} OpenSecret. Tous droits réservés." + } +} diff --git a/frontend/public/locales/pt.json b/frontend/public/locales/pt.json new file mode 100644 index 00000000..860f96a1 --- /dev/null +++ b/frontend/public/locales/pt.json @@ -0,0 +1,178 @@ +{ + "app": { + "title": "Maple - Chat IA Privado", + "welcome": "Bem-vindo ao Maple", + "description": "Chat com IA Privada" + }, + "navigation": { + "home": "Início", + "chat": "Chat", + "teams": "Equipes", + "pricing": "Preços", + "downloads": "Downloads", + "about": "Sobre" + }, + "auth": { + "signIn": "Entrar", + "signOut": "Sair", + "signUp": "Cadastrar", + "login": "Login", + "logout": "Logout", + "email": "E-mail", + "password": "Senha", + "confirmPassword": "Confirmar senha", + "forgotPassword": "Esqueceu a senha?", + "resetPassword": "Redefinir senha", + "createAccount": "Criar conta", + "alreadyHaveAccount": "Já tem uma conta?", + "dontHaveAccount": "Não tem uma conta?", + "signInWithApple": "Entrar com Apple", + "signInWithGoogle": "Entrar com Google", + "verification": "Verificação", + "verifyEmail": "Verifique seu e-mail", + "checkEmail": "Verifique seu e-mail para o link de verificação" + }, + "chat": { + "newChat": "Novo chat", + "chatHistory": "Histórico de chat", + "typeMessage": "Digite sua mensagem...", + "send": "Enviar", + "thinking": "Pensando...", + "selectModel": "Selecionar modelo", + "systemPrompt": "Prompt do sistema", + "attachFile": "Anexar arquivo", + "deleteChat": "Excluir chat", + "renameChat": "Renomear chat", + "confirmDelete": "Tem certeza que deseja excluir este chat?", + "enterChatName": "Digite um nome para o chat", + "untitledChat": "Chat sem título" + }, + "teams": { + "createTeam": "Criar equipe", + "joinTeam": "Entrar em equipe", + "teamName": "Nome da equipe", + "teamMembers": "Membros da equipe", + "inviteMembers": "Convidar membros", + "manageTeam": "Gerenciar equipe", + "leaveTeam": "Sair da equipe", + "deleteTeam": "Excluir equipe", + "teamSettings": "Configurações da equipe", + "pendingInvites": "Convites pendentes", + "teamAdmin": "Administrador da equipe", + "teamMember": "Membro da equipe" + }, + "billing": { + "subscription": "Assinatura", + "billing": "Cobrança", + "usage": "Uso", + "credits": "Créditos", + "upgrade": "Melhorar plano", + "downgrade": "Rebaixar plano", + "cancel": "Cancelar", + "paymentMethod": "Método de pagamento", + "billingHistory": "Histórico de cobrança", + "currentPlan": "Plano atual", + "freePlan": "Plano gratuito", + "proPlan": "Plano Pro", + "enterprisePlan": "Plano Empresarial" + }, + "button": { + "save": "Salvar", + "cancel": "Cancelar", + "delete": "Excluir", + "edit": "Editar", + "create": "Criar", + "update": "Atualizar", + "confirm": "Confirmar", + "close": "Fechar", + "next": "Próximo", + "previous": "Anterior", + "submit": "Enviar", + "retry": "Tentar novamente", + "refresh": "Atualizar", + "loading": "Carregando...", + "copy": "Copiar", + "copied": "Copiado!", + "download": "Baixar", + "upload": "Enviar" + }, + "settings": { + "settings": "Configurações", + "account": "Conta", + "profile": "Perfil", + "preferences": "Preferências", + "notifications": "Notificações", + "privacy": "Privacidade", + "security": "Segurança", + "appearance": "Aparência", + "language": "Idioma", + "theme": "Tema", + "darkMode": "Modo escuro", + "lightMode": "Modo claro", + "systemDefault": "Padrão do sistema" + }, + "error": { + "general": "Ocorreu um erro", + "network": "Erro de rede - tente novamente mais tarde", + "unauthorized": "Ação não autorizada", + "notFound": "Recurso não encontrado", + "serverError": "Erro interno do servidor", + "validationError": "Verifique os dados e tente novamente", + "timeout": "Tempo limite excedido", + "unknown": "Erro desconhecido", + "sessionExpired": "Sessão expirada. Faça login novamente.", + "invalidCredentials": "E-mail ou senha inválidos", + "emailRequired": "E-mail obrigatório", + "passwordRequired": "Senha obrigatória", + "emailInvalid": "Digite um e-mail válido", + "passwordTooShort": "Senha deve ter pelo menos 8 caracteres", + "passwordsDontMatch": "As senhas não coincidem" + }, + "success": { + "saved": "Salvo com sucesso", + "deleted": "Excluído com sucesso", + "updated": "Atualizado com sucesso", + "created": "Criado com sucesso", + "sent": "Enviado com sucesso", + "copied": "Copiado para a área de transferência", + "emailSent": "E-mail enviado com sucesso", + "passwordReset": "Senha redefinida com sucesso", + "accountCreated": "Conta criada com sucesso", + "signedIn": "Login realizado com sucesso", + "signedOut": "Logout realizado com sucesso" + }, + "common": { + "yes": "Sim", + "no": "Não", + "ok": "OK", + "or": "ou", + "and": "e", + "optional": "Opcional", + "required": "Obrigatório", + "search": "Pesquisar", + "filter": "Filtrar", + "sort": "Ordenar", + "name": "Nome", + "email": "E-mail", + "date": "Data", + "time": "Hora", + "status": "Status", + "active": "Ativo", + "inactive": "Inativo", + "enabled": "Habilitado", + "disabled": "Desabilitado", + "public": "Público", + "private": "Privado", + "examples": "Exemplos" + }, + "footer": { + "privacy": "Política de Privacidade", + "terms": "Termos de Serviço", + "contact": "Contato", + "support": "Suporte", + "documentation": "Documentação", + "status": "Status", + "changelog": "Registro de Alterações", + "copyright": "© {{year}} OpenSecret. Todos os direitos reservados." + } +} diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 9ce367f2..62d1e7da 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -29,6 +29,7 @@ tauri-plugin = "2.1.1" tauri-plugin-deep-link = "2" tauri-plugin-opener = "2" tauri-plugin-os = "2" +tauri-plugin-localization = "0.2" tauri-plugin-sign-in-with-apple = "1.0.2" tokio = { version = "1.0", features = ["time"] } once_cell = "1.18.0" diff --git a/frontend/src-tauri/gen/apple/maple_iOS/Info.plist b/frontend/src-tauri/gen/apple/maple_iOS/Info.plist index 51603792..4b3be393 100644 --- a/frontend/src-tauri/gen/apple/maple_iOS/Info.plist +++ b/frontend/src-tauri/gen/apple/maple_iOS/Info.plist @@ -62,5 +62,12 @@ Maple needs access to your photo library to upload images to your AI conversations. NSCameraUsageDescription Maple needs access to your camera to take photos for your AI conversations. + CFBundleLocalizations + + en + fr + es + pt + \ No newline at end of file diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index a78e400b..576617b5 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -22,6 +22,7 @@ pub fn run() { .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_localization::init()) .setup(|app| { // Set up the deep link handler // Use a cloned handle with 'static lifetime @@ -204,7 +205,8 @@ pub fn run() { ) .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_opener::init()) - .plugin(tauri_plugin_os::init()); + .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_localization::init()); // Only add the Apple Sign In plugin on iOS #[cfg(all(not(desktop), target_os = "ios"))] diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 70e75e77..c8e27d89 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -46,6 +46,13 @@ "csp": "default-src 'self'; connect-src 'self' https://opensecret.cloud https://*.opensecret.cloud https://trymaple.ai https://*.trymaple.ai https://secretgpt.ai https://*.secretgpt.ai https://*.maple-ca8.pages.dev https://raw.githubusercontent.com localhost:*; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:" } }, + "tauri": { + "allowlist": { + "protocol": { + "asset": true + } + } + }, "bundle": { "active": true, "targets": "all", diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index de8ed404..02a11a5b 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -4,8 +4,10 @@ import { Button } from "./ui/button"; import { useOpenSecret } from "@opensecret/react"; import { Menu, X } from "lucide-react"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; export function TopNav() { + const { t } = useTranslation(); const os = useOpenSecret(); const navigate = useNavigate(); const matchRoute = useMatchRoute(); @@ -50,9 +52,9 @@ export function TopNav() { {/* Desktop Navigation */}

- Pricing + {t('navigation.pricing')} Proof - Teams + {t('navigation.teams')} navigate({ to: "/" })} className="bg-[#9469F8] text-[#111111] hover:bg-[#A57FF9] transition-colors" > - Chat + {t('navigation.chat')} ) : ( )} @@ -99,13 +101,13 @@ export function TopNav() {
setMobileMenuOpen(false)}> - Pricing + {t('navigation.pricing')} setMobileMenuOpen(false)}> Proof setMobileMenuOpen(false)}> - Teams + {t('navigation.teams')} - - - ); -} +// Initialize localization BEFORE we render anything +initI18n().then(() => { + console.log('[main] i18n initialized, rendering app...'); + + // Render the app + const rootElement = document.getElementById("root")!; + if (!rootElement.innerHTML) { + const root = createRoot(rootElement); + root.render( + + + + ); + } +}).catch((error) => { + console.error('[main] Failed to initialize i18n:', error); + + // Still render the app even if i18n fails, but show a warning + const rootElement = document.getElementById("root")!; + if (!rootElement.innerHTML) { + const root = createRoot(rootElement); + root.render( + + + + ); + } +}); diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index f902daeb..54300b66 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -8,6 +8,7 @@ import { Sidebar, SidebarToggle } from "@/components/Sidebar"; import { cva } from "class-variance-authority"; import { InfoContent } from "@/components/Explainer"; import { useState, useCallback, useEffect } from "react"; +import { useTranslation } from 'react-i18next'; import { Card, CardHeader } from "@/components/ui/card"; import { VerificationModal } from "@/components/VerificationModal"; import { TopNav } from "@/components/TopNav"; @@ -50,6 +51,7 @@ export const Route = createFileRoute("/")({ }); function Index() { + const { t } = useTranslation(); const navigate = useNavigate(); const localState = useLocalState(); @@ -162,7 +164,7 @@ function Index() { />

- Private AI Chat + {t('app.description')}

diff --git a/frontend/src/utils/i18n.ts b/frontend/src/utils/i18n.ts new file mode 100644 index 00000000..b0d0477a --- /dev/null +++ b/frontend/src/utils/i18n.ts @@ -0,0 +1,83 @@ +import i18next, { InitOptions } from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import { invoke } from '@tauri-apps/api/core'; + +/** + * Resolve the locale to use: + * 1. Try the native Tauri localization plugin. + * 2. Fallback to the browser language. + */ +async function resolveLocale(): Promise { + try { + const native = await invoke('plugin:localization|get_locale'); + if (native) { + console.log('[i18n] Native locale detected:', native); + return native; + } + } catch (e) { + console.warn('[i18n] Native locale unavailable, using navigator.language', e); + } + + const browserLocale = navigator.language || 'en-US'; + console.log('[i18n] Using browser locale:', browserLocale); + return browserLocale; +} + +/** + * Load the JSON file that matches the locale. + * Vite's import.meta.glob creates a map of all json files in the locales folder. + * The path needs to be relative to the project root where public/ is located. + */ +const localeModules = import.meta.glob('/public/locales/*.json') as Record< + string, + () => Promise<{ default: unknown }> +>; + +async function loadResources(requested: string) { + const short = requested.split('-')[0]; // en-US → en + const path = `/public/locales/${short}.json`; + + console.log('[i18n] Looking for locale file:', path); + console.log('[i18n] Available locale modules:', Object.keys(localeModules)); + + const loader = localeModules[path]; + if (!loader) { + console.warn(`[i18n] Locale ${short} not found – falling back to English`); + const englishLoader = localeModules['/public/locales/en.json']; + if (!englishLoader) { + console.error('[i18n] English fallback not found!'); + return { en: { translation: {} } }; + } + const mod = await englishLoader(); + return { en: { translation: mod.default as Record } }; + } + + const mod = await loader(); + return { [short]: { translation: mod.default as Record } }; +} + +/** + * Initialize i18next – call once at startup. + */ +export async function initI18n(): Promise { + console.log('[i18n] Initializing i18next...'); + + const locale = await resolveLocale(); + const resources = await loadResources(locale); + const short = locale.split('-')[0]; // en-US → en + + console.log('[i18n] Using locale:', short); + console.log('[i18n] Resources loaded:', Object.keys(resources)); + + const options: InitOptions = { + lng: short, + fallbackLng: 'en', + resources, + interpolation: { escapeValue: false }, + react: { useSuspense: false }, + debug: import.meta.env.DEV // Enable debug logging in development + }; + + await i18next.use(initReactI18next).init(options); + console.log('[i18n] i18next initialized successfully'); +}