Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions frontend/README-localization.md
Original file line number Diff line number Diff line change
@@ -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
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>fr</string>
<string>es</string>
<string>de</string> <!-- Add your new language -->
</array>
```

### 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 (
<div>
<h1>{t('app.title')}</h1>
<button>{t('button.save')}</button>
</div>
);
}
```

### 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.
Binary file modified frontend/bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
178 changes: 178 additions & 0 deletions frontend/public/locales/en.json
Original file line number Diff line number Diff line change
@@ -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."
}
}
Loading
Loading