[call-me] - add support for multilanguages, update dep
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# i18n Support - Quick Start
|
||||
|
||||
The Call-me application now supports multiple languages! 🌍
|
||||
|
||||
## Supported Languages
|
||||
|
||||
- 🇬🇧 English (en)
|
||||
- 🇪🇸 Spanish (es)
|
||||
- 🇫🇷 French (fr)
|
||||
- 🇮🇹 Italian (it)
|
||||
- 🇩🇪 German (de)
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Automatic language detection from browser
|
||||
- ✅ Language persistence in localStorage
|
||||
- ✅ Real-time language switching
|
||||
- ✅ JSON-based translations
|
||||
- ✅ RESTful translations API
|
||||
|
||||
## Usage
|
||||
|
||||
### For Users
|
||||
|
||||
1. Open the application
|
||||
2. Click the sidebar button (users icon)
|
||||
3. Go to "Settings" tab
|
||||
4. Select your preferred language from the dropdown (with flag emojis)
|
||||
|
||||
### For Developers
|
||||
|
||||
**Add translation to HTML:**
|
||||
|
||||
```html
|
||||
<button data-i18n="signIn.button">Sign In</button>
|
||||
<input data-i18n-placeholder="signIn.username" placeholder="Enter username" />
|
||||
```
|
||||
|
||||
**Use in JavaScript:**
|
||||
|
||||
```javascript
|
||||
const text = t('signIn.button'); // Returns: "Sign In"
|
||||
const message = t('room.userJoined', { username: 'John' }); // Returns: "John joined the call"
|
||||
```
|
||||
|
||||
**API Endpoint:**
|
||||
|
||||
```bash
|
||||
GET /translations/:locale
|
||||
# Example: GET /translations/es
|
||||
```
|
||||
|
||||
## Files Structure
|
||||
|
||||
```
|
||||
locales/
|
||||
├── en.json # English translations
|
||||
├── es.json # Spanish translations
|
||||
├── fr.json # French translations
|
||||
├── it.json # Italian translations
|
||||
└── de.json # German translations
|
||||
|
||||
public/
|
||||
└── i18n.js # Client-side i18n library
|
||||
|
||||
doc/
|
||||
└── i18n.md # Complete documentation
|
||||
```
|
||||
|
||||
## Quick Test
|
||||
|
||||
```bash
|
||||
# Test English translations
|
||||
curl http://localhost:8000/translations/en
|
||||
|
||||
# Test Spanish translations
|
||||
curl http://localhost:8000/translations/es
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For complete documentation, see [doc/i18n.md](doc/i18n.md)
|
||||
|
||||
## Based On
|
||||
|
||||
Implementation follows the [Crowdin Node.js i18n guide](https://crowdin.com/blog/nodejs-i18n-and-localization)
|
||||
@@ -13,6 +13,7 @@ const helmet = require('helmet');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
const i18n = require('i18n');
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
// Logs
|
||||
@@ -100,6 +101,20 @@ const corsOptions = {
|
||||
// Create Express application
|
||||
const app = express();
|
||||
|
||||
// Configure i18n
|
||||
i18n.configure({
|
||||
locales: ['en', 'es', 'fr', 'it', 'de'],
|
||||
defaultLocale: 'en',
|
||||
directory: path.join(__dirname, '../locales'),
|
||||
objectNotation: true,
|
||||
updateFiles: false,
|
||||
syncFiles: false,
|
||||
api: {
|
||||
__: 'translate',
|
||||
__n: 'translateN',
|
||||
},
|
||||
});
|
||||
|
||||
// Server configurations
|
||||
const port = process.env.PORT || 4000;
|
||||
const host = process.env.HOST || `http://localhost:${port}`;
|
||||
@@ -199,6 +214,29 @@ app.get('/randomImage', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get translations for a specific language
|
||||
app.get('/translations/:locale', (req, res) => {
|
||||
try {
|
||||
const locale = req.params.locale || 'en';
|
||||
const validLocales = ['en', 'es', 'fr', 'it', 'de'];
|
||||
|
||||
if (!validLocales.includes(locale)) {
|
||||
return res.status(400).json({ error: 'Invalid locale' });
|
||||
}
|
||||
|
||||
const translationPath = path.join(__dirname, '../locales', `${locale}.json`);
|
||||
const translations = JSON.parse(fs.readFileSync(translationPath, 'utf8'));
|
||||
|
||||
res.json({
|
||||
locale: locale,
|
||||
translations: translations,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('Error fetching translations', error.message);
|
||||
res.status(500).json({ error: 'Error loading translations' });
|
||||
}
|
||||
});
|
||||
|
||||
// Direct Join room
|
||||
app.get('/join/', (req, res) => {
|
||||
if (Object.keys(req.query).length > 0) {
|
||||
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
# Internationalization (i18n) Implementation Guide
|
||||
|
||||
This document explains how the internationalization (i18n) system is implemented in the Call-me application.
|
||||
|
||||
## Overview
|
||||
|
||||
The application now supports multiple languages using a JSON-based translation system. The implementation follows best practices from the Crowdin Node.js i18n guide.
|
||||
|
||||
## Supported Languages
|
||||
|
||||
- **English (en)** - Default
|
||||
- **Spanish (es)**
|
||||
- **French (fr)**
|
||||
- **Italian (it)**
|
||||
- **German (de)**
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend (Server-side)
|
||||
|
||||
#### Dependencies
|
||||
|
||||
- **i18n**: Node.js internationalization library
|
||||
|
||||
#### Configuration
|
||||
|
||||
The i18n module is configured in [app/server.js](app/server.js):
|
||||
|
||||
```javascript
|
||||
i18n.configure({
|
||||
locales: ['en', 'es', 'fr', 'it', 'de'],
|
||||
defaultLocale: 'en',
|
||||
directory: path.join(__dirname, '../locales'),
|
||||
objectNotation: true,
|
||||
updateFiles: false,
|
||||
syncFiles: false,
|
||||
});
|
||||
```
|
||||
|
||||
#### API Endpoint
|
||||
|
||||
**GET** `/translations/:locale`
|
||||
|
||||
Returns translations for the specified locale:
|
||||
|
||||
```json
|
||||
{
|
||||
"locale": "en",
|
||||
"translations": {
|
||||
"appName": "Call-me",
|
||||
"signIn": {
|
||||
"title": "Sign In",
|
||||
"username": "Enter username"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend (Client-side)
|
||||
|
||||
#### Core Files
|
||||
|
||||
- **[public/i18n.js](public/i18n.js)**: Client-side i18n library
|
||||
- **[locales/\*.json](locales/)**: Translation files
|
||||
|
||||
#### Key Features
|
||||
|
||||
1. **Automatic Language Detection**
|
||||
- Checks localStorage for saved preference
|
||||
- Falls back to browser language
|
||||
- Defaults to English if unsupported
|
||||
|
||||
2. **Translation Function**
|
||||
|
||||
```javascript
|
||||
t('signIn.username'); // Returns: "Enter username"
|
||||
t('room.userJoined', { username: 'John' }); // Returns: "John joined the call"
|
||||
```
|
||||
|
||||
3. **HTML Attributes**
|
||||
- `data-i18n`: Translates element text content
|
||||
- `data-i18n-placeholder`: Translates input placeholder
|
||||
- `data-i18n-title`: Translates element title/tooltip
|
||||
|
||||
4. **Language Switcher**
|
||||
Located in the Settings tab, allows users to change language in real-time. Each language option is displayed with its corresponding flag emoji (🇬🇧 English, 🇪🇸 Español, 🇫🇷 Français, 🇮🇹 Italiano, 🇩🇪 Deutsch) for easy visual identification.
|
||||
|
||||
## Translation Files Structure
|
||||
|
||||
All translation files are located in the `locales/` directory:
|
||||
|
||||
```
|
||||
locales/
|
||||
├── en.json # English
|
||||
├── es.json # Spanish
|
||||
├── fr.json # French
|
||||
├── it.json # Italian
|
||||
└── de.json # German
|
||||
```
|
||||
|
||||
### JSON Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"appName": "Call-me",
|
||||
"appTitle": "Call-me - Instant Video Calls",
|
||||
"signIn": {
|
||||
"title": "Sign In",
|
||||
"username": "Enter username",
|
||||
"button": "Sign In"
|
||||
},
|
||||
"room": {
|
||||
"users": "Users",
|
||||
"chat": "Chat"
|
||||
},
|
||||
"settings": {
|
||||
"language": "Language",
|
||||
"selectLanguage": "Select Language"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Adding New Languages
|
||||
|
||||
1. Create a new JSON file in `locales/` directory (e.g., `locales/pt.json` for Portuguese)
|
||||
2. Copy the structure from `locales/en.json` and translate all values
|
||||
3. Update the server configuration in `app/server.js`:
|
||||
```javascript
|
||||
i18n.configure({
|
||||
locales: ['en', 'es', 'fr', 'it', 'de', 'pt'], // Add 'pt'
|
||||
// ...
|
||||
});
|
||||
```
|
||||
4. Update the frontend language selector in `public/i18n.js`:
|
||||
```javascript
|
||||
const supportedLocales = ['en', 'es', 'fr', 'it', 'de', 'pt']; // Add 'pt'
|
||||
```
|
||||
5. Add the option to the language selector in `public/index.html` with the appropriate flag emoji:
|
||||
```html
|
||||
<option value="pt">🇵🇹 Português</option>
|
||||
```
|
||||
|
||||
## Adding New Translation Keys
|
||||
|
||||
1. Add the key to all language files in `locales/`:
|
||||
|
||||
```json
|
||||
{
|
||||
"newSection": {
|
||||
"newKey": "Translation text"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Use in HTML with data attributes:
|
||||
|
||||
```html
|
||||
<button data-i18n="newSection.newKey">Default Text</button>
|
||||
```
|
||||
|
||||
3. Or use in JavaScript:
|
||||
```javascript
|
||||
const text = t('newSection.newKey');
|
||||
```
|
||||
|
||||
## Dynamic Text Replacement
|
||||
|
||||
For translations with dynamic values, use placeholder syntax:
|
||||
|
||||
**Translation file:**
|
||||
|
||||
```json
|
||||
{
|
||||
"room": {
|
||||
"userJoined": "__username__ joined the call"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```javascript
|
||||
const message = t('room.userJoined', { username: 'Alice' });
|
||||
// Result: "Alice joined the call"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Key Naming**: Use dot notation for nested keys (e.g., `settings.language`)
|
||||
2. **Consistency**: Keep the same structure across all language files
|
||||
3. **Placeholders**: Use `__placeholder__` format for dynamic values
|
||||
4. **Fallbacks**: Always provide English translations as fallback
|
||||
5. **Context**: Group related translations under common parent keys
|
||||
|
||||
## Testing
|
||||
|
||||
To test the implementation:
|
||||
|
||||
1. Start the server:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
2. Open the application in a browser
|
||||
3. Navigate to Settings tab
|
||||
4. Select different languages from the dropdown
|
||||
5. Verify that UI elements update correctly
|
||||
|
||||
## Integration with Crowdin
|
||||
|
||||
To use with Crowdin for collaborative translation:
|
||||
|
||||
1. Install Crowdin CLI: `npm install -g @crowdin/cli`
|
||||
2. Create a `crowdin.yml` configuration file
|
||||
3. Upload source files: `crowdin upload sources`
|
||||
4. Download translations: `crowdin download`
|
||||
|
||||
## API Documentation
|
||||
|
||||
The translations endpoint is documented in the Swagger API documentation under `/api/v1/docs`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: Translations not loading
|
||||
|
||||
- Check browser console for errors
|
||||
- Verify the locale file exists in `locales/` directory
|
||||
- Ensure the API endpoint is accessible
|
||||
|
||||
**Issue**: Some text not translating
|
||||
|
||||
- Verify the HTML element has the correct `data-i18n` attribute
|
||||
- Check that the translation key exists in the locale file
|
||||
- Ensure `translatePage()` is called after DOM updates
|
||||
|
||||
**Issue**: Language not persisting
|
||||
|
||||
- Check browser localStorage
|
||||
- Ensure the locale is in the `supportedLocales` array
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Add more languages (Portuguese, Chinese, Japanese, etc.)
|
||||
- Implement pluralization support
|
||||
- Add date/time localization
|
||||
- Support RTL languages (Arabic, Hebrew)
|
||||
- Add translation management UI
|
||||
- Integrate with professional translation services
|
||||
|
||||
## Resources
|
||||
|
||||
- [i18n npm package](https://www.npmjs.com/package/i18n)
|
||||
- [Crowdin Node.js i18n Guide](https://crowdin.com/blog/nodejs-i18n-and-localization)
|
||||
- [IANA Language Subtag Registry](https://www.iana.org/assignments/language-subtag-registry)
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"appName": "Call-me",
|
||||
"appTitle": "Call-me - Sofortige Videoanrufe",
|
||||
"appDescription": "Ihre erste Wahl für sofortige Videoanrufe!",
|
||||
"signIn": {
|
||||
"title": "Anmelden",
|
||||
"username": "Benutzernamen eingeben",
|
||||
"button": "Anmelden",
|
||||
"enterUsername": "Bitte geben Sie Ihren Benutzernamen ein"
|
||||
},
|
||||
"room": {
|
||||
"sessionTime": "Sitzungszeit",
|
||||
"localUsername": "Sie",
|
||||
"remoteUsername": "Remote-Benutzer",
|
||||
"waiting": "Warte darauf, dass jemand beitritt...",
|
||||
"connecting": "Verbinden...",
|
||||
"userJoined": "__username__ ist dem Anruf beigetreten",
|
||||
"userLeft": "__username__ hat den Anruf verlassen",
|
||||
"users": "Benutzer",
|
||||
"chat": "Chat",
|
||||
"searchUsers": "Benutzer suchen..."
|
||||
},
|
||||
"controls": {
|
||||
"microphone": "Mikrofon",
|
||||
"camera": "Kamera",
|
||||
"screenShare": "Bildschirm Teilen",
|
||||
"endCall": "Anruf Beenden",
|
||||
"settings": "Einstellungen",
|
||||
"fullscreen": "Vollbild"
|
||||
},
|
||||
"messages": {
|
||||
"microphoneEnabled": "Mikrofon aktiviert",
|
||||
"microphoneDisabled": "Mikrofon deaktiviert",
|
||||
"cameraEnabled": "Kamera aktiviert",
|
||||
"cameraDisabled": "Kamera deaktiviert",
|
||||
"screenShareStarted": "Bildschirmfreigabe gestartet",
|
||||
"screenShareStopped": "Bildschirmfreigabe beendet",
|
||||
"callEnded": "Anruf beendet",
|
||||
"connectionFailed": "Verbindung fehlgeschlagen",
|
||||
"permissionDenied": "Berechtigung verweigert",
|
||||
"error": "Ein Fehler ist aufgetreten",
|
||||
"copied": "In die Zwischenablage kopiert",
|
||||
"invalidPassword": "Ungültiges Passwort"
|
||||
},
|
||||
"errors": {
|
||||
"noUsername": "Benutzername erforderlich",
|
||||
"connectionLost": "Verbindung verloren",
|
||||
"mediaDevices": "Zugriff auf Mediengeräte nicht möglich",
|
||||
"noSupport": "Ihr Browser unterstützt diese Funktion nicht"
|
||||
},
|
||||
"settings": {
|
||||
"language": "Sprache",
|
||||
"selectLanguage": "Sprache Auswählen",
|
||||
"audioInput": "Mikrofon",
|
||||
"videoInput": "Kamera",
|
||||
"audioOutput": "Lautsprecher",
|
||||
"resolution": "Videoauflösung",
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"settings": "Einstellungen",
|
||||
"testDevices": "Geräte Testen",
|
||||
"refresh": "Aktualisieren",
|
||||
"saveMessages": "Nachrichten Speichern",
|
||||
"clearAll": "Alles Löschen"
|
||||
},
|
||||
"chat": {
|
||||
"addEmoji": "Emoji hinzufügen",
|
||||
"typeMessage": "Nachricht eingeben...",
|
||||
"sendMessage": "Nachricht senden"
|
||||
},
|
||||
"api": {
|
||||
"unauthorized": "Nicht autorisierter Zugriff",
|
||||
"invalidApiKey": "Ungültiger API-Schlüssel",
|
||||
"serverError": "Serverfehler",
|
||||
"notFound": "Ressource nicht gefunden"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"appName": "Call-me",
|
||||
"appTitle": "Call-me - Instant Video Calls",
|
||||
"appDescription": "Your Go-To for Instant Video Calls!",
|
||||
"signIn": {
|
||||
"title": "Sign In",
|
||||
"username": "Enter username",
|
||||
"button": "Sign In",
|
||||
"enterUsername": "Please enter your username"
|
||||
},
|
||||
"room": {
|
||||
"sessionTime": "Session Time",
|
||||
"localUsername": "You",
|
||||
"remoteUsername": "Remote User",
|
||||
"waiting": "Waiting for someone to join...",
|
||||
"connecting": "Connecting...",
|
||||
"userJoined": "__username__ joined the call",
|
||||
"userLeft": "__username__ left the call",
|
||||
"users": "Users",
|
||||
"chat": "Chat",
|
||||
"searchUsers": "Search users..."
|
||||
},
|
||||
"controls": {
|
||||
"microphone": "Microphone",
|
||||
"camera": "Camera",
|
||||
"screenShare": "Share Screen",
|
||||
"endCall": "End Call",
|
||||
"settings": "Settings",
|
||||
"fullscreen": "Fullscreen"
|
||||
},
|
||||
"messages": {
|
||||
"microphoneEnabled": "Microphone enabled",
|
||||
"microphoneDisabled": "Microphone disabled",
|
||||
"cameraEnabled": "Camera enabled",
|
||||
"cameraDisabled": "Camera disabled",
|
||||
"screenShareStarted": "Screen sharing started",
|
||||
"screenShareStopped": "Screen sharing stopped",
|
||||
"callEnded": "Call ended",
|
||||
"connectionFailed": "Connection failed",
|
||||
"permissionDenied": "Permission denied",
|
||||
"error": "An error occurred",
|
||||
"copied": "Copied to clipboard",
|
||||
"invalidPassword": "Invalid password"
|
||||
},
|
||||
"errors": {
|
||||
"noUsername": "Username is required",
|
||||
"connectionLost": "Connection lost",
|
||||
"mediaDevices": "Could not access media devices",
|
||||
"noSupport": "Your browser does not support this feature"
|
||||
},
|
||||
"settings": {
|
||||
"language": "Language",
|
||||
"selectLanguage": "Select Language",
|
||||
"audioInput": "Microphone",
|
||||
"videoInput": "Camera",
|
||||
"audioOutput": "Speaker",
|
||||
"resolution": "Video Resolution",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"settings": "Settings",
|
||||
"testDevices": "Test Devices",
|
||||
"refresh": "Refresh",
|
||||
"saveMessages": "Save Messages",
|
||||
"clearAll": "Clear All"
|
||||
},
|
||||
"chat": {
|
||||
"addEmoji": "Add emoji",
|
||||
"typeMessage": "Type a message...",
|
||||
"sendMessage": "Send message"
|
||||
},
|
||||
"api": {
|
||||
"unauthorized": "Unauthorized access",
|
||||
"invalidApiKey": "Invalid API key",
|
||||
"serverError": "Server error",
|
||||
"notFound": "Resource not found"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"appName": "Call-me",
|
||||
"appTitle": "Call-me - Videollamadas Instantáneas",
|
||||
"appDescription": "¡Tu opción para videollamadas instantáneas!",
|
||||
"signIn": {
|
||||
"title": "Iniciar Sesión",
|
||||
"username": "Ingrese nombre de usuario",
|
||||
"button": "Iniciar Sesión",
|
||||
"enterUsername": "Por favor ingrese su nombre de usuario"
|
||||
},
|
||||
"room": {
|
||||
"sessionTime": "Tiempo de Sesión",
|
||||
"localUsername": "Tú",
|
||||
"remoteUsername": "Usuario Remoto",
|
||||
"waiting": "Esperando a que alguien se una...",
|
||||
"connecting": "Conectando...",
|
||||
"userJoined": "__username__ se unió a la llamada",
|
||||
"userLeft": "__username__ salió de la llamada",
|
||||
"users": "Usuarios",
|
||||
"chat": "Chat",
|
||||
"searchUsers": "Buscar usuarios..."
|
||||
},
|
||||
"controls": {
|
||||
"microphone": "Micrófono",
|
||||
"camera": "Cámara",
|
||||
"screenShare": "Compartir Pantalla",
|
||||
"endCall": "Finalizar Llamada",
|
||||
"settings": "Configuración",
|
||||
"fullscreen": "Pantalla Completa"
|
||||
},
|
||||
"messages": {
|
||||
"microphoneEnabled": "Micrófono activado",
|
||||
"microphoneDisabled": "Micrófono desactivado",
|
||||
"cameraEnabled": "Cámara activada",
|
||||
"cameraDisabled": "Cámara desactivada",
|
||||
"screenShareStarted": "Compartir pantalla iniciado",
|
||||
"screenShareStopped": "Compartir pantalla detenido",
|
||||
"callEnded": "Llamada finalizada",
|
||||
"connectionFailed": "Conexión fallida",
|
||||
"permissionDenied": "Permiso denegado",
|
||||
"error": "Ocurrió un error",
|
||||
"copied": "Copiado al portapapeles",
|
||||
"invalidPassword": "Contraseña inválida"
|
||||
},
|
||||
"errors": {
|
||||
"noUsername": "Se requiere nombre de usuario",
|
||||
"connectionLost": "Conexión perdida",
|
||||
"mediaDevices": "No se pudo acceder a los dispositivos multimedia",
|
||||
"noSupport": "Su navegador no soporta esta función"
|
||||
},
|
||||
"settings": {
|
||||
"language": "Idioma",
|
||||
"selectLanguage": "Seleccionar Idioma",
|
||||
"audioInput": "Micrófono",
|
||||
"videoInput": "Cámara",
|
||||
"audioOutput": "Altavoz",
|
||||
"resolution": "Resolución de Video",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"settings": "Configuración",
|
||||
"testDevices": "Probar Dispositivos",
|
||||
"refresh": "Actualizar",
|
||||
"saveMessages": "Guardar Mensajes",
|
||||
"clearAll": "Borrar Todo"
|
||||
},
|
||||
"chat": {
|
||||
"addEmoji": "Agregar emoji",
|
||||
"typeMessage": "Escribe un mensaje...",
|
||||
"sendMessage": "Enviar mensaje"
|
||||
},
|
||||
"api": {
|
||||
"unauthorized": "Acceso no autorizado",
|
||||
"invalidApiKey": "Clave de API inválida",
|
||||
"serverError": "Error del servidor",
|
||||
"notFound": "Recurso no encontrado"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"appName": "Call-me",
|
||||
"appTitle": "Call-me - Appels Vidéo Instantanés",
|
||||
"appDescription": "Votre solution pour les appels vidéo instantanés!",
|
||||
"signIn": {
|
||||
"title": "Se Connecter",
|
||||
"username": "Entrez le nom d'utilisateur",
|
||||
"button": "Se Connecter",
|
||||
"enterUsername": "Veuillez entrer votre nom d'utilisateur"
|
||||
},
|
||||
"room": {
|
||||
"sessionTime": "Temps de Session",
|
||||
"localUsername": "Vous",
|
||||
"remoteUsername": "Utilisateur Distant",
|
||||
"waiting": "En attente de quelqu'un pour rejoindre...",
|
||||
"connecting": "Connexion...",
|
||||
"userJoined": "__username__ a rejoint l'appel",
|
||||
"userLeft": "__username__ a quitté l'appel",
|
||||
"users": "Utilisateurs",
|
||||
"chat": "Chat",
|
||||
"searchUsers": "Rechercher des utilisateurs..."
|
||||
},
|
||||
"controls": {
|
||||
"microphone": "Microphone",
|
||||
"camera": "Caméra",
|
||||
"screenShare": "Partager l'Écran",
|
||||
"endCall": "Terminer l'Appel",
|
||||
"settings": "Paramètres",
|
||||
"fullscreen": "Plein Écran"
|
||||
},
|
||||
"messages": {
|
||||
"microphoneEnabled": "Microphone activé",
|
||||
"microphoneDisabled": "Microphone désactivé",
|
||||
"cameraEnabled": "Caméra activée",
|
||||
"cameraDisabled": "Caméra désactivée",
|
||||
"screenShareStarted": "Partage d'écran démarré",
|
||||
"screenShareStopped": "Partage d'écran arrêté",
|
||||
"callEnded": "Appel terminé",
|
||||
"connectionFailed": "Échec de la connexion",
|
||||
"permissionDenied": "Permission refusée",
|
||||
"error": "Une erreur s'est produite",
|
||||
"copied": "Copié dans le presse-papiers",
|
||||
"invalidPassword": "Mot de passe invalide"
|
||||
},
|
||||
"errors": {
|
||||
"noUsername": "Le nom d'utilisateur est requis",
|
||||
"connectionLost": "Connexion perdue",
|
||||
"mediaDevices": "Impossible d'accéder aux périphériques multimédias",
|
||||
"noSupport": "Votre navigateur ne prend pas en charge cette fonctionnalité"
|
||||
},
|
||||
"settings": {
|
||||
"language": "Langue",
|
||||
"selectLanguage": "Sélectionner la Langue",
|
||||
"audioInput": "Microphone",
|
||||
"videoInput": "Caméra",
|
||||
"audioOutput": "Haut-parleur",
|
||||
"resolution": "Résolution Vidéo",
|
||||
"save": "Enregistrer",
|
||||
"cancel": "Annuler",
|
||||
"settings": "Paramètres",
|
||||
"testDevices": "Tester les Appareils",
|
||||
"refresh": "Actualiser",
|
||||
"saveMessages": "Enregistrer les Messages",
|
||||
"clearAll": "Tout Effacer"
|
||||
},
|
||||
"chat": {
|
||||
"addEmoji": "Ajouter un emoji",
|
||||
"typeMessage": "Tapez un message...",
|
||||
"sendMessage": "Envoyer le message"
|
||||
},
|
||||
"api": {
|
||||
"unauthorized": "Accès non autorisé",
|
||||
"invalidApiKey": "Clé API invalide",
|
||||
"serverError": "Erreur du serveur",
|
||||
"notFound": "Ressource non trouvée"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"appName": "Call-me",
|
||||
"appTitle": "Call-me - Videochiamate Istantanee",
|
||||
"appDescription": "La tua scelta per videochiamate istantanee!",
|
||||
"signIn": {
|
||||
"title": "Accedi",
|
||||
"username": "Inserisci nome utente",
|
||||
"button": "Accedi",
|
||||
"enterUsername": "Inserisci il tuo nome utente"
|
||||
},
|
||||
"room": {
|
||||
"sessionTime": "Tempo di Sessione",
|
||||
"localUsername": "Tu",
|
||||
"remoteUsername": "Utente Remoto",
|
||||
"waiting": "In attesa che qualcuno si unisca...",
|
||||
"connecting": "Connessione...",
|
||||
"userJoined": "__username__ è entrato nella chiamata",
|
||||
"userLeft": "__username__ ha lasciato la chiamata",
|
||||
"users": "Utenti",
|
||||
"chat": "Chat",
|
||||
"searchUsers": "Cerca utenti..."
|
||||
},
|
||||
"controls": {
|
||||
"microphone": "Microfono",
|
||||
"camera": "Fotocamera",
|
||||
"screenShare": "Condividi Schermo",
|
||||
"endCall": "Termina Chiamata",
|
||||
"settings": "Impostazioni",
|
||||
"fullscreen": "Schermo Intero"
|
||||
},
|
||||
"messages": {
|
||||
"microphoneEnabled": "Microfono attivato",
|
||||
"microphoneDisabled": "Microfono disattivato",
|
||||
"cameraEnabled": "Fotocamera attivata",
|
||||
"cameraDisabled": "Fotocamera disattivata",
|
||||
"screenShareStarted": "Condivisione schermo avviata",
|
||||
"screenShareStopped": "Condivisione schermo interrotta",
|
||||
"callEnded": "Chiamata terminata",
|
||||
"connectionFailed": "Connessione fallita",
|
||||
"permissionDenied": "Permesso negato",
|
||||
"error": "Si è verificato un errore",
|
||||
"copied": "Copiato negli appunti",
|
||||
"invalidPassword": "Password non valida"
|
||||
},
|
||||
"errors": {
|
||||
"noUsername": "Il nome utente è obbligatorio",
|
||||
"connectionLost": "Connessione persa",
|
||||
"mediaDevices": "Impossibile accedere ai dispositivi multimediali",
|
||||
"noSupport": "Il tuo browser non supporta questa funzionalità"
|
||||
},
|
||||
"settings": {
|
||||
"language": "Lingua",
|
||||
"selectLanguage": "Seleziona Lingua",
|
||||
"audioInput": "Microfono",
|
||||
"videoInput": "Fotocamera",
|
||||
"audioOutput": "Altoparlante",
|
||||
"resolution": "Risoluzione Video",
|
||||
"save": "Salva",
|
||||
"cancel": "Annulla",
|
||||
"settings": "Impostazioni",
|
||||
"testDevices": "Prova Dispositivi",
|
||||
"refresh": "Aggiorna",
|
||||
"saveMessages": "Salva Messaggi",
|
||||
"clearAll": "Cancella Tutto"
|
||||
},
|
||||
"chat": {
|
||||
"addEmoji": "Aggiungi emoji",
|
||||
"typeMessage": "Scrivi un messaggio...",
|
||||
"sendMessage": "Invia messaggio"
|
||||
},
|
||||
"api": {
|
||||
"unauthorized": "Accesso non autorizzato",
|
||||
"invalidApiKey": "Chiave API non valida",
|
||||
"serverError": "Errore del server",
|
||||
"notFound": "Risorsa non trovata"
|
||||
}
|
||||
}
|
||||
Generated
+139
-6
@@ -1,22 +1,23 @@
|
||||
{
|
||||
"name": "call-me",
|
||||
"version": "1.2.85",
|
||||
"version": "1.2.86",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "call-me",
|
||||
"version": "1.2.85",
|
||||
"version": "1.2.86",
|
||||
"license": "AGPLv3",
|
||||
"dependencies": {
|
||||
"@ngrok/ngrok": "1.7.0",
|
||||
"axios": "^1.13.4",
|
||||
"colors": "^1.4.0",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.2.3",
|
||||
"dotenv": "^17.2.4",
|
||||
"express": "^5.2.1",
|
||||
"helmet": "^8.1.0",
|
||||
"httpolyglot": "0.1.2",
|
||||
"i18n": "^0.15.3",
|
||||
"js-yaml": "4.1.1",
|
||||
"socket.io": "^4.8.3",
|
||||
"swagger-ui-express": "5.0.1"
|
||||
@@ -26,6 +27,50 @@
|
||||
"prettier": "3.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@messageformat/core": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.4.0.tgz",
|
||||
"integrity": "sha512-NgCFubFFIdMWJGN5WuQhHCNmzk7QgiVfrViFxcS99j7F5dDS5EP6raR54I+2ydhe4+5/XTn/YIEppFaqqVWHsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@messageformat/date-skeleton": "^1.0.0",
|
||||
"@messageformat/number-skeleton": "^1.0.0",
|
||||
"@messageformat/parser": "^5.1.0",
|
||||
"@messageformat/runtime": "^3.0.1",
|
||||
"make-plural": "^7.0.0",
|
||||
"safe-identifier": "^0.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@messageformat/date-skeleton": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz",
|
||||
"integrity": "sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@messageformat/number-skeleton": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz",
|
||||
"integrity": "sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@messageformat/parser": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.1.tgz",
|
||||
"integrity": "sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moo": "^0.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@messageformat/runtime": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@messageformat/runtime/-/runtime-3.0.2.tgz",
|
||||
"integrity": "sha512-dkIPDCjXcfhSHgNE1/qV6TeczQZR59Yx0xXeafVKgK3QVWoxc38ljwpksUpnzCGvN151KUbCJTDZVmahtf1YZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"make-plural": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok/-/ngrok-1.7.0.tgz",
|
||||
@@ -609,9 +654,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||
"version": "17.2.4",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz",
|
||||
"integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -857,6 +902,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-printf": {
|
||||
"version": "1.6.10",
|
||||
"resolved": "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.10.tgz",
|
||||
"integrity": "sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@@ -1131,6 +1185,49 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18n": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/i18n/-/i18n-0.15.3.tgz",
|
||||
"integrity": "sha512-tW/AA5R4lJZLnd60Agcd0PfXB1C2G7UqTrdNewuv/SIYdxcHkCE8w4Zx1SgCjJ+2BLuAAGIG/KXb/xNYF1lO5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@messageformat/core": "^3.4.0",
|
||||
"debug": "^4.4.3",
|
||||
"fast-printf": "^1.6.10",
|
||||
"make-plural": "^7.4.0",
|
||||
"math-interval-parser": "^2.0.1",
|
||||
"mustache": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mashpie"
|
||||
}
|
||||
},
|
||||
"node_modules/i18n/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18n/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
||||
@@ -1227,6 +1324,21 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/make-plural": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.5.0.tgz",
|
||||
"integrity": "sha512-0booA+aVYyVFoR67JBHdfVk0U08HmrBH2FrtmBqBa+NldlqXv/G2Z9VQuQq6Wgp2jDWdybEWGfBkk1cq5264WA==",
|
||||
"license": "Unicode-DFS-2016"
|
||||
},
|
||||
"node_modules/math-interval-parser": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/math-interval-parser/-/math-interval-parser-2.0.1.tgz",
|
||||
"integrity": "sha512-VmlAmb0UJwlvMyx8iPhXUDnVW1F9IrGEd9CIOmv+XL8AErCUUuozoDMrgImvnYt2A+53qVX/tPW6YJurMKYsvA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -1288,12 +1400,27 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moo": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
|
||||
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
}
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
@@ -1562,6 +1689,12 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-identifier": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz",
|
||||
"integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "call-me",
|
||||
"version": "1.2.85",
|
||||
"version": "1.2.86",
|
||||
"description": "Your Go-To for Instant Video Calls",
|
||||
"author": "Miroslav Pejic - miroslav.pejic.85@gmail.com",
|
||||
"license": "AGPLv3",
|
||||
@@ -23,10 +23,11 @@
|
||||
"axios": "^1.13.4",
|
||||
"colors": "^1.4.0",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.2.3",
|
||||
"dotenv": "^17.2.4",
|
||||
"express": "^5.2.1",
|
||||
"helmet": "^8.1.0",
|
||||
"httpolyglot": "0.1.2",
|
||||
"i18n": "^0.15.3",
|
||||
"js-yaml": "4.1.1",
|
||||
"socket.io": "^4.8.3",
|
||||
"swagger-ui-express": "5.0.1"
|
||||
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
// i18n.js - Client-side internationalization support
|
||||
'use strict';
|
||||
|
||||
// Global i18n object
|
||||
const i18n = {
|
||||
currentLocale: 'en',
|
||||
translations: {},
|
||||
defaultLocale: 'en',
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize i18n
|
||||
*/
|
||||
async function initI18n() {
|
||||
// Get saved locale from localStorage or use browser language or default
|
||||
const savedLocale = localStorage.getItem('locale');
|
||||
const browserLocale = navigator.language.split('-')[0]; // Get 'en' from 'en-US'
|
||||
const supportedLocales = ['en', 'es', 'fr', 'it', 'de'];
|
||||
|
||||
// Determine which locale to use
|
||||
if (savedLocale && supportedLocales.includes(savedLocale)) {
|
||||
i18n.currentLocale = savedLocale;
|
||||
} else if (supportedLocales.includes(browserLocale)) {
|
||||
i18n.currentLocale = browserLocale;
|
||||
} else {
|
||||
i18n.currentLocale = i18n.defaultLocale;
|
||||
}
|
||||
|
||||
// Load translations
|
||||
await loadTranslations(i18n.currentLocale);
|
||||
|
||||
// Set up language selector
|
||||
setupLanguageSelector();
|
||||
|
||||
// Translate the page
|
||||
translatePage();
|
||||
|
||||
console.log('i18n initialized with locale:', i18n.currentLocale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load translations from the server
|
||||
* @param {string} locale - The locale to load
|
||||
*/
|
||||
async function loadTranslations(locale) {
|
||||
try {
|
||||
const response = await fetch(`/translations/${locale}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load translations for locale: ${locale}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
i18n.translations = data.translations;
|
||||
i18n.currentLocale = locale;
|
||||
localStorage.setItem('locale', locale);
|
||||
} catch (error) {
|
||||
console.error('Error loading translations:', error);
|
||||
// Fallback to English if loading fails
|
||||
if (locale !== 'en') {
|
||||
await loadTranslations('en');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation for a key
|
||||
* @param {string} key - The translation key (supports dot notation, e.g., 'signIn.title')
|
||||
* @param {object} replacements - Object with placeholder replacements
|
||||
* @returns {string} - The translated string
|
||||
*/
|
||||
function t(key, replacements = {}) {
|
||||
const keys = key.split('.');
|
||||
let value = i18n.translations;
|
||||
|
||||
// Navigate through nested keys
|
||||
for (const k of keys) {
|
||||
if (value && typeof value === 'object' && k in value) {
|
||||
value = value[k];
|
||||
} else {
|
||||
console.warn(`Translation key not found: ${key}`);
|
||||
return key; // Return the key itself if translation not found
|
||||
}
|
||||
}
|
||||
|
||||
// If value is an object, return the key (shouldn't happen with proper keys)
|
||||
if (typeof value === 'object') {
|
||||
console.warn(`Translation key is an object, not a string: ${key}`);
|
||||
return key;
|
||||
}
|
||||
|
||||
// Replace placeholders in the translation
|
||||
let translation = value;
|
||||
for (const [placeholder, replacement] of Object.entries(replacements)) {
|
||||
translation = translation.replace(new RegExp(`__${placeholder}__`, 'g'), replacement);
|
||||
}
|
||||
|
||||
return translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the current language
|
||||
* @param {string} locale - The new locale
|
||||
*/
|
||||
async function changeLanguage(locale) {
|
||||
await loadTranslations(locale);
|
||||
translatePage();
|
||||
|
||||
// Update language selector
|
||||
const languageSelect = document.getElementById('languageSelect');
|
||||
if (languageSelect) {
|
||||
languageSelect.value = locale;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup language selector event listener
|
||||
*/
|
||||
function setupLanguageSelector() {
|
||||
const languageSelect = document.getElementById('languageSelect');
|
||||
if (languageSelect) {
|
||||
// Set current locale
|
||||
languageSelect.value = i18n.currentLocale;
|
||||
|
||||
// Add change event listener
|
||||
languageSelect.addEventListener('change', async (e) => {
|
||||
const newLocale = e.target.value;
|
||||
await changeLanguage(newLocale);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate all elements with data-i18n attribute
|
||||
*/
|
||||
function translatePage() {
|
||||
// Translate elements with data-i18n attribute
|
||||
document.querySelectorAll('[data-i18n]').forEach((element) => {
|
||||
const key = element.getAttribute('data-i18n');
|
||||
const translation = t(key);
|
||||
|
||||
// Update the appropriate property based on element type
|
||||
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
||||
if (element.type === 'button' || element.type === 'submit') {
|
||||
element.value = translation;
|
||||
} else {
|
||||
element.placeholder = translation;
|
||||
}
|
||||
} else {
|
||||
element.textContent = translation;
|
||||
}
|
||||
});
|
||||
|
||||
// Translate elements with data-i18n-placeholder attribute
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach((element) => {
|
||||
const key = element.getAttribute('data-i18n-placeholder');
|
||||
element.placeholder = t(key);
|
||||
});
|
||||
|
||||
// Translate elements with data-i18n-title attribute (for tooltips)
|
||||
document.querySelectorAll('[data-i18n-title]').forEach((element) => {
|
||||
const key = element.getAttribute('data-i18n-title');
|
||||
element.title = t(key);
|
||||
});
|
||||
|
||||
// Update document title
|
||||
document.title = t('appTitle');
|
||||
|
||||
// Custom translations for specific elements that need special handling
|
||||
updateCustomTranslations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update custom translations for specific elements
|
||||
*/
|
||||
function updateCustomTranslations() {
|
||||
// Update app title
|
||||
const appTitle = document.getElementById('appTitle');
|
||||
if (appTitle) appTitle.textContent = t('appTitle');
|
||||
|
||||
// Update app name
|
||||
const appName = document.getElementById('appName');
|
||||
if (appName) appName.textContent = t('appName');
|
||||
|
||||
// Update settings title for language
|
||||
const settingsTitles = document.querySelectorAll('.settings-title');
|
||||
if (settingsTitles.length > 0) {
|
||||
settingsTitles[0].innerHTML = '<i class="fas fa-globe"></i> ' + t('settings.language');
|
||||
}
|
||||
if (settingsTitles.length > 1) {
|
||||
settingsTitles[1].innerHTML = '<i class="fas fa-video"></i> Media Devices';
|
||||
}
|
||||
if (settingsTitles.length > 2) {
|
||||
settingsTitles[2].innerHTML = '<i class="fas fa-comments"></i> Chat Settings';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to show translated messages using SweetAlert
|
||||
* @param {string} titleKey - Translation key for the title
|
||||
* @param {string} textKey - Translation key for the text
|
||||
* @param {string} icon - Icon type (success, error, warning, info)
|
||||
*/
|
||||
function showTranslatedAlert(titleKey, textKey, icon = 'info') {
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
title: t(titleKey),
|
||||
text: t(textKey),
|
||||
icon: icon,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize i18n when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initI18n);
|
||||
} else {
|
||||
initI18n();
|
||||
}
|
||||
+56
-14
@@ -66,15 +66,23 @@
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h1 id="appName">Call-me</h1></div>
|
||||
<div class="card-header"><h1 id="appName" data-i18n="appName">Call-me</h1></div>
|
||||
<div class="card-body">
|
||||
<!-- Sign-in Form -->
|
||||
<div class="mb-3">
|
||||
<!-- Input field for entering the username -->
|
||||
<input id="usernameIn" type="text" placeholder="Enter username" required />
|
||||
<input
|
||||
id="usernameIn"
|
||||
type="text"
|
||||
placeholder="Enter username"
|
||||
data-i18n-placeholder="signIn.username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<!-- Sign-in button -->
|
||||
<button id="signInBtn" class="btn btn-primary">Sign In</button>
|
||||
<button id="signInBtn" class="btn btn-primary" data-i18n="signIn.button">
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -194,14 +202,14 @@
|
||||
<div class="user-sidebar-header">
|
||||
<div class="sidebar-tabs">
|
||||
<button id="usersTab" class="sidebar-tab active" data-tab="users">
|
||||
<i class="fas fa-users"></i> Users
|
||||
<i class="fas fa-users"></i> <span data-i18n="room.users">Users</span>
|
||||
</button>
|
||||
<button id="chatTab" class="sidebar-tab" data-tab="chat">
|
||||
<i class="fas fa-comments"></i> Chat
|
||||
<i class="fas fa-comments"></i> <span data-i18n="room.chat">Chat</span>
|
||||
<span id="chatNotification" class="chat-notification hidden">0</span>
|
||||
</button>
|
||||
<button id="settingsTab" class="sidebar-tab" data-tab="settings">
|
||||
<i class="fas fa-cog"></i> Settings
|
||||
<i class="fas fa-cog"></i> <span data-i18n="settings.settings">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -213,7 +221,13 @@
|
||||
<!-- Users Tab Content -->
|
||||
<div id="usersContent" class="tab-content active">
|
||||
<div class="user-search-bar">
|
||||
<input type="text" id="userSearchInput" placeholder="Search users..." autocomplete="off" />
|
||||
<input
|
||||
type="text"
|
||||
id="userSearchInput"
|
||||
placeholder="Search users..."
|
||||
data-i18n-placeholder="room.searchUsers"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<ul id="userList" class="user-list"></ul>
|
||||
</div>
|
||||
@@ -222,11 +236,23 @@
|
||||
<div id="chatContent" class="tab-content mt-5">
|
||||
<div id="chatMessages" class="chat-messages"></div>
|
||||
<form id="chatForm" class="chat-form" autocomplete="off">
|
||||
<button type="button" id="emojiBtn" class="btn btn-emoji" title="Add emoji">
|
||||
<button
|
||||
type="button"
|
||||
id="emojiBtn"
|
||||
class="btn btn-emoji"
|
||||
title="Add emoji"
|
||||
data-i18n-title="chat.addEmoji"
|
||||
>
|
||||
<i class="fas fa-smile"></i>
|
||||
</button>
|
||||
<input id="chatInput" type="text" placeholder="Type a message..." autocomplete="off" />
|
||||
<button type="submit" class="btn btn-primary btn-send-chat">
|
||||
<input
|
||||
id="chatInput"
|
||||
type="text"
|
||||
placeholder="Type a message..."
|
||||
data-i18n-placeholder="chat.typeMessage"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button type="submit" class="btn btn-primary btn-send-chat" data-i18n-title="chat.sendMessage">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</form>
|
||||
@@ -236,6 +262,21 @@
|
||||
<!-- Settings Tab Content -->
|
||||
<div id="settingsContent" class="tab-content">
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-title"><i class="fas fa-globe"></i> Language</h3>
|
||||
|
||||
<div class="setting-group">
|
||||
<label for="languageSelect" class="setting-label">
|
||||
<i class="fas fa-language"></i> Select Language
|
||||
</label>
|
||||
<select id="languageSelect" class="setting-select">
|
||||
<option value="en">🇬🇧 English</option>
|
||||
<option value="es">🇪🇸 Español</option>
|
||||
<option value="fr">🇫🇷 Français</option>
|
||||
<option value="it">🇮🇹 Italiano</option>
|
||||
<option value="de">🇩🇪 Deutsch</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<h3 class="settings-title"><i class="fas fa-video"></i> Media Devices</h3>
|
||||
|
||||
<div class="setting-group">
|
||||
@@ -265,20 +306,20 @@
|
||||
|
||||
<div class="settings-actions">
|
||||
<button id="testDevicesBtn" class="btn btn-success btn-test-devices">
|
||||
<i class="fas fa-play"></i> Test Devices
|
||||
<i class="fas fa-play"></i> <span data-i18n="settings.testDevices">Test Devices</span>
|
||||
</button>
|
||||
<button id="refreshDevicesBtn" class="btn btn-secondary btn-refresh-devices">
|
||||
<i class="fas fa-sync"></i> Refresh
|
||||
<i class="fas fa-sync"></i> <span data-i18n="settings.refresh">Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 class="settings-title"><i class="fas fa-comments"></i> Chat Settings</h3>
|
||||
<div class="settings-actions">
|
||||
<button id="saveChatBtn" class="btn btn-success btn-save-chat">
|
||||
<i class="fas fa-download"></i> Save Messages
|
||||
<i class="fas fa-download"></i> <span data-i18n="settings.saveMessages">Save Messages</span>
|
||||
</button>
|
||||
<button id="clearChatBtn" class="btn btn-danger btn-clear-chat">
|
||||
<i class="fas fa-trash"></i> Clear All
|
||||
<i class="fas fa-trash"></i> <span data-i18n="settings.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -288,6 +329,7 @@
|
||||
<!-- JavaScript libraries for WebSocket and custom client code -->
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="i18n.js"></script>
|
||||
<script src="client.js"></script>
|
||||
|
||||
<!-- Include UaParser -->
|
||||
|
||||
+32
-5
@@ -670,7 +670,7 @@ input {
|
||||
display: flex;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 350px;
|
||||
width: 430px;
|
||||
height: 100%;
|
||||
background: rgba(30, 32, 36, 0.98);
|
||||
flex-direction: column;
|
||||
@@ -700,11 +700,22 @@ input {
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(24, 25, 28, 0.95);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
gap: 0.5rem;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sidebar-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
.sidebar-tabs::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
}
|
||||
|
||||
.sidebar-tab {
|
||||
@@ -720,6 +731,14 @@ input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sidebar-tab span {
|
||||
white-space: nowrap;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sidebar-tab:hover {
|
||||
@@ -772,6 +791,8 @@ input {
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.btn-exit-sidebar:hover {
|
||||
@@ -1175,15 +1196,21 @@ input {
|
||||
|
||||
.sidebar-tabs {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.sidebar-tab {
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
text-align: center;
|
||||
padding: 8px 4px;
|
||||
font-size: 0.75rem;
|
||||
padding: 8px 8px;
|
||||
font-size: 0.8rem;
|
||||
gap: 4px;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.sidebar-tab span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.sidebar-tab i {
|
||||
|
||||
Reference in New Issue
Block a user