feat(bisaya-course): enhance German course content and localization support
All checks were successful
Deploy to production / deploy (push) Successful in 2m47s

- Updated the create-german-for-bisaya-course-content.js script to improve lesson pattern retrieval by introducing a new function for generating a lesson pattern pool.
- Added new exercises for various topics including 'Wohnung & Nachbarn', 'Besuch empfangen', 'Arzt, Apotheke, Termin', and 'Amt, Dokumente, Anmeldung', enhancing practical language skills for learners.
- Improved localization by integrating translation keys for various UI elements and error messages across multiple components, ensuring a consistent user experience in both German and Bisaya.
- Enhanced the main.js file to recognize Bisaya language preferences in browser settings, improving accessibility for users.
This commit is contained in:
Torsten Schulz (local)
2026-03-31 17:40:03 +02:00
parent b1990334b9
commit c6caeefb5f
49 changed files with 3468 additions and 262 deletions

View File

@@ -1,61 +1,61 @@
<template>
<div class="blog-editor">
<h1>{{ isEdit ? 'Blog bearbeiten' : 'Blog erstellen' }}</h1>
<h1>{{ isEdit ? $t('blog.editor.editTitle') : $t('blog.editor.createTitle') }}</h1>
<form @submit.prevent="save">
<div>
<label>Titel</label>
<label>{{ $t('blog.title') }}</label>
<input v-model="form.title" required />
</div>
<div>
<label>Beschreibung</label>
<label>{{ $t('blog.editor.description') }}</label>
<textarea v-model="form.description"></textarea>
</div>
<div>
<label>Sichtbarkeit</label>
<label>{{ $t('blog.editor.visibility') }}</label>
<select v-model="form.visibility">
<option value="public">Öffentlich</option>
<option value="logged_in">Nur eingeloggte Nutzer</option>
<option value="public">{{ $t('blog.editor.visibilityPublic') }}</option>
<option value="logged_in">{{ $t('blog.editor.visibilityLoggedIn') }}</option>
</select>
</div>
<div v-if="form.visibility === 'logged_in'">
<label>Altersbereich</label>
<label>{{ $t('blog.editor.ageRange') }}</label>
<div class="row">
<input type="number" min="0" v-model.number="form.ageMin" placeholder="min" />
<input type="number" min="0" v-model.number="form.ageMax" placeholder="max" />
</div>
<label>Geschlecht</label>
<label>{{ $t('blog.editor.gender') }}</label>
<div class="row">
<label><input type="checkbox" value="m" v-model="genderSel"> Männlich</label>
<label><input type="checkbox" value="f" v-model="genderSel"> Weiblich</label>
<label><input type="checkbox" value="m" v-model="genderSel"> {{ $t('blog.editor.genderMale') }}</label>
<label><input type="checkbox" value="f" v-model="genderSel"> {{ $t('blog.editor.genderFemale') }}</label>
</div>
</div>
<button class="btn" type="submit">Speichern</button>
<button class="btn" type="submit">{{ $t('blog.editor.save') }}</button>
</form>
<div v-if="isEdit" class="post-editor">
<h2>Neuer Beitrag</h2>
<h2>{{ $t('blog.editor.newPostTitle') }}</h2>
<form @submit.prevent="addPost">
<input v-model="post.title" placeholder="Titel" required />
<input v-model="post.title" :placeholder="$t('blog.title')" required />
<RichTextEditor v-model="post.content" :blog-id="$route.params.id" />
<button class="btn" type="submit">Beitrag hinzufügen</button>
<button class="btn" type="submit">{{ $t('blog.editor.addPost') }}</button>
</form>
</div>
<div v-if="isEdit" class="share-section">
<h2>Blog teilen</h2>
<h2>{{ $t('blog.editor.shareTitle') }}</h2>
<div class="share-url">
<label>URL</label>
<label>{{ $t('blog.editor.url') }}</label>
<input :value="currentShareUrl" readonly @focus="$event.target.select()" />
<button class="btn" type="button" @click="copyUrl">Link kopieren</button>
<button class="btn" type="button" @click="copyUrl">{{ $t('blog.editor.copyLink') }}</button>
</div>
<div class="share-actions">
<button class="btn" type="button" @click="shareToFriends">An Freunde senden</button>
<button class="btn" type="button" @click="shareToFriends">{{ $t('blog.editor.shareToFriends') }}</button>
</div>
<div class="share-email">
<label>E-Mail-Adressen (Kommagetrennt)</label>
<label>{{ $t('blog.editor.emailAddresses') }}</label>
<input v-model="emailInput" placeholder="name@example.com, second@example.org" />
<button class="btn" type="button" @click="shareToEmails">Senden</button>
<p v-if="form.visibility !== 'public'" class="hint">Hinweis: Dieser Blog ist nicht öffentlich. Empfänger benötigen ggf. ein Login und passende Alters/Geschlechts-Berechtigung.</p>
<button class="btn" type="button" @click="shareToEmails">{{ $t('blog.editor.send') }}</button>
<p v-if="form.visibility !== 'public'" class="hint">{{ $t('blog.editor.restrictedHint') }}</p>
</div>
<p v-if="shareStatus" class="status">{{ shareStatus }}</p>
</div>
@@ -108,7 +108,7 @@ export default {
async save() {
if (this.form.visibility === 'logged_in') {
if (this.form.ageMin != null && this.form.ageMax != null && this.form.ageMin > this.form.ageMax) {
showError(this, 'Ungültiger Altersbereich');
showError(this, 'tr:blog.editor.invalidAgeRange');
return;
}
}
@@ -152,9 +152,9 @@ export default {
const url = this.currentShareUrl || this.blogAbsoluteUrl();
try {
await navigator.clipboard.writeText(url);
this.shareStatus = 'Link kopiert';
this.shareStatus = this.$t('blog.editor.copySuccess');
} catch {
this.shareStatus = 'Kopieren fehlgeschlagen';
this.shareStatus = this.$t('blog.editor.copyError');
}
setTimeout(() => (this.shareStatus = ''), 2000);
},
@@ -162,9 +162,9 @@ export default {
try {
const res = await shareBlog(this.$route.params.id, { toFriends: true });
if (res.url) this.currentShareUrl = res.url;
this.shareStatus = `An ${res.notifiedFriends || 0} Freund(e) gesendet.`;
this.shareStatus = this.$t('blog.editor.friendsSent', { count: res.notifiedFriends || 0 });
} catch (e) {
this.shareStatus = 'Teilen fehlgeschlagen';
this.shareStatus = this.$t('blog.editor.shareError');
}
setTimeout(() => (this.shareStatus = ''), 3000);
},
@@ -174,9 +174,9 @@ export default {
try {
const res = await shareBlog(this.$route.params.id, { emails });
if (res.url) this.currentShareUrl = res.url;
this.shareStatus = `${res.emailsSent || 0} E-Mail(s) versendet.`;
this.shareStatus = this.$t('blog.editor.emailsSent', { count: res.emailsSent || 0 });
} catch (e) {
this.shareStatus = 'E-Mail-Versand fehlgeschlagen';
this.shareStatus = this.$t('blog.editor.emailError');
}
setTimeout(() => (this.shareStatus = ''), 3000);
}

View File

@@ -2,23 +2,23 @@
<div class="blog-list">
<section class="blog-list__hero surface-card">
<div>
<span class="blog-list__kicker">Community-Blogs</span>
<h1>Blogs</h1>
<p>Artikel, Projektstände und persönliche Einblicke aus der YourPart-Community.</p>
<span class="blog-list__kicker">{{ $t('blog.list.eyebrow') }}</span>
<h1>{{ $t('blog.list.title') }}</h1>
<p>{{ $t('blog.list.intro') }}</p>
</div>
<div class="toolbar">
<router-link v-if="$store.getters.isLoggedIn" class="btn" to="/blogs/create">Neuen Blog erstellen</router-link>
<router-link v-if="$store.getters.isLoggedIn" class="btn" to="/blogs/create">{{ $t('blog.list.create') }}</router-link>
</div>
</section>
<div v-if="loading" class="blog-list__state surface-card">Laden</div>
<div v-else-if="!blogs.length" class="blog-list__state surface-card">Keine Blogs gefunden.</div>
<div v-if="loading" class="blog-list__state surface-card">{{ $t('blog.list.loading') }}</div>
<div v-else-if="!blogs.length" class="blog-list__state surface-card">{{ $t('blog.list.empty') }}</div>
<div v-else class="blog-grid">
<article v-for="b in blogs" :key="b.id" class="blog-card surface-card">
<div class="blog-card__meta">von {{ b.owner?.username || 'Unbekannt' }}</div>
<div class="blog-card__meta">{{ $t('blog.list.by') }} {{ b.owner?.username || $t('blog.list.unknownAuthor') }}</div>
<h2><router-link :to="blogUrl(b)">{{ b.title }}</router-link></h2>
<p>{{ blogExcerpt(b) }}</p>
<router-link class="blog-card__link" :to="blogUrl(b)">Zum Blog</router-link>
<router-link class="blog-card__link" :to="blogUrl(b)">{{ $t('blog.list.open') }}</router-link>
</article>
</div>
</div>
@@ -39,7 +39,7 @@ export default {
return slug ? `/blogs/${encodeURIComponent(slug)}` : `/blogs/${blog.id}`;
},
blogExcerpt(blog) {
const source = blog?.description || 'Öffentliche Einträge, Gedanken und Projektstände aus der Community.';
const source = blog?.description || this.$t('blog.list.fallbackExcerpt');
return source.length > 150 ? `${source.slice(0, 147)}...` : source;
},
},

View File

@@ -1,6 +1,6 @@
<template>
<div class="blog-view">
<div v-if="loading" class="blog-view__state surface-card">Laden</div>
<div v-if="loading" class="blog-view__state surface-card">{{ $t('blog.view.loading') }}</div>
<div v-else-if="blog" class="blog-layout">
<section class="blog-hero surface-card">
<div>
@@ -9,16 +9,16 @@
<p v-if="blog.description" class="blog-description">{{ blog.description }}</p>
</div>
<div v-if="$store.getters.isLoggedIn" class="actions">
<router-link class="editbutton" v-if="isOwner" :to="{ name: 'BlogEdit', params: { id: blog.id } }">Bearbeiten</router-link>
<router-link class="editbutton" v-if="isOwner" :to="{ name: 'BlogEdit', params: { id: blog.id } }">{{ $t('blog.view.edit') }}</router-link>
</div>
</section>
<div class="blog-content">
<section class="posts surface-card">
<div class="posts__header">
<h2>{{ $t('blog.posts') }}</h2>
<span class="posts__count">{{ total }} Einträge</span>
<span class="posts__count">{{ $t('blog.view.entriesCount', { count: total }) }}</span>
</div>
<div v-if="!items.length" class="blog-view__state">Keine Einträge vorhanden.</div>
<div v-if="!items.length" class="blog-view__state">{{ $t('blog.view.empty') }}</div>
<article v-for="p in items" :key="p.id" class="post">
<h3>{{ p.title }}</h3>
<div class="content" v-html="sanitize(p.content)" />
@@ -89,7 +89,7 @@ export default {
.map((item) => `${item.title || ''} ${stripHtml(item.content || '')}`.trim())
.filter(Boolean)
.join(' ');
const summarySource = this.blog.description || plainTextPosts || 'Öffentlicher Community-Blog auf YourPart.';
const summarySource = this.blog.description || plainTextPosts || this.$t('blog.view.fallbackDescription');
const description = truncateText(summarySource, 160);
const canonicalPath = this.canonicalBlogPath();
@@ -146,8 +146,8 @@ export default {
console.error('Blog konnte nicht geladen werden:', e);
// this.$router.replace('/blogs');
applySeo({
title: 'Blog nicht gefunden | YourPart',
description: 'Der angeforderte Blog konnte nicht geladen werden.',
title: this.$t('blog.view.notFoundTitle'),
description: this.$t('blog.view.notFoundDescription'),
canonicalPath: '/blogs',
robots: 'noindex, nofollow',
});