feat(localization): expand language support and enhance UI for user settings
All checks were successful
Deploy to production / deploy (push) Successful in 3m0s
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user