Skip to content

Commit 548a39a

Browse files
committed
Add i18n support with language switcher and translations
1 parent 98eda9d commit 548a39a

File tree

10 files changed

+1358
-20
lines changed

10 files changed

+1358
-20
lines changed

public/i18n-test.html

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>i18n Test</title>
7+
<style>
8+
body {
9+
font-family: 'Arial', sans-serif;
10+
max-width: 800px;
11+
margin: 0 auto;
12+
padding: 20px;
13+
}
14+
.result {
15+
margin: 10px 0;
16+
padding: 10px;
17+
border: 1px solid #ddd;
18+
border-radius: 4px;
19+
}
20+
.language-buttons {
21+
margin: 20px 0;
22+
}
23+
button {
24+
padding: 8px 16px;
25+
margin-right: 10px;
26+
cursor: pointer;
27+
}
28+
.active {
29+
background-color: #e02337;
30+
color: white;
31+
border-color: #e02337;
32+
}
33+
</style>
34+
</head>
35+
<body>
36+
<h1>i18n Test Page</h1>
37+
38+
<div class="language-buttons">
39+
<button data-lang="en">English</button>
40+
<button data-lang="fr">Français</button>
41+
<button data-lang="de">Deutsch</button>
42+
</div>
43+
44+
<h2>Current Language: <span id="current-lang"></span></h2>
45+
46+
<h3>Basic Translations</h3>
47+
<div class="result" id="app-name"></div>
48+
<div class="result" id="dashboard"></div>
49+
<div class="result" id="login"></div>
50+
51+
<h3>Interpolation</h3>
52+
<div class="result" id="page-not-found"></div>
53+
54+
<script type="module">
55+
// Import directly from ESM
56+
const { localizer, _t } = await import('https://esm.sh/@profullstack/[email protected]');
57+
58+
// Available languages
59+
const AVAILABLE_LANGUAGES = ['en', 'fr', 'de'];
60+
61+
// Flatten a nested object into a flat object with dot notation keys
62+
function flattenObject(obj, prefix = '') {
63+
return Object.keys(obj).reduce((acc, key) => {
64+
const prefixedKey = prefix ? `${prefix}.${key}` : key;
65+
66+
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
67+
Object.assign(acc, flattenObject(obj[key], prefixedKey));
68+
} else {
69+
acc[prefixedKey] = obj[key];
70+
}
71+
72+
return acc;
73+
}, {});
74+
}
75+
76+
// Load translations
77+
async function loadTranslations() {
78+
try {
79+
// Load translations for all languages
80+
for (const lang of AVAILABLE_LANGUAGES) {
81+
const response = await fetch(`/i18n/${lang}.json`);
82+
if (!response.ok) {
83+
throw new Error(`Failed to load ${lang}.json: ${response.status}`);
84+
}
85+
const translations = await response.json();
86+
console.log(`Loaded translations for ${lang}:`, translations);
87+
88+
// Flatten the nested translations
89+
const flattenedTranslations = flattenObject(translations);
90+
console.log(`Flattened translations for ${lang}:`, flattenedTranslations);
91+
92+
// Load the flattened translations
93+
localizer.loadTranslations(lang, flattenedTranslations);
94+
}
95+
96+
// Set initial language to English
97+
localizer.setLanguage('en');
98+
99+
// Update UI
100+
updateUI();
101+
102+
// Set active button
103+
document.querySelector('button[data-lang="en"]').classList.add('active');
104+
} catch (error) {
105+
console.error('Error loading translations:', error);
106+
}
107+
}
108+
109+
// Update UI with translations
110+
function updateUI() {
111+
const currentLang = localizer.getLanguage();
112+
document.getElementById('current-lang').textContent = currentLang;
113+
114+
// Basic translations
115+
document.getElementById('app-name').textContent = `app_name: ${_t('app_name')}`;
116+
document.getElementById('dashboard').textContent = `navigation.dashboard: ${_t('navigation.dashboard')}`;
117+
document.getElementById('login').textContent = `navigation.login: ${_t('navigation.login')}`;
118+
119+
// Interpolation
120+
document.getElementById('page-not-found').textContent =
121+
`errors.page_not_found_message: ${_t('errors.page_not_found_message', { path: '/example' })}`;
122+
}
123+
124+
// Set up language buttons
125+
document.querySelectorAll('button[data-lang]').forEach(button => {
126+
button.addEventListener('click', () => {
127+
const lang = button.getAttribute('data-lang');
128+
localizer.setLanguage(lang);
129+
130+
// Update active state
131+
document.querySelectorAll('button[data-lang]').forEach(btn => {
132+
btn.classList.remove('active');
133+
});
134+
button.classList.add('active');
135+
136+
// Update UI
137+
updateUI();
138+
});
139+
});
140+
141+
// Initialize
142+
loadTranslations();
143+
</script>
144+
</body>
145+
</html>

public/i18n/README.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Internationalization (i18n) and Localization (l10n) Implementation
2+
3+
This document explains how we implemented internationalization and localization in the PDF project.
4+
5+
## Overview
6+
7+
We've implemented a complete i18n/l10n solution that allows the application to be translated into multiple languages. The implementation consists of:
8+
9+
1. A standalone localizer library (`@profullstack/localizer`) that provides the core functionality
10+
2. Translation files for each supported language
11+
3. Integration with the application's UI components
12+
4. A language switcher component
13+
14+
## Localizer Library
15+
16+
The `@profullstack/localizer` library is a lightweight, dependency-free library that provides:
17+
18+
- Simple API for translating text
19+
- Support for multiple languages
20+
- Fallback to default language when a translation is missing
21+
- Interpolation of variables in translations
22+
- Support for pluralization
23+
- Works in both browser and Node.js environments
24+
25+
The library is published as an npm package and can be used in any JavaScript project.
26+
27+
## Translation Files
28+
29+
Translation files are stored in the `public/i18n` directory as JSON files, with one file per language:
30+
31+
- `en.json`: English (default)
32+
- `fr.json`: French
33+
- `de.json`: German
34+
35+
Each file contains translations for all the text in the application, organized in a nested structure:
36+
37+
```json
38+
{
39+
"navigation": {
40+
"dashboard": "Dashboard",
41+
"api_docs": "API Docs",
42+
"api_keys": "API Keys",
43+
...
44+
},
45+
"errors": {
46+
"page_not_found": "404 - Page Not Found",
47+
...
48+
},
49+
...
50+
}
51+
```
52+
53+
## Integration with the Application
54+
55+
The integration is handled by the `public/js/i18n.js` file, which:
56+
57+
1. Imports the localizer library
58+
2. Loads the translation files
59+
3. Sets the initial language based on browser preference or localStorage
60+
4. Provides functions to translate text and change the language
61+
5. Observes DOM changes to translate dynamically added content
62+
63+
Since the localizer library expects flat key-value pairs, we implemented a `flattenObject` function that converts the nested translation structure to a flat one:
64+
65+
```javascript
66+
function flattenObject(obj, prefix = '') {
67+
return Object.keys(obj).reduce((acc, key) => {
68+
const prefixedKey = prefix ? `${prefix}.${key}` : key;
69+
70+
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
71+
Object.assign(acc, flattenObject(obj[key], prefixedKey));
72+
} else {
73+
acc[prefixedKey] = obj[key];
74+
}
75+
76+
return acc;
77+
}, {});
78+
}
79+
```
80+
81+
## Language Switcher Component
82+
83+
We created a `language-switcher` web component (`public/js/components/language-switcher.js`) that allows users to switch between languages. The component:
84+
85+
1. Displays a dropdown with available languages
86+
2. Shows the current language
87+
3. Allows users to select a new language
88+
4. Updates the UI when the language changes
89+
90+
## Usage in HTML
91+
92+
To translate text in HTML, we use the `data-i18n` attribute:
93+
94+
```html
95+
<span data-i18n="navigation.dashboard">Dashboard</span>
96+
```
97+
98+
For interpolation, we use the `data-i18n-params` attribute:
99+
100+
```html
101+
<span data-i18n="errors.page_not_found_message" data-i18n-params='{"path":"/example"}'>
102+
The page "/example" could not be found.
103+
</span>
104+
```
105+
106+
## Usage in JavaScript
107+
108+
To translate text in JavaScript, we use the `_t` function:
109+
110+
```javascript
111+
import { _t } from '/js/i18n.js';
112+
113+
const message = _t('common.loading');
114+
const welcomeMessage = _t('auth.welcome', { name: 'John' });
115+
```
116+
117+
## Demo Page
118+
119+
We created a demo page (`public/views/i18n-demo.html`) that showcases the localization features:
120+
121+
1. Language selection
122+
2. Basic translations
123+
3. Interpolation
124+
4. JavaScript API
125+
126+
## Adding a New Language
127+
128+
To add a new language:
129+
130+
1. Create a new translation file in the `public/i18n` directory (e.g., `es.json` for Spanish)
131+
2. Add the language code to the `AVAILABLE_LANGUAGES` array in `public/js/i18n.js`
132+
3. Add the language name to the `getLanguageName` function in both `public/js/i18n.js` and `public/js/components/language-switcher.js`
133+
134+
## Adding New Translations
135+
136+
To add new translations:
137+
138+
1. Add the new keys and values to each language file
139+
2. Use the `data-i18n` attribute or `_t` function to translate the text
140+
141+
## Future Improvements
142+
143+
Possible future improvements include:
144+
145+
1. Adding more languages
146+
2. Supporting region-specific languages (e.g., `en-US`, `en-GB`)
147+
3. Adding a translation management system
148+
4. Implementing automatic translation detection
149+
5. Adding support for RTL languages

public/i18n/de.json

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"app_name": "convert2doc",
3+
"navigation": {
4+
"dashboard": "Dashboard",
5+
"api_docs": "API-Dokumentation",
6+
"api_keys": "API-Schlüssel",
7+
"state_demo": "Status-Demo",
8+
"i18n_demo": "i18n-Demo",
9+
"login": "Anmelden",
10+
"register": "Registrieren",
11+
"settings": "Einstellungen",
12+
"logout": "Abmelden",
13+
"theme": "Thema",
14+
"language": "Sprache"
15+
},
16+
"errors": {
17+
"page_not_found": "404 - Seite nicht gefunden",
18+
"page_not_found_message": "Die Seite \"${path}\" konnte nicht gefunden werden.",
19+
"go_back_home": "Zurück zur Startseite",
20+
"error_loading_page": "Fehler beim Laden der Seite",
21+
"login_failed": "Anmeldung fehlgeschlagen: ",
22+
"invalid_credentials": "Ungültige Anmeldedaten. Bitte überprüfen Sie Ihre E-Mail und Ihr Passwort.",
23+
"auth_server_error": "Authentifizierungsserverfehler. Bitte versuchen Sie es später erneut.",
24+
"registration_failed": "Registrierung fehlgeschlagen: ",
25+
"passwords_not_match": "Passwörter stimmen nicht überein"
26+
},
27+
"auth": {
28+
"email": "E-Mail",
29+
"password": "Passwort",
30+
"confirm_password": "Passwort bestätigen",
31+
"new_password": "Neues Passwort",
32+
"logging_in": "Anmeldung läuft...",
33+
"checking_auth": "Authentifizierungsstatus wird überprüft...",
34+
"change_password_required": "Aus Sicherheitsgründen ändern Sie bitte Ihr Standardpasswort, bevor Sie fortfahren.",
35+
"login_successful": "Anmeldung erfolgreich, Weiterleitung zur API-Schlüsselseite",
36+
"profile_updated": "Profil erfolgreich aktualisiert!",
37+
"password_changed": "Passwort erfolgreich geändert!",
38+
"delete_account_confirm": "Sind Sie sicher, dass Sie Ihr Konto löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
39+
"delete_account": "Konto löschen",
40+
"delete": "Löschen",
41+
"cancel": "Abbrechen"
42+
},
43+
"subscription": {
44+
"active_subscription_required": "Sie benötigen ein aktives Abonnement, um auf das Dashboard zuzugreifen.",
45+
"processing": "Verarbeitung läuft...",
46+
"registration_successful": "Registrierung erfolgreich!",
47+
"payment_instructions": "Bitte senden Sie <strong>${amount} USD</strong> (<strong>${cryptoAmount} ${coin}</strong>) an die folgende Adresse:",
48+
"amount": "Betrag:",
49+
"address": "Adresse:",
50+
"copy": "Kopieren",
51+
"copied": "Kopiert!",
52+
"waiting_payment": "Warten auf Zahlungsbestätigung...",
53+
"payment_received": "Zahlung erhalten! Weiterleitung zu Ihrem Dashboard..."
54+
},
55+
"common": {
56+
"save": "Speichern",
57+
"cancel": "Abbrechen",
58+
"submit": "Absenden",
59+
"loading": "Wird geladen...",
60+
"success": "Erfolg",
61+
"error": "Fehler",
62+
"warning": "Warnung",
63+
"info": "Information",
64+
"yes": "Ja",
65+
"no": "Nein",
66+
"back": "Zurück",
67+
"next": "Weiter",
68+
"continue": "Fortfahren",
69+
"check_status": "Zahlungsstatus prüfen"
70+
}
71+
}

0 commit comments

Comments
 (0)