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:
@@ -4,7 +4,7 @@
|
||||
<div v-else-if="course">
|
||||
<section class="course-hero surface-card">
|
||||
<div>
|
||||
<span class="course-kicker">Lernkurs</span>
|
||||
<span class="course-kicker">{{ $t('socialnetwork.vocab.courses.courseKicker') }}</span>
|
||||
<h2>{{ course.title }}</h2>
|
||||
<p v-if="course.description">{{ course.description }}</p>
|
||||
</div>
|
||||
@@ -41,13 +41,13 @@
|
||||
<section v-if="course.lessons && course.lessons.length > 0" class="surface-card course-flow">
|
||||
<div class="course-flow__header">
|
||||
<div>
|
||||
<span class="course-flow__eyebrow">Tagesfluss</span>
|
||||
<h3>Heute sinnvoll weitermachen</h3>
|
||||
<p>Die Reihenfolge folgt dem Konzept: fällige Wiederholung zuerst, dann aktueller Block, danach Intensivphase und freie Vertiefung.</p>
|
||||
<span class="course-flow__eyebrow">{{ $t('socialnetwork.vocab.courses.courseFlowEyebrow') }}</span>
|
||||
<h3>{{ $t('socialnetwork.vocab.courses.courseFlowTitle') }}</h3>
|
||||
<p>{{ $t('socialnetwork.vocab.courses.courseFlowIntro') }}</p>
|
||||
</div>
|
||||
<div class="course-flow__stats">
|
||||
<span class="course-flow__stat">Fällige Wiederholung: {{ dueReviewLessons.length }}</span>
|
||||
<span class="course-flow__stat">Aktiver Block: {{ currentBlockNumber || '—' }}</span>
|
||||
<span class="course-flow__stat">{{ $t('socialnetwork.vocab.courses.courseFlowReviewStat', { count: dueReviewLessons.length }) }}</span>
|
||||
<span class="course-flow__stat">{{ $t('socialnetwork.vocab.courses.courseFlowBlockStat', { block: currentBlockNumber || '—' }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
<div class="course-flow-card__top">
|
||||
<span class="course-flow-card__badge course-flow-card__badge--review">1</span>
|
||||
<div>
|
||||
<h4>Fällige Wiederholung</h4>
|
||||
<p>Bereits abgeschlossene Lektionen, die heute wieder drankommen sollten.</p>
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.courseFlowReviewTitle') }}</h4>
|
||||
<p>{{ $t('socialnetwork.vocab.courses.courseFlowReviewDescription') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="dueReviewLessons.length > 0" class="course-flow-card__list">
|
||||
@@ -72,15 +72,15 @@
|
||||
<span>{{ formatReviewDue(getLessonProgress(lesson.id)?.reviewNextDueAt) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="course-flow-card__empty">Heute ist keine ältere Lektion als fällige Wiederholung markiert.</p>
|
||||
<p v-else class="course-flow-card__empty">{{ $t('socialnetwork.vocab.courses.courseFlowReviewEmpty') }}</p>
|
||||
</article>
|
||||
|
||||
<article class="course-flow-card">
|
||||
<div class="course-flow-card__top">
|
||||
<span class="course-flow-card__badge course-flow-card__badge--block">2</span>
|
||||
<div>
|
||||
<h4>Aktueller Block</h4>
|
||||
<p>Hier liegt der nächste reguläre Fortschritt im Kurs.</p>
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.courseFlowBlockTitle') }}</h4>
|
||||
<p>{{ $t('socialnetwork.vocab.courses.courseFlowBlockDescription') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentBlockLessons.length > 0" class="course-flow-card__list">
|
||||
@@ -95,15 +95,15 @@
|
||||
<span>#{{ lesson.lessonNumber }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="course-flow-card__empty">Der aktuelle Block ist bereits abgeschlossen oder es gibt gerade keine offene Blocklektion.</p>
|
||||
<p v-else class="course-flow-card__empty">{{ $t('socialnetwork.vocab.courses.courseFlowBlockEmpty') }}</p>
|
||||
</article>
|
||||
|
||||
<article class="course-flow-card">
|
||||
<div class="course-flow-card__top">
|
||||
<span class="course-flow-card__badge course-flow-card__badge--intensive">3</span>
|
||||
<div>
|
||||
<h4>Fällige Intensivphase</h4>
|
||||
<p>Verdichtete Wiederholung, sobald der Block davor weitgehend sitzt.</p>
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveTitle') }}</h4>
|
||||
<p>{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveDescription') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="nextIntensiveReviewLesson" class="course-flow-card__list">
|
||||
@@ -113,18 +113,18 @@
|
||||
@click="openLesson(nextIntensiveReviewLesson.id)"
|
||||
>
|
||||
<strong>{{ nextIntensiveReviewLesson.title }}</strong>
|
||||
<span>Block {{ nextIntensiveReviewLesson.pedagogy?.blockNumber || '—' }}</span>
|
||||
<span>{{ $t('socialnetwork.vocab.courses.lessonBlockLabel', { number: nextIntensiveReviewLesson.pedagogy?.blockNumber || '—' }) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="course-flow-card__empty">Aktuell ist keine neue Intensivphase freigeschaltet.</p>
|
||||
<p v-else class="course-flow-card__empty">{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveEmpty') }}</p>
|
||||
</article>
|
||||
|
||||
<article class="course-flow-card">
|
||||
<div class="course-flow-card__top">
|
||||
<span class="course-flow-card__badge course-flow-card__badge--practice">4</span>
|
||||
<div>
|
||||
<h4>Freie Vertiefung</h4>
|
||||
<p>Abgeschlossene Lektionen für lockeres Nachtrainieren außerhalb des Pflichtpfads.</p>
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.courseFlowPracticeTitle') }}</h4>
|
||||
<p>{{ $t('socialnetwork.vocab.courses.courseFlowPracticeDescription') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="freePracticeLessons.length > 0" class="course-flow-card__list">
|
||||
@@ -136,10 +136,10 @@
|
||||
@click="openLessonPractice(lesson)"
|
||||
>
|
||||
<strong>{{ lesson.title }}</strong>
|
||||
<span>Im Trainer üben</span>
|
||||
<span>{{ $t('socialnetwork.vocab.courses.practiceInTrainer') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="course-flow-card__empty">Sobald du erste Lektionen abgeschlossen hast, erscheinen sie hier für freies Nachtrainieren.</p>
|
||||
<p v-else class="course-flow-card__empty">{{ $t('socialnetwork.vocab.courses.courseFlowPracticeEmpty') }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@@ -157,7 +157,7 @@
|
||||
</div>
|
||||
<div class="lessons-header">
|
||||
<h3>{{ $t('socialnetwork.vocab.courses.lessons') }}</h3>
|
||||
<span class="lessons-count">{{ course.lessons.length }} Lektionen</span>
|
||||
<span class="lessons-count">{{ $t('socialnetwork.vocab.courses.lessonsCount', { count: course.lessons.length }) }}</span>
|
||||
</div>
|
||||
<div class="lesson-cards">
|
||||
<article v-for="lesson in sortedLessons" :key="lesson.id" class="lesson-card">
|
||||
@@ -189,8 +189,8 @@
|
||||
<div class="lesson-pedagogy" v-if="lesson.pedagogy">
|
||||
<span class="lesson-chip lesson-chip--phase">{{ getPhaseLabel(lesson.pedagogy.phaseLabel) }}</span>
|
||||
<span class="lesson-chip lesson-chip--mode">{{ getDidacticModeLabel(lesson.pedagogy.didacticMode) }}</span>
|
||||
<span v-if="lesson.pedagogy.blockNumber" class="lesson-chip lesson-chip--block">Block {{ lesson.pedagogy.blockNumber }}</span>
|
||||
<span v-if="lesson.pedagogy.isIntensiveReview" class="lesson-chip lesson-chip--intensive">Intensive Wiederholung</span>
|
||||
<span v-if="lesson.pedagogy.blockNumber" class="lesson-chip lesson-chip--block">{{ $t('socialnetwork.vocab.courses.lessonBlockLabel', { number: lesson.pedagogy.blockNumber }) }}</span>
|
||||
<span v-if="lesson.pedagogy.isIntensiveReview" class="lesson-chip lesson-chip--intensive">{{ $t('socialnetwork.vocab.courses.lessonIntensiveBadge') }}</span>
|
||||
</div>
|
||||
<div class="lesson-actions-content">
|
||||
<button
|
||||
@@ -206,7 +206,7 @@
|
||||
@click="openLessonPractice(lesson)"
|
||||
class="btn-edit"
|
||||
>
|
||||
Im Trainer üben
|
||||
{{ $t('socialnetwork.vocab.courses.practiceInTrainer') }}
|
||||
</button>
|
||||
<button v-if="isOwner" @click="editLesson(lesson.id)" class="btn-edit">{{ $t('socialnetwork.vocab.courses.edit') }}</button>
|
||||
<button v-if="isOwner" @click="deleteLesson(lesson.id)" class="btn-delete">{{ $t('general.delete') }}</button>
|
||||
@@ -245,7 +245,7 @@
|
||||
<option v-for="chapter in chapters" :key="chapter.id" :value="chapter.id">{{ chapter.title }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<span v-if="lessonFormTouched && !canCreateLesson" class="form-error">Bitte Nummer, Titel und Kapitel vollständig angeben.</span>
|
||||
<span v-if="lessonFormTouched && !canCreateLesson" class="form-error">{{ $t('socialnetwork.vocab.courses.addLessonValidation') }}</span>
|
||||
<div class="form-actions form-actions-row">
|
||||
<button type="submit" :disabled="!canCreateLesson">{{ $t('general.create') }}</button>
|
||||
<button type="button" @click="showAddLessonDialog = false" class="button-secondary">{{ $t('general.cancel') }}</button>
|
||||
@@ -460,44 +460,44 @@ export default {
|
||||
formatDaysSince(dateString) {
|
||||
const days = this.daysSince(dateString);
|
||||
if (days <= 0) {
|
||||
return 'heute';
|
||||
return this.$t('socialnetwork.vocab.courses.timeToday');
|
||||
}
|
||||
if (days === 1) {
|
||||
return 'seit 1 Tag';
|
||||
return this.$t('socialnetwork.vocab.courses.timeSinceOneDay');
|
||||
}
|
||||
return `seit ${days} Tagen`;
|
||||
return this.$t('socialnetwork.vocab.courses.timeSinceDays', { count: days });
|
||||
},
|
||||
formatReviewDue(reviewNextDueAt) {
|
||||
if (!reviewNextDueAt) {
|
||||
return 'jetzt fällig';
|
||||
return this.$t('socialnetwork.vocab.courses.reviewDueNow');
|
||||
}
|
||||
const dueTimestamp = new Date(reviewNextDueAt).getTime();
|
||||
if (!Number.isFinite(dueTimestamp)) {
|
||||
return 'jetzt fällig';
|
||||
return this.$t('socialnetwork.vocab.courses.reviewDueNow');
|
||||
}
|
||||
const diffMs = dueTimestamp - Date.now();
|
||||
if (diffMs > 0) {
|
||||
const untilDays = Math.ceil(diffMs / (24 * 60 * 60 * 1000));
|
||||
if (untilDays <= 1) {
|
||||
return 'morgen fällig';
|
||||
return this.$t('socialnetwork.vocab.courses.reviewDueTomorrow');
|
||||
}
|
||||
return `in ${untilDays} Tagen fällig`;
|
||||
return this.$t('socialnetwork.vocab.courses.reviewDueInDays', { count: untilDays });
|
||||
}
|
||||
const diffDays = Math.floor((Date.now() - dueTimestamp) / (24 * 60 * 60 * 1000));
|
||||
if (diffDays <= 0) {
|
||||
return 'heute fällig';
|
||||
return this.$t('socialnetwork.vocab.courses.reviewDueToday');
|
||||
}
|
||||
if (diffDays === 1) {
|
||||
return 'seit 1 Tag fällig';
|
||||
return this.$t('socialnetwork.vocab.courses.reviewDueSinceOneDay');
|
||||
}
|
||||
return `seit ${diffDays} Tagen fällig`;
|
||||
return this.$t('socialnetwork.vocab.courses.reviewDueSinceDays', { count: diffDays });
|
||||
},
|
||||
getReviewStageLabel(progress) {
|
||||
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';
|
||||
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 '';
|
||||
},
|
||||
getReviewBadgeLabel(progress) {
|
||||
@@ -564,15 +564,15 @@ export default {
|
||||
chapterId: null
|
||||
};
|
||||
await this.loadCourse();
|
||||
showSuccess(this, 'Lektion erfolgreich angelegt.');
|
||||
showSuccess(this, this.$t('socialnetwork.vocab.courses.addLessonSuccess'));
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Hinzufügen der Lektion:', e);
|
||||
showApiError(this, e, 'Fehler beim Hinzufügen der Lektion');
|
||||
showApiError(this, e, this.$t('socialnetwork.vocab.courses.addLessonError'));
|
||||
}
|
||||
},
|
||||
async deleteLesson(lessonId) {
|
||||
const confirmed = await confirmAction(this, {
|
||||
title: 'Lektion löschen',
|
||||
title: this.$t('socialnetwork.vocab.courses.deleteLessonTitle'),
|
||||
message: this.$t('socialnetwork.vocab.courses.confirmDelete')
|
||||
});
|
||||
if (!confirmed) {
|
||||
@@ -581,10 +581,10 @@ export default {
|
||||
try {
|
||||
await apiClient.delete(`/api/vocab/lessons/${lessonId}`);
|
||||
await this.loadCourse();
|
||||
showSuccess(this, 'Lektion erfolgreich gelöscht.');
|
||||
showSuccess(this, this.$t('socialnetwork.vocab.courses.deleteLessonSuccess'));
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Löschen der Lektion:', e);
|
||||
showApiError(this, e, 'Fehler beim Löschen der Lektion');
|
||||
showApiError(this, e, this.$t('socialnetwork.vocab.courses.deleteLessonError'));
|
||||
}
|
||||
},
|
||||
openLesson(lessonId) {
|
||||
@@ -608,37 +608,37 @@ 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 'Lerneinheit';
|
||||
return this.$t('socialnetwork.vocab.courses.didacticModeDefault');
|
||||
}
|
||||
},
|
||||
editLesson() {
|
||||
showInfo(this, 'Die Bearbeitung einzelner Lektionen folgt noch.');
|
||||
showInfo(this, this.$t('socialnetwork.vocab.courses.editLessonPending'));
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
|
||||
Reference in New Issue
Block a user