feat(localization): expand language support and enhance UI for user settings
All checks were successful
Deploy to production / deploy (push) Successful in 3m0s

- Added support for additional UI locales including Cebuano and Spanish, improving accessibility for a broader user base.
- Updated language selection components in the AppHeader and SettingsWidget to reflect new language options, enhancing user experience.
- Enhanced localization of various UI elements across components, ensuring consistent language representation and improved user engagement.
- Implemented logic to synchronize user language preferences with backend settings, providing a seamless experience when changing languages.
This commit is contained in:
Torsten Schulz (local)
2026-04-02 07:54:44 +02:00
parent ac5d436a36
commit 6d9d69dc10
72 changed files with 1792 additions and 343 deletions

View File

@@ -42,7 +42,7 @@
<strong>{{ getLessonTypeLabel(lesson.lessonType) }}</strong>
</div>
<div class="lesson-meta-item" v-if="lessonPedagogy.didacticMode">
<span class="lesson-meta-label">Fokus</span>
<span class="lesson-meta-label">{{ $t('socialnetwork.vocab.courses.lessonMetaFocus') }}</span>
<strong>{{ getDidacticModeLabel(lessonPedagogy.didacticMode) }}</strong>
</div>
<div class="lesson-meta-item">
@@ -74,15 +74,15 @@
</summary>
<div class="lesson-overview-more__grid">
<div class="lesson-meta-item" v-if="lessonPedagogy.phaseLabel">
<span class="lesson-meta-label">Phase</span>
<span class="lesson-meta-label">{{ $t('socialnetwork.vocab.courses.lessonMetaPhase') }}</span>
<strong>{{ getPhaseLabel(lessonPedagogy.phaseLabel) }}</strong>
</div>
<div class="lesson-meta-item" v-if="lessonPedagogy.newUnitTarget">
<span class="lesson-meta-label">Neue Einheiten</span>
<span class="lesson-meta-label">{{ $t('socialnetwork.vocab.courses.lessonMetaNewUnits') }}</span>
<strong>{{ lessonPedagogy.newUnitTarget }}</strong>
</div>
<div class="lesson-meta-item" v-if="lessonPedagogy.reviewWeight != null">
<span class="lesson-meta-label">Wiederholung</span>
<span class="lesson-meta-label">{{ $t('socialnetwork.vocab.courses.lessonMetaReview') }}</span>
<strong>{{ lessonPedagogy.reviewWeight }}%</strong>
</div>
</div>
@@ -90,8 +90,8 @@
</div>
<div v-if="lessonPedagogy.isIntensiveReview" class="lesson-intensity-banner">
<strong>Intensive Wiederholungsphase</strong>
<p>Diese Lektion priorisiert Wiederholung und Vertiefung. Neuer Stoff wird bewusst reduziert, damit vorhandene Muster stabil werden.</p>
<strong>{{ $t('socialnetwork.vocab.courses.intensiveReviewTitle') }}</strong>
<p>{{ $t('socialnetwork.vocab.courses.intensiveReviewIntro') }}</p>
</div>
<section class="lesson-primary-flow surface-card">
@@ -173,18 +173,18 @@
<div v-if="trainableLessonVocab.length > 0" class="vocab-trainer-section">
<h4>{{ $t('socialnetwork.vocab.courses.vocabTrainer') }}</h4>
<div v-if="hasPreviousVocab" class="review-priority-note">
<strong>Wiederholung läuft schrittweise mit</strong>
<p>Zuerst liegt der Fokus auf den neuen Begriffen dieser Lektion. Mit deinem Fortschritt fließen ältere Vokabeln dann zunehmend mit ein.</p>
<strong>{{ $t('socialnetwork.vocab.courses.reviewPriorityTitle') }}</strong>
<p>{{ $t('socialnetwork.vocab.courses.reviewPriorityIntro') }}</p>
</div>
<div v-if="hasExercises && !canAccessExercises" class="exercise-lock-note">
<strong>Kapitel-Prüfung noch gesperrt</strong>
<strong>{{ $t('socialnetwork.vocab.courses.exerciseLockTitle') }}</strong>
<p>{{ exerciseUnlockHint }}</p>
</div>
<div v-if="!vocabTrainerActive" class="vocab-trainer-start">
<template v-if="canStartVocabTrainerPrep">
<p>{{ hasPreviousVocab ? 'Starte mit den neuen Vokabeln dieser Lektion. Mit fortschreitendem Üben mischt der Trainer automatisch passende Wiederholungen ein.' : $t('socialnetwork.vocab.courses.vocabTrainerDescription') }}</p>
<p>{{ hasPreviousVocab ? $t('socialnetwork.vocab.courses.trainerStartWithReview') : $t('socialnetwork.vocab.courses.vocabTrainerDescription') }}</p>
<button @click="startVocabTrainer" class="btn-start-trainer">
{{ hasPreviousVocab ? 'Lektion starten' : $t('socialnetwork.vocab.courses.startVocabTrainer') }}
{{ hasPreviousVocab ? $t('socialnetwork.vocab.courses.startLesson') : $t('socialnetwork.vocab.courses.startVocabTrainer') }}
</button>
</template>
<p v-else class="vocab-trainer-locked-hint">{{ $t('socialnetwork.vocab.courses.vocabTrainerLockedHint') }}</p>
@@ -201,10 +201,10 @@
</div>
<div class="stats-row">
<span class="mode-badge" :class="{ 'mode-active': vocabTrainerPhase === 'current' }">
{{ $t('socialnetwork.vocab.courses.currentLesson') || 'Aktuelle Lektion' }}
{{ $t('socialnetwork.vocab.courses.currentLesson') }}
</span>
<span v-if="previousVocab && previousVocab.length > 0" class="mode-badge" :class="{ 'mode-active': vocabTrainerPhase === 'mixed' }">
{{ $t('socialnetwork.vocab.courses.mixedReview') || 'Gemischt' }}
{{ $t('socialnetwork.vocab.courses.mixedReview') }}
</span>
<span class="mode-badge" :class="{ 'mode-active': vocabTrainerMode === 'multiple_choice', 'mode-completed': vocabTrainerMode === 'typing' }">
{{ $t('socialnetwork.vocab.courses.modeMultipleChoice') }}
@@ -215,9 +215,9 @@
<button @click="stopVocabTrainer" class="btn-stop-trainer">{{ $t('socialnetwork.vocab.courses.stopTrainer') }}</button>
</div>
<div v-if="hasPreviousVocab" class="stats-row trainer-progress-row">
<span>Neue Inhalte: {{ vocabTrainerCurrentAttempts }}/{{ trainerNewFocusTarget }}</span>
<span>Wiederholung: {{ vocabTrainerReviewAttempts }}</span>
<span>Mischanteil: {{ Math.round(currentReviewShare * 100) }}%</span>
<span>{{ $t('socialnetwork.vocab.courses.trainerProgressNewContent', { current: vocabTrainerCurrentAttempts, target: trainerNewFocusTarget }) }}</span>
<span>{{ $t('socialnetwork.vocab.courses.trainerProgressReview', { count: vocabTrainerReviewAttempts }) }}</span>
<span>{{ $t('socialnetwork.vocab.courses.trainerProgressMixShare', { percent: Math.round(currentReviewShare * 100) }) }}</span>
</div>
</div>
<div v-if="currentVocabQuestion" class="vocab-question">
@@ -796,8 +796,8 @@
<!-- Fallback für unbekannte Typen -->
<div v-else class="unknown-exercise">
<p>Dieser Übungstyp wird in der aktuellen Ansicht noch nicht interaktiv dargestellt.</p>
<p class="unknown-exercise__type">Typ: {{ getExerciseType(exercise) }}</p>
<p>{{ $t('socialnetwork.vocab.courses.unknownExerciseTypeNotice') }}</p>
<p class="unknown-exercise__type">{{ $t('socialnetwork.vocab.courses.unknownExerciseTypeLabel', { type: getExerciseType(exercise) }) }}</p>
</div>
</div>
</div>
@@ -1231,12 +1231,7 @@ export default {
if (!progress?.completed) {
return '';
}
const stage = Number(progress.reviewStage || 0);
if (stage === 0) return 'Tag 1';
if (stage === 1) return 'Tag 3';
if (stage === 2) return 'Tag 7';
if (stage >= 3) return 'Review abgeschlossen';
return '';
return this.getReviewStageLabel(progress);
},
lessonReviewStatusClass() {
const progress = this.lessonProgress;
@@ -1257,12 +1252,12 @@ export default {
return '';
}
if (progress.reviewCompleted) {
return 'Diese Lektion ist in der freien Vertiefung angekommen.';
return this.$t('socialnetwork.vocab.courses.lessonReviewHeadlineDone');
}
if (progress.reviewDue) {
return 'Diese Review-Welle ist jetzt fällig.';
return this.$t('socialnetwork.vocab.courses.lessonReviewHeadlineDue');
}
return 'Diese Lektion ist für die nächste Review-Welle vorgemerkt.';
return this.$t('socialnetwork.vocab.courses.lessonReviewHeadlineScheduled');
},
lessonReviewHint() {
const progress = this.lessonProgress;
@@ -1270,9 +1265,11 @@ export default {
return '';
}
if (progress.reviewCompleted) {
return 'Die 1/3/7-Tage-Wiederholung ist abgeschlossen. Du kannst die Lektion jetzt flexibel weitertrainieren.';
return this.$t('socialnetwork.vocab.courses.lessonReviewHintDone');
}
return `Nächste Fälligkeit: ${this.formatLessonReviewDue(progress.reviewNextDueAt)}.`;
return this.$t('socialnetwork.vocab.courses.lessonReviewHintNextDue', {
due: this.formatLessonReviewDue(progress.reviewNextDueAt)
});
},
assistantAvailable() {
if (!this.assistantSettings) {
@@ -2019,35 +2016,43 @@ export default {
getPhaseLabel(phaseLabel) {
switch (phaseLabel) {
case 'quickstart':
return 'Schnellstart';
return this.$t('socialnetwork.vocab.courses.phaseQuickstart');
case 'daily_life':
return 'Alltag';
return this.$t('socialnetwork.vocab.courses.phaseDailyLife');
case 'stabilization':
return 'Stabilisierung';
return this.$t('socialnetwork.vocab.courses.phaseStabilization');
default:
return 'Lernphase';
return this.$t('socialnetwork.vocab.courses.phaseDefault');
}
},
getDidacticModeLabel(didacticMode) {
switch (didacticMode) {
case 'core_input':
return 'Neuer Stoff';
return this.$t('socialnetwork.vocab.courses.didacticModeCoreInput');
case 'guided_dialogue':
return 'Geführter Dialog';
return this.$t('socialnetwork.vocab.courses.didacticModeGuidedDialogue');
case 'contrast_training':
return 'Kontrasttraining';
return this.$t('socialnetwork.vocab.courses.didacticModeContrastTraining');
case 'pattern_drill':
return 'Mustertraining';
return this.$t('socialnetwork.vocab.courses.didacticModePatternDrill');
case 'real_life_scenario':
return 'Alltagsszenario';
return this.$t('socialnetwork.vocab.courses.didacticModeRealLifeScenario');
case 'intensive_review':
return 'Wiederholungsphase';
return this.$t('socialnetwork.vocab.courses.didacticModeIntensiveReview');
case 'checkpoint':
return 'Checkpoint';
return this.$t('socialnetwork.vocab.courses.didacticModeCheckpoint');
default:
return 'Lernfokus';
return this.$t('socialnetwork.vocab.courses.didacticModeFocusDefault');
}
},
getReviewStageLabel(progress) {
const stage = Number(progress?.reviewStage || 0);
if (stage === 0) return this.$t('socialnetwork.vocab.courses.reviewStageDay1');
if (stage === 1) return this.$t('socialnetwork.vocab.courses.reviewStageDay3');
if (stage === 2) return this.$t('socialnetwork.vocab.courses.reviewStageDay7');
if (stage >= 3) return this.$t('socialnetwork.vocab.courses.reviewStageCompleted');
return '';
},
formatTargetMinutes(targetMinutes) {
const minutes = Number(targetMinutes);
if (!minutes) {
@@ -2057,28 +2062,28 @@ export default {
},
formatLessonReviewDue(reviewNextDueAt) {
if (!reviewNextDueAt) {
return 'jetzt';
return this.$t('socialnetwork.vocab.courses.reviewTimeNow');
}
const dueTimestamp = new Date(reviewNextDueAt).getTime();
if (!Number.isFinite(dueTimestamp)) {
return 'jetzt';
return this.$t('socialnetwork.vocab.courses.reviewTimeNow');
}
const diffMs = dueTimestamp - Date.now();
if (diffMs > 0) {
const untilDays = Math.ceil(diffMs / (24 * 60 * 60 * 1000));
if (untilDays <= 1) {
return 'morgen';
return this.$t('socialnetwork.vocab.courses.reviewTimeTomorrow');
}
return `in ${untilDays} Tagen`;
return this.$t('socialnetwork.vocab.courses.reviewTimeInDays', { count: untilDays });
}
const overdueDays = Math.floor((Date.now() - dueTimestamp) / (24 * 60 * 60 * 1000));
if (overdueDays <= 0) {
return 'heute';
return this.$t('socialnetwork.vocab.courses.timeToday');
}
if (overdueDays === 1) {
return 'seit 1 Tag';
return this.$t('socialnetwork.vocab.courses.timeSinceOneDay');
}
return `seit ${overdueDays} Tagen`;
return this.$t('socialnetwork.vocab.courses.timeSinceDays', { count: overdueDays });
},
getQuestionData(exercise) {
if (!exercise.questionData) return null;