feat(i18n): add French language support and enhance localization
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s

- Introduced French as a supported language across the application, updating locale files and adding translations for various components.
- Enhanced language handling logic to accommodate French, ensuring proper detection and fallback mechanisms.
- Updated UI elements to include French language options, improving accessibility for French-speaking users.
- Refactored SEO handling to include French in hreflang links, enhancing search engine indexing for multilingual content.
- Added new scripts for managing French translations and ensuring consistency across language files.
This commit is contained in:
Torsten Schulz (local)
2026-04-07 18:04:03 +02:00
parent f715c6125d
commit f7030bbabe
56 changed files with 5220 additions and 175 deletions

View File

@@ -0,0 +1,510 @@
{
"admin": {
"interests": {
"title": "[Admin] - Gérer les intérêts",
"newinterests": {
"name": "Nom d'intérêt",
"status": "Libéré",
"adultonly": "Réservé aux adultes",
"translations": "Traductions",
"isactive": "Activé",
"isadult": "Réservé aux adultes",
"delete": "Supprimer"
}
},
"contacts": {
"title": "[Admin] - Demandes de contact",
"date": "Date",
"from": "Expéditeur",
"actions": "Actes",
"open": "Modifier",
"finished": "Complet"
},
"editcontactrequest": {
"title": "[Admin] - Modifier la demande de contact"
},
"user": {
"name": "nom d'utilisateur",
"active": "Aktiv",
"blocked": "Gesperrt",
"actions": "Actes",
"search": "Suchen"
},
"vocabLessonReset": {
"title": "Sprachkurs: Lektionsfortschritt",
"intro": "Fortschritt, Übungsergebnisse und gespeicherter Lektionszustand für eine einzelne Lektion löschen (nicht der ganze Kurs). Es werden nur Sprachkurse gelistet, in die dieser Benutzer eingeschrieben ist.",
"loadCourses": "Eingeschriebene Kurse laden",
"selectCourse": "Kurs",
"selectLesson": "Lektion",
"reset": "Lektion für diesen Nutzer zurücksetzen",
"confirmTitle": "Lektionsfortschritt löschen",
"confirm": "Fortschritt der Lektion „{lesson}“ für {username} wirklich löschen?",
"success": "Lektionsfortschritt wurde zurückgesetzt.",
"error": "Zurücksetzen fehlgeschlagen.",
"pickUserFirst": "Zuerst einen Benutzer auswählen.",
"noEnrolledCourses": "Dieser Benutzer ist in keinem Sprachkurs eingeschrieben.",
"loadCoursesError": "Die Kursliste konnte nicht geladen werden.",
"loadingLessons": "Lektionen werden geladen …"
},
"vocabLessonMarkComplete": {
"divider": "Fortschritt reparieren (ohne Übungsergebnisse zu fälschen)",
"throughLabel": "Alle Lektionen bis Lektionsnummer (einschließlich)",
"hint": "Setzt fehlende oder offene Einträge auf „abgeschlossen“, inkl. Ziel-Score und erster Review-Welle. Bereits abgeschlossene Lektionen bleiben unverändert.",
"submit": "Bis hier als abgeschlossen markieren",
"confirmTitle": "Lektionen als abgeschlossen markieren",
"confirm": "Alle Lektionen mit Nummer ≤ {n} für {username} in diesem Kurs als abgeschlossen markieren?",
"success": "{marked} Lektion(en) neu als abgeschlossen gesetzt ({unchanged} waren bereits erledigt).",
"successNone": "Keine Änderung: alle betroffenen Lektionen ({unchanged}) waren bereits abgeschlossen.",
"error": "Markieren fehlgeschlagen."
},
"adultVerification": {
"title": "[Admin] - Erotik-Freigaben",
"intro": "Volljährige Nutzer können den Erotikbereich beantragen. Hier werden Anfragen geprüft und freigegeben oder abgelehnt.",
"username": "Benutzer",
"age": "Alter",
"statusLabel": "Status",
"requestLabel": "Nachweis",
"actions": "Actes",
"approve": "Freigeben",
"reject": "Ablehnen",
"resetPending": "Auf Prüfung setzen",
"openDocument": "Dokument ansehen",
"previewTitle": "Nachweis-Vorschau",
"closePreview": "Vorschau schließen",
"previewUnavailable": "Für diesen Dateityp ist hier keine Vorschau verfügbar.",
"documentMissing": "Le dossier de preuve n'a pas été trouvé sur le serveur.",
"empty": "Aucune demande correspondante trouvée.",
"loadError": "Les partages n'ont pas pu être chargés.",
"updateError": "Le statut n'a pas pu être modifié.",
"documentError": "Le document n'a pas pu être ouvert.",
"filters": {
"pending": "Ouvrir",
"approved": "Libéré",
"rejected": "Rejeté",
"all": "Tous"
},
"status": {
"none": "Non demandé",
"pending": "En cours d'examen",
"approved": "Libéré",
"rejected": "Rejeté"
},
"messages": {
"approved": "Libération accordée.",
"rejected": "Libération refusée.",
"pending": "Anfrage wieder auf Prüfung gesetzt."
}
},
"eroticModeration": {
"title": "[Admin] - Erotik-Moderation",
"intro": "Gemeldete Erotikbilder und -videos können hier geprüft, verborgen, gelöscht oder gegen den Account eskaliert werden.",
"empty": "Keine passenden Meldungen gefunden.",
"loadError": "Die Meldungen konnten nicht geladen werden.",
"actionError": "Die Moderationsaktion konnte nicht ausgeführt werden.",
"actionSuccess": "Die Moderationsaktion wurde gespeichert.",
"target": "Ziel",
"owner": "Besitzer",
"reporter": "Meldender",
"reason": "Grund",
"statusLabel": "Status",
"meta": "Zeit / Maßnahme",
"actions": "Actes",
"image": "Bild",
"video": "Video",
"hidden": "Verborgen",
"preview": "Vorschau",
"previewError": "Die Vorschau konnte nicht geladen werden.",
"dismiss": "Zurückweisen",
"hide": "Verbergen",
"restore": "Wieder freigeben",
"delete": "Supprimer",
"blockUploads": "Uploads sperren",
"revokeAccess": "Erotikzugang entziehen",
"notePrompt": "Notiz zur Moderationsentscheidung",
"actionLabels": {
"dismiss": "Zurückgewiesen",
"hide_content": "Verborgen",
"restore_content": "Libéré",
"delete_content": "Gelöscht",
"block_uploads": "Uploads gesperrt",
"revoke_access": "Zugang entzogen"
},
"filters": {
"open": "Ouvrir",
"actioned": "Bearbeitet",
"dismissed": "Zurückgewiesen",
"all": "Tous"
},
"status": {
"open": "Ouvrir",
"actioned": "Bearbeitet",
"dismissed": "Zurückgewiesen"
}
},
"rights": {
"add": "Recht hinzufügen",
"select": "Bitte wählen",
"current": "Aktuelle Rechte"
},
"forum": {
"title": "[Admin] - Forum",
"currentForums": "Existierende Foren",
"edit": "Ändern",
"delete": "Supprimer",
"createForum": "Anlegen",
"forumName": "Titel",
"create": "Anlegen",
"permissions": {
"label": "Berechtigungen",
"all": "Tout le monde",
"admin": "Administrateurs uniquement",
"teammember": "Membres de l'équipe uniquement",
"user": "Seulement certains utilisateurs",
"age": "Seulement à partir de 14 ans"
},
"selectPermissions": "Veuillez sélectionner",
"confirmDeleteMessage": "Faut-il vraiment supprimer le forum ?",
"confirmDeleteTitle": "Supprimer le forum"
},
"falukant": {
"edituser": {
"title": "Modifier l'utilisateur Falukant",
"username": "nom d'utilisateur",
"characterName": "Nom du personnage",
"user": "Benutzer",
"success": "Les modifications ont été enregistrées.",
"error": "Les modifications n'ont pas pu être enregistrées.",
"errorLoadingBranches": "Fehler beim Laden der Niederlassungen.",
"errorUpdatingStock": "Fehler beim Aktualisieren des Lagers.",
"stockUpdated": "Lager erfolgreich aktualisiert.",
"search": "Suchen",
"tabs": {
"userdata": "Benutzerdaten",
"branches": "Niederlassungen"
},
"branches": {
"title": "Niederlassungen & Lager",
"noStocks": "Kein Lager vorhanden",
"noBranches": "Keine Niederlassungen gefunden",
"addStock": "Lager hinzufügen",
"stockType": "Lagertyp",
"selectStockType": "Lagertyp auswählen",
"quantity": "Menge",
"allStocksAdded": "Alle verfügbaren Lagertypen sind bereits vorhanden"
},
"errorLoadingStockTypes": "Fehler beim Laden der Lagertypen.",
"errorAddingStock": "Fehler beim Hinzufügen des Lagers.",
"stockAdded": "Lager erfolgreich hinzugefügt.",
"invalidStockData": "Veuillez saisir des informations valides sur le type de stockage et la quantité.",
"pregnancy": {
"title": "Grossesse (Administrateur)",
"characterId": "ID du personnage",
"status": "Status",
"statusActive": "Enceinte jusqu'à",
"statusNone": "Pas enceinte",
"fatherId": "ID du personnage du père (facultatif)",
"fatherSelect": "Vater (Ehepartner / Verlobter / Liebhaber)",
"fatherNone": "— aucun père n'a été sauvé —",
"fatherHintList": "Liste des relations de ce caractère (mariage, fiançailles, histoire d'amour active).",
"fatherHintManual": "Kein passender Partner in der Datenbank: Vater-Charakter-ID manuell eintragen.",
"fatherManualPlaceholder": "ID du personnage",
"dueDays": "Tage bis zum Termin",
"dueDaysHint": "0 = Termin heute (Geburt kann je nach Spiel-Logik zeitnah anstehen).",
"force": "Schwangerschaft setzen",
"clear": "Schwangerschaft entfernen",
"successForce": "Schwangerschaft wurde gesetzt.",
"successClear": "Schwangerschaft wurde entfernt.",
"error": "Aktion fehlgeschlagen.",
"relationship": {
"married": "Ehepartner",
"engaged": "Verlobter",
"lover": "Liebhaber"
}
},
"birth": {
"title": "Geburt erzwingen (Admin)",
"motherHint": "Es wird der oben genannte Charakter (Mutter) verwendet.",
"fatherId": "Vater-Charakter-ID",
"fatherSelect": "Vater (Ehepartner / Verlobter / Liebhaber)",
"fatherChoose": "— Vater wählen —",
"fatherHintList": "Liste aus Beziehungen dieses Charakters.",
"fatherHintManual": "Kein Partner in der Liste: Vater-Charakter-ID manuell eintragen.",
"fatherRequired": "Bitte einen Vater auswählen oder die Charakter-ID angeben.",
"context": "Kontext",
"contextMarriage": "Ehe",
"contextLover": "Liebschaft",
"legitimacy": "Legitimität",
"legitimate": "Legitim",
"ackBastard": "Anerkannt unehelich",
"hiddenBastard": "Verborgen unehelich",
"gender": "Kind-Geschlecht",
"genderRandom": "Zufällig",
"male": "Männlich",
"female": "Weiblich",
"force": "Geburt auslösen",
"success": "Kind wurde angelegt (Taufe ausstehend).",
"error": "Geburt konnte nicht ausgelöst werden."
}
},
"map": {
"title": "Falukant Karten-Editor (Regionen)",
"description": "Zeichne Rechtecke auf der Falukant-Karte und weise sie Städten zu.",
"tabs": {
"regions": "Positionen",
"distances": "Entfernungen"
},
"regionList": "Städte",
"noCoords": "Keine Koordinaten gesetzt",
"currentRect": "Aktuelles Rechteck",
"hintDraw": "Wähle eine Stadt und ziehe mit der Maus ein Rechteck auf der Karte, um die Position festzulegen.",
"saveAll": "Enregistrer toutes les villes modifiées",
"connectionsTitle": "Connexions (region_distance)",
"source": "Depuis",
"target": "Après",
"selectSource": "Sélectionnez la ville source",
"selectTarget": "Sélectionnez la ville de destination",
"mode": "Type de transport",
"modeLand": "pays",
"modeWater": "Eau",
"modeAir": "Air",
"distance": "distance",
"saveConnection": "Enregistrer la connexion",
"pickOnMap": "Auf Karte wählen",
"errorSaveConnection": "Die Verbindung konnte nicht gespeichert werden.",
"errorDeleteConnection": "Die Verbindung konnte nicht gelöscht werden.",
"confirmDeleteConnection": "Verbindung wirklich löschen?"
},
"createNPC": {
"title": "NPCs erstellen",
"region": "Stadt",
"allRegions": "Alle Städte",
"ageRange": "Altersbereich",
"to": "bis",
"years": "Jahre",
"titleRange": "Titel-Bereich",
"count": "Anzahl pro Stadt-Titel-Kombination",
"countHelp": "Diese Anzahl wird für jede Kombination aus gewählter Stadt und Titel erstellt.",
"create": "NPCs erstellen",
"creating": "Erstelle...",
"result": "Ergebnis",
"createdCount": "{count} NPCs wurden erstellt.",
"combinationInfo": "{perCombination} PNJ par combinaison × {combinations} combinaisons = {count} PNJ au total",
"age": "Alter",
"errorLoadingRegions": "Erreur lors du chargement des villes.",
"errorLoadingTitles": "Erreur de chargement des titres.",
"errorCreating": "Erreur lors de la création des PNJ.",
"invalidAgeRange": "Tranche d'âge non valide.",
"invalidTitleRange": "Plage de titres non valide.",
"invalidCount": "Numéro invalide (1-500).",
"progress": "Progrès",
"progressDetails": "{actuel} créé par {total} PNJ",
"timeRemainingSeconds": "Temps restant : {secondes} secondes",
"timeRemainingMinutes": "Temps restant : {minutes} minutes {secondes} secondes",
"almostDone": "Presque fini...",
"jobNotFound": "Emploi introuvable ou expiré."
}
},
"chatrooms": {
"title": "[Admin] - Gérer les salons de discussion",
"roomName": "Raumname",
"create": "Chatraum anlegen",
"edit": "Chatraum bearbeiten",
"type": "Typ",
"isPublic": "Öffentlich sichtbar",
"isAdultOnly": "Nur Erotikbereich",
"actions": "Actes",
"genderRestriction": {
"show": "Geschlechtsbeschränkung aktivieren",
"label": "Geschlechtsbeschränkung"
},
"minAge": {
"show": "Mindestalter angeben",
"label": "Mindestalter"
},
"maxAge": {
"show": "Höchstalter angeben",
"label": "Höchstalter"
},
"password": {
"show": "Passwortschutz aktivieren",
"label": "Passwort"
},
"friendsOfOwnerOnly": "Nur Freunde des Besitzers",
"requiredUserRight": {
"show": "Benötigtes Benutzerrecht angeben",
"label": "Benötigtes Benutzerrecht"
},
"roomtype": {
"chat": "Reden",
"dice": "Würfeln",
"poker": "Poker",
"hangman": "Hangman"
},
"rights": {
"talk": "Reden",
"scream": "Schreien",
"whisper": "Flüstern",
"start game": "Spiel starten",
"open room": "Raum öffnen",
"systemmessage": "Systemnachricht"
},
"confirmDelete": "Soll dieser Chatraum wirklich gelöscht werden?"
},
"match3": {
"title": "Match3 Level verwalten",
"newLevel": "Neues Level erstellen",
"editLevel": "Level bearbeiten",
"deleteLevel": "Level löschen",
"confirmDelete": "Möchtest du dieses Level wirklich löschen?",
"levelName": "Name",
"levelDescription": "Beschreibung",
"boardWidth": "Breite",
"boardHeight": "Höhe",
"moveLimit": "Zug-Limit",
"levelOrder": "Reihenfolge",
"boardLayout": "Board-Layout",
"tileTypes": "Types de tuiles disponibles",
"actions": "Actes",
"edit": "Modifier",
"delete": "Supprimer",
"save": "Sauvegarder",
"cancel": "Annuler",
"update": "Mise à jour",
"create": "Créer",
"boardControls": {
"fillAll": "Activer tout",
"clearAll": "Désactiver tout",
"invert": "Inverser"
},
"loading": "Niveau de chargement...",
"retry": "Essayer à nouveau",
"availableLevels": "Niveaux disponibles : {count}",
"levelFormat": "Niveau {number} : {name}",
"levelObjectives": "Level-Objekte",
"objectivesTitle": "Siegvoraussetzungen",
"addObjective": "Objektiv hinzufügen",
"removeObjective": "Entfernen",
"objectiveType": "Typ",
"objectiveTypeScore": "Punkte sammeln",
"objectiveTypeMatches": "Matches machen",
"objectiveTypeMoves": "Züge verwenden",
"objectiveTypeTime": "Zeit einhalten",
"objectiveTypeSpecial": "Spezialziel",
"objectiveOperator": "Operator",
"operatorGreaterEqual": "Größer oder gleich (≥)",
"operatorLessEqual": "Kleiner oder gleich (≤)",
"operatorEqual": "Gleich (=)",
"operatorGreater": "Größer als (>)",
"operatorLess": "Kleiner als (<)",
"objectiveTarget": "Zielwert",
"objectiveTargetPlaceholder": "z.B. 100",
"objectiveOrder": "Reihenfolge",
"objectiveOrderPlaceholder": "1, 2, 3...",
"objectiveDescription": "Beschreibung",
"objectiveDescriptionPlaceholder": "par ex. Collectez 100 points",
"objectiveRequired": "Requis pour terminer le niveau",
"noObjectives": "Aucune condition de victoire définie. Cliquez sur « Ajouter un objectif » pour en créer un."
},
"userStatistics": {
"title": "[Admin] - Statistiques utilisateur",
"totalUsers": "Nombre total d'utilisateurs",
"genderDistribution": "Répartition par sexe",
"ageDistribution": "Répartition par âge"
},
"taxiTools": {
"title": "Outils de taxi",
"description": "Gérer les plans, niveaux et configurations des taxis",
"mapEditor": {
"title": "Modifier la carte",
"availableMaps": "Cartes disponibles : {count}",
"newMap": "Neue Map erstellen",
"mapFormat": "{name} (Position: {x},{y})",
"mapName": "Map-Name",
"mapDescription": "Beschreibung",
"mapWidth": "Breite",
"mapHeight": "Höhe",
"tileSize": "Tile-Größe",
"positionX": "X-Position",
"positionY": "Y-Position",
"mapType": "Map-Typ",
"mapLayout": "Map-Layout",
"tilePalette": "Tile-Palette",
"streetNames": "Straßennamen",
"extraElements": "Zusätzliche Elemente",
"streetNameHorizontal": "Straßenname (horizontal)",
"streetNameVertical": "Straßenname (vertikal)",
"continueHorizontal": "In anderer Richtung fortführen (→)",
"continueVertical": "In anderer Richtung fortführen (↓)",
"continueOther": "In anderer Richtung fortführen",
"position": "position",
"fillAllRoads": "Toutes les rues",
"clearAll": "Supprimer tout",
"generateRandom": "Générer aléatoirement",
"delete": "Supprimer",
"update": "Mise à jour",
"cancel": "Annuler",
"create": "Créer",
"createSuccess": "La carte a été créée avec succès !",
"updateSuccess": "La carte a été mise à jour avec succès !",
"deleteSuccess": "La carte a été supprimée avec succès !"
}
},
"servicesStatus": {
"title": "Statut des services",
"description": "Surveiller l'état du backend, du chat et du démon",
"status": {
"connected": "Attachés ensemble",
"connecting": "Connecter...",
"disconnected": "Non connecté",
"error": "Erreur",
"unknown": "Inconnu"
},
"backend": {
"title": "Backend",
"connected": "Backend-Service ist erreichbar und verbunden"
},
"chat": {
"title": "Chat",
"connected": "Chat-Service ist erreichbar und verbunden"
},
"daemon": {
"title": "Daemon",
"connected": "Daemon-Service ist erreichbar und verbunden",
"connections": {
"title": "Aktive Verbindungen",
"none": "Keine aktiven Verbindungen",
"userId": "Benutzer-ID",
"username": "nom d'utilisateur",
"connections": "Verbindungen",
"duration": "Verbindungsdauer",
"lastPong": "Zeit seit letztem Pong",
"pingTimeouts": "Ping-Timeouts",
"pongReceived": "Pong empfangen",
"yes": "Ja",
"no": "Nein",
"notConnected": "Démon non connecté",
"sendError": "Erreur lors de l'envoi de la demande",
"error": "Erreur lors de l'obtention des connexions"
},
"websocketLog": {
"title": "Journal WebSocket",
"showLog": "Afficher le journal WebSocket",
"refresh": "Mise à jour",
"loading": "Chargement...",
"close": "Fermer",
"entryCount": "{count} entrées",
"noEntries": "Keine Log-Einträge vorhanden",
"notConnected": "Démon non connecté",
"sendError": "Erreur lors de l'envoi de la demande",
"parseError": "Fehler beim Verarbeiten der Antwort",
"timestamp": "Zeitstempel",
"direction": "Richtung",
"peer": "Peer",
"connUser": "Verbindungs-User",
"targetUser": "Ziel-User",
"event": "Event"
}
}
}
}
}