From 9c121d2dc28d3995b1872a8227aacd112f7db3e1 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 17 Apr 2026 16:20:20 +0200 Subject: [PATCH] feat(i18n): update Cebuano localization for minigames and enhance UI text - Added new localized strings for minigames in Cebuano, including loading hints, objectives, and play focus descriptions to improve user experience. - Updated existing translations for clarity and consistency across various game elements. - Enhanced the Match3Game component to utilize localized strings, ensuring dynamic text rendering based on user language preferences. - Included a new entry in .gitignore for the locale audit report to maintain a clean repository. --- .gitignore | 1 + frontend/scripts/audit-ceb-locale.mjs | 140 +++++++++++++++++++ frontend/src/i18n/locales/ceb/falukant.json | 104 ++++++++++---- frontend/src/i18n/locales/ceb/minigames.json | 15 +- frontend/src/i18n/locales/de/minigames.json | 15 +- frontend/src/i18n/locales/en/minigames.json | 15 +- frontend/src/i18n/locales/es/minigames.json | 15 +- frontend/src/i18n/locales/fr/minigames.json | 15 +- frontend/src/views/minigames/Match3Game.vue | 43 +++--- 9 files changed, 316 insertions(+), 47 deletions(-) create mode 100644 frontend/scripts/audit-ceb-locale.mjs diff --git a/.gitignore b/.gitignore index 05ed680..9008349 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ frontend/dist frontend/dist/* frontend/scripts/.i18n-de-fr-cache.json frontend/scripts/.falukant-fr-smooth-cache.json +frontend/ceb-locale-audit-report.json frontedtree.txt backend/dist/ backend/data/model-cache diff --git a/frontend/scripts/audit-ceb-locale.mjs b/frontend/scripts/audit-ceb-locale.mjs new file mode 100644 index 0000000..c7fb4eb --- /dev/null +++ b/frontend/scripts/audit-ceb-locale.mjs @@ -0,0 +1,140 @@ +#!/usr/bin/env node +/** + * Vergleicht en- mit ceb-Locale-Chunks (wie in src/i18n/index.js). + * - identical: Blattstrings, die nach Merge noch gleich en sind (typisch: fehlende Bisaya-Übersetzung). + * - germanHints: Blattstrings in ceb mit deutschen Anzeichen (Umlaute / typische Wörter). + * + * Usage: node frontend/scripts/audit-ceb-locale.mjs + */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const localesRoot = path.join(__dirname, '../src/i18n/locales'); + +const EN_CHUNKS = [ + 'general.json', + 'header.json', + 'navigation.json', + 'home.json', + 'chat.json', + 'register.json', + 'passwordReset.json', + 'error.json', + 'activate.json', + 'settings.json', + 'admin.json', + 'socialnetwork.json', + 'friends.json', + 'falukant.json', + 'blog.json', + 'minigames.json', + 'message.json', + 'personal.json', + 'seo.json', +]; + +function isPlainObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function deepMerge(target, source) { + if (!isPlainObject(source)) return target; + const base = isPlainObject(target) ? { ...target } : {}; + for (const key of Object.keys(source)) { + const sv = source[key]; + const tv = base[key]; + if (isPlainObject(sv) && isPlainObject(tv)) { + base[key] = deepMerge(tv, sv); + } else if (isPlainObject(sv)) { + base[key] = deepMerge({}, sv); + } else { + base[key] = sv; + } + } + return base; +} + +function loadJson(rel) { + const p = path.join(localesRoot, rel); + const raw = fs.readFileSync(p, 'utf8'); + return JSON.parse(raw); +} + +function flatten(obj, prefix = '', out = {}) { + if (!isPlainObject(obj)) return out; + for (const k of Object.keys(obj)) { + const v = obj[k]; + const key = prefix ? `${prefix}.${k}` : k; + if (isPlainObject(v)) { + flatten(v, key, out); + } else if (typeof v === 'string') { + out[key] = v; + } + } + return out; +} + +const germanWordHints = + /\b(der|die|das|und|nicht|wird|werden|schon|keine|kein|Ziel|Ziele|Zug|Züge|Spiel|Level|Bitte|Hier|wurde|können|müssen)\b/i; +const germanChars = /[äöüÄÖÜß]/; + +function audit() { + let enMerged = {}; + let cebMerged = {}; + + for (const f of EN_CHUNKS) { + const enPath = path.join('en', f); + const cebPath = path.join('ceb', f); + if (!fs.existsSync(path.join(localesRoot, cebPath))) { + console.warn(`Warnung: fehlt ${cebPath} (übersprungen)`); + continue; + } + enMerged = deepMerge(enMerged, loadJson(enPath)); + cebMerged = deepMerge(cebMerged, loadJson(enPath)); + cebMerged = deepMerge(cebMerged, loadJson(cebPath)); + } + + const enFlat = flatten(enMerged); + const cebFlat = flatten(cebMerged); + + const identical = []; + const germanHints = []; + + for (const key of Object.keys(cebFlat)) { + const ev = enFlat[key]; + const cv = cebFlat[key]; + if (typeof cv !== 'string') continue; + if (ev === cv && typeof ev === 'string') { + identical.push(key); + } + if (germanChars.test(cv) || germanWordHints.test(cv)) { + germanHints.push({ key, sample: cv.slice(0, 120) }); + } + } + + identical.sort(); + germanHints.sort((a, b) => a.key.localeCompare(b.key)); + + const report = { + generated: new Date().toISOString(), + summary: { + totalLeafStringsCeb: Object.keys(cebFlat).length, + identicalToEnCount: identical.length, + germanHintCount: germanHints.length, + }, + identicalToEn: identical, + germanHintsInCeb: germanHints, + }; + + const outFile = path.join(__dirname, '../ceb-locale-audit-report.json'); + fs.writeFileSync(outFile, JSON.stringify(report, null, 2), 'utf8'); + console.log(`Bericht geschrieben: ${outFile}`); + console.log( + `Zusammenfassung: ${report.summary.identicalToEnCount} Blätter noch identisch mit EN, ` + + `${report.summary.germanHintCount} Strings mit DE-Hinweis (heuristisch).` + ); +} + +audit(); diff --git a/frontend/src/i18n/locales/ceb/falukant.json b/frontend/src/i18n/locales/ceb/falukant.json index 0dacba8..a3d97aa 100644 --- a/frontend/src/i18n/locales/ceb/falukant.json +++ b/frontend/src/i18n/locales/ceb/falukant.json @@ -73,15 +73,15 @@ }, "character_illness": { "title": "Sakit", - "description": "Nagsakit si {characterName} ug nawad-an og {healthChange} health." + "description": "Nagsakit si {characterName} ug nawad-an og {healthChange} panglawas." }, "character_recovery": { "title": "Pagkamaayo", - "description": "Naayo si {characterName} gikan sa sakit ug nabawi ang {healthChange} health." + "description": "Naayo si {characterName} gikan sa sakit ug nabawi ang {healthChange} panglawas." }, "character_accident": { "title": "Aksidente", - "description": "Ang grabe nga aksidente nakapasakit pag-ayo kang {characterName}. Health: {healthChange}." + "description": "Ang grabe nga aksidente nakapasakit pag-ayo kang {characterName}. Panglawas: {healthChange}." }, "sudden_infant_death": { "title": "Kalit nga pagkamatay sa masuso", @@ -124,6 +124,16 @@ "children": "Mga anak", "children_unbaptised": "Mga anak nga wala pa mabunyagi" }, + "create": { + "title": "Apil sa duwa", + "gender": "Sekso", + "male": "Lalaki", + "female": "Babaye", + "firstname": "Ngalan", + "lastname": "Apelyido", + "random": "Random", + "submit": "Apil" + }, "overview": { "title": "Falukant - Overview", "heroIntro": "Ang imong kahimtang sa ekonomiya, pamilya ug kabtangan sa usa ka mubo ug klarong overview.", @@ -211,7 +221,10 @@ "branches": { "title": "Mga branch", "level": { - "city": "Siyudad" + "city": "Siyudad", + "production": "Produksyon", + "store": "Pagbaligya", + "fullstack": "Produksyon uban sa pagbaligya" } }, "routine": { @@ -443,7 +456,46 @@ } }, "sale": { - "runningGuards": "Mga guwardiya" + "title": "Imbentaryo", + "info": "Dinhi nimo makita ang katingbanan sa mga produkto sa branch.", + "region": "Rehiyon", + "product": "Produkto", + "quality": "Kalidad", + "quantity": "Kadaghanon", + "noInventory": "Walay imbentaryo nga available.", + "loadError": "Sayop sa pag-load sa imbentaryo.", + "sell": "Pagbaligya", + "sellButton": "Ibaligya", + "sellAllButton": "Ibaligya tanan", + "transportTitle": "Paghimo og transport", + "transportSource": "Artikulo", + "transportSourcePlaceholder": "Pilia ang artikulo", + "transportVehicle": "Sakyanan sa transport", + "transportVehiclePlaceholder": "Pilia ang sakyanan", + "transportTarget": "Target nga siyudad", + "transportTargetPlaceholder": "Pilia ang target", + "transportQuantity": "Kadaghanon", + "transportMax": "Pinakadaghan: {max}", + "transportCreate": "Sugdi ang transport", + "transportError": "Dili mahimo ang pag-set sa transport.", + "transportDuration": "Gidugayon sa transport: {duration}", + "transportArrival": "Oras sa pag-abot: {datetime}", + "transportRoute": "Ruta", + "transportCost": "Gasto sa transport: {cost}", + "transportStarted": "Nasugdan na ang transport.", + "runningTransportsTitle": "Nagdagan nga mga transport", + "runningDirection": "Direksyon", + "runningProduct": "Artikulo", + "runningQuantity": "Kadaghanon", + "runningNoProduct": "Transport sa sakyanan", + "runningSource": "Gigikanan", + "runningTarget": "Padulngan", + "runningEta": "Pag-abot", + "runningRemaining": "Nabiling oras", + "runningVehicleCount": "Mga sakyanan", + "runningGuards": "Mga guwardiya", + "runningDirectionOut": "Paingon gawas", + "runningDirectionIn": "Padulong sulod" }, "storage": { "buyPartialError": "Sayop sa pagpalit sa usa ka bahin sa kapasidad sa storage.", @@ -884,37 +936,37 @@ "name": "Ngalan", "age": "Edad", "gender": "Sekso", - "heir": "Heir", - "isHeir": "Heir", - "notHeir": "Not Heir", - "setAsHeir": "Set as Heir", - "heirSetSuccess": "Ang bata naay successfully set as heir.", - "heirSetError": "Sayop setting heir.", + "heir": "Manununod", + "isHeir": "Manununod", + "notHeir": "Dili manununod", + "setAsHeir": "Himua nga manununod", + "heirSetSuccess": "Nailhan na ang bata isip manununod.", + "heirSetError": "Napakyas ang pagtakda sa manununod.", "actions": "Aksyons", - "none": "No bataren available.", - "detailButton": "Show Details", - "addChild": "Add Bata", - "baptism": "Baptize", - "notBaptized": "Not yet baptized", - "baptismNotice": "This bata naay dili baptized yet ug therefore naay no name.", + "none": "Walay anak nga ipakita.", + "detailButton": "Ipakita ang detalye", + "addChild": "Dugangi og anak", + "baptism": "Bunyagi", + "notBaptized": "Wala pa mabunyagi", + "baptismNotice": "Wala pa mabunyagi kining bata, busa wala pa siyay ngalan.", "legitimacy": { - "legitimate": "Legitimate", - "acknowledged_bastard": "Ackahibalod illegitimate", - "hidden_bastard": "Illegitimate" + "legitimate": "Lehitimo", + "acknowledged_bastard": "Giilang dili-lehitimo", + "hidden_bastard": "Dili-lehitimo" }, - "otherParent": "Other parent", - "otherParentUnknown": "Unknown", - "birthContextLabel": "Origin", + "otherParent": "Pikas nga ginikanan", + "otherParentUnknown": "Wala mailhi", + "birthContextLabel": "Gigikanan", "birthContextShort": { "marriage": "Kasal", "lover": "Uyab" }, "birthContextLong": { - "marriage": "From kasal", - "lover": "From usa ka uyab" + "marriage": "Gikan sa kasal", + "lover": "Gikan sa usa ka uyab" }, "details": { - "title": "Bata Details" + "title": "Detalye sa bata" } }, "taxes": { diff --git a/frontend/src/i18n/locales/ceb/minigames.json b/frontend/src/i18n/locales/ceb/minigames.json index cf583a0..914d95f 100644 --- a/frontend/src/i18n/locales/ceb/minigames.json +++ b/frontend/src/i18n/locales/ceb/minigames.json @@ -38,7 +38,20 @@ "objectivesShow": "Ipakita ang mga tumong", "objectives": "Mga tumong", "loadingBoard": "Ginaandam ang game board...", - "loadingHint": "Ang datos sa level, mga tumong ug layout sa board gi-sync pa." + "loadingHint": "Ang datos sa level, mga tumong ug layout sa board gi-sync pa.", + "badgeMinigames": "Mga minidula", + "objectivesProgress": "{completed}/{total} ka tumong", + "movesLeftBadge": "{count} ka lihok ang nahabilin", + "playFocus": { + "pausedTitle": "Gi-pause ang dula", + "pausedDescription": "Ipadayon ang level o sugdan pag-usab nga kontrolado nga dili mawala ang konteksto.", + "preparingTitle": "Nag-load ang level", + "preparingDescription": "Kung na-load na ang level, makita dinhi ang sunod nga tumong ug angunay nga aksyon.", + "defaultObjectiveTitle": "Humana ang kasamtangang tumong", + "finishLevelTitle": "Humana ang level nga tarong", + "focusDescription": "Unahon kini nga tumong. Nahuman na: {completed} sa {total}.", + "allObjectivesDoneDescription": "Nahuman na ang tanang makita nga tumong. Karon, humana lang ang level nga tarong." + } }, "taxi": { "title": "Taxi Simulator", diff --git a/frontend/src/i18n/locales/de/minigames.json b/frontend/src/i18n/locales/de/minigames.json index 4379cac..53a001d 100644 --- a/frontend/src/i18n/locales/de/minigames.json +++ b/frontend/src/i18n/locales/de/minigames.json @@ -38,7 +38,20 @@ "objectivesShow": "Ziele anzeigen", "objectives": "Ziele", "loadingBoard": "Spielbrett wird vorbereitet...", - "loadingHint": "Leveldaten, Ziele und Feldlayout werden gerade synchronisiert." + "loadingHint": "Leveldaten, Ziele und Feldlayout werden gerade synchronisiert.", + "badgeMinigames": "Minispiele", + "objectivesProgress": "{completed}/{total} Ziele", + "movesLeftBadge": "{count} Züge übrig", + "playFocus": { + "pausedTitle": "Spiel ist pausiert", + "pausedDescription": "Setze das Level fort oder starte es kontrolliert neu, ohne den aktuellen Kontext zu verlieren.", + "preparingTitle": "Level wird vorbereitet", + "preparingDescription": "Sobald das Level geladen ist, erscheinen hier das nächste Ziel und die passende Hauptaktion.", + "defaultObjectiveTitle": "Aktuelles Ziel abschließen", + "finishLevelTitle": "Level sauber zu Ende spielen", + "focusDescription": "Konzentriere dich zuerst auf dieses Ziel. Bereits erledigt: {completed} von {total}.", + "allObjectivesDoneDescription": "Alle sichtbaren Ziele sind erledigt. Jetzt zählt nur noch der saubere Abschluss des Levels." + } }, "taxi": { "title": "Taxi Simulator", diff --git a/frontend/src/i18n/locales/en/minigames.json b/frontend/src/i18n/locales/en/minigames.json index 79463fb..82e0c6b 100644 --- a/frontend/src/i18n/locales/en/minigames.json +++ b/frontend/src/i18n/locales/en/minigames.json @@ -38,7 +38,20 @@ "objectivesShow": "Show objectives", "objectives": "Objectives", "loadingBoard": "Preparing game board...", - "loadingHint": "Level data, objectives and board layout are being synchronized." + "loadingHint": "Level data, objectives and board layout are being synchronized.", + "badgeMinigames": "Mini games", + "objectivesProgress": "{completed}/{total} objectives", + "movesLeftBadge": "{count} moves left", + "playFocus": { + "pausedTitle": "Game is paused", + "pausedDescription": "Resume the level or restart in a controlled way without losing context.", + "preparingTitle": "Level is loading", + "preparingDescription": "Once the level loads, the next goal and main action will appear here.", + "defaultObjectiveTitle": "Complete the current objective", + "finishLevelTitle": "Finish the level cleanly", + "focusDescription": "Focus on this goal first. Already completed: {completed} of {total}.", + "allObjectivesDoneDescription": "All visible objectives are done. Now finish the level cleanly." + } }, "taxi": { "title": "Taxi Simulator", diff --git a/frontend/src/i18n/locales/es/minigames.json b/frontend/src/i18n/locales/es/minigames.json index 370c417..abe2b1b 100644 --- a/frontend/src/i18n/locales/es/minigames.json +++ b/frontend/src/i18n/locales/es/minigames.json @@ -38,7 +38,20 @@ "objectivesShow": "Mostrar objetivos", "objectives": "Objetivos", "loadingBoard": "Preparando el tablero…", - "loadingHint": "Sincronizando datos del nivel, objetivos y disposición del campo." + "loadingHint": "Sincronizando datos del nivel, objetivos y disposición del campo.", + "badgeMinigames": "Minijuegos", + "objectivesProgress": "{completed}/{total} objetivos", + "movesLeftBadge": "{count} movimientos restantes", + "playFocus": { + "pausedTitle": "Juego en pausa", + "pausedDescription": "Continúa el nivel o reinícialo de forma controlada sin perder el contexto.", + "preparingTitle": "Cargando nivel", + "preparingDescription": "Cuando el nivel esté cargado, aquí aparecerá el siguiente objetivo y la acción principal.", + "defaultObjectiveTitle": "Completa el objetivo actual", + "finishLevelTitle": "Termina el nivel limpiamente", + "focusDescription": "Concéntrate primero en este objetivo. Ya completados: {completed} de {total}.", + "allObjectivesDoneDescription": "Todos los objetivos visibles están hechos. Ahora solo importa terminar el nivel limpiamente." + } }, "taxi": { "title": "Taxi Simulator", diff --git a/frontend/src/i18n/locales/fr/minigames.json b/frontend/src/i18n/locales/fr/minigames.json index a2723c5..ba9230f 100644 --- a/frontend/src/i18n/locales/fr/minigames.json +++ b/frontend/src/i18n/locales/fr/minigames.json @@ -38,7 +38,20 @@ "objectivesShow": "Afficher les objectifs", "objectives": "Objectifs", "loadingBoard": "Le plateau de jeu est en préparation...", - "loadingHint": "Les données de niveau, les objectifs et la disposition du terrain sont actuellement en cours de synchronisation." + "loadingHint": "Les données de niveau, les objectifs et la disposition du terrain sont actuellement en cours de synchronisation.", + "badgeMinigames": "Mini-jeux", + "objectivesProgress": "{completed}/{total} objectifs", + "movesLeftBadge": "{count} coups restants", + "playFocus": { + "pausedTitle": "Jeu en pause", + "pausedDescription": "Reprends le niveau ou relance-le proprement sans perdre le contexte.", + "preparingTitle": "Chargement du niveau", + "preparingDescription": "Une fois le niveau chargé, le prochain objectif et l’action principale apparaîtront ici.", + "defaultObjectiveTitle": "Terminer l’objectif en cours", + "finishLevelTitle": "Terminer le niveau proprement", + "focusDescription": "Concentre-toi d’abord sur cet objectif. Déjà réalisés : {completed} sur {total}.", + "allObjectivesDoneDescription": "Tous les objectifs visibles sont faits. Il ne reste plus qu’à finir le niveau proprement." + } }, "taxi": { "title": "Simulateur de taxi", diff --git a/frontend/src/views/minigames/Match3Game.vue b/frontend/src/views/minigames/Match3Game.vue index 5efc1d1..01d9ed3 100644 --- a/frontend/src/views/minigames/Match3Game.vue +++ b/frontend/src/views/minigames/Match3Game.vue @@ -2,28 +2,33 @@
- Minispiele + {{ $t('minigames.match3.badgeMinigames') }}

{{ $t('minigames.match3.title') }}

{{ $t('minigames.match3.campaignDescription') }}

- Nächster Schritt + {{ $t('minigames.match3.nextStep') }}

{{ playFocusTitle }}

{{ playFocusDescription }}

- Level {{ currentLevel }} - {{ completedObjectivesCount }}/{{ totalObjectivesCount || 0 }} Ziele - {{ safeMovesLeft }} Züge übrig + {{ $t('minigames.match3.level') }} {{ currentLevel }} + {{ + $t('minigames.match3.objectivesProgress', { + completed: completedObjectivesCount, + total: totalObjectivesCount || 0 + }) + }} + {{ $t('minigames.match3.movesLeftBadge', { count: safeMovesLeft }) }}