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

@@ -4,10 +4,12 @@
<div class="footer-system">
<button class="footer-brand" type="button" @click="showFalukantDaemonStatus">
<img src="/images/icons/logo_color.png" alt="YourPart" />
<span>System</span>
<span>{{ $t('appShell.footer.systemLabel') }}</span>
</button>
<span class="footer-caption">
{{ openDialogs.length === 0 ? 'Keine offenen Dialoge' : `${openDialogs.length} Fenster aktiv` }}
{{ openDialogs.length === 0
? $t('appShell.footer.noOpenDialogs')
: $t('appShell.footer.activeWindows', { count: openDialogs.length }) }}
</span>
</div>
@@ -23,7 +25,7 @@
<span class="button-text">{{ dialog.dialog.isTitleTranslated ? $t(dialog.dialog.localTitle) :
dialog.dialog.localTitle }}</span>
</button>
<span v-if="openDialogs.length === 0" class="window-bar__empty">System bereit</span>
<span v-if="openDialogs.length === 0" class="window-bar__empty">{{ $t('appShell.footer.systemReady') }}</span>
</div>
<div class="static-block">
@@ -72,7 +74,7 @@ export default {
},
// Daemon WebSocket deaktiviert - diese Funktionen sind nicht mehr verfügbar
async showFalukantDaemonStatus() {
showInfo(this, 'Der Systemstatus ist in dieser Ansicht derzeit nicht direkt verfügbar.');
showInfo(this, this.$t('appShell.footer.systemStatusUnavailable'));
},
handleDaemonMessage() {
// Status-Events werden hier bewusst nicht verarbeitet.

View File

@@ -5,21 +5,34 @@
<div class="logo"><img src="/images/logos/logo.png" alt="YourPart" /></div>
<div class="brand-copy">
<strong>YourPart</strong>
<span>Community-Plattform</span>
<span>{{ $t('appShell.header.tagline') }}</span>
</div>
</div>
<div class="header-meta">
<div class="header-meta__context">
<span class="header-pill">Beta</span>
<span class="header-pill">{{ $t('appShell.header.beta') }}</span>
<label class="header-lang">
<span class="header-lang__label">{{ $t('appShell.header.language') }}</span>
<select
class="header-lang__select"
:aria-label="$t('appShell.header.language')"
:value="language"
@change="onUiLanguageChange($event.target.value)"
>
<option v-for="opt in uiLocaleOptions" :key="opt.value" :value="opt.value">
{{ $t(opt.labelTr) }}
</option>
</select>
</label>
</div>
<div class="connection-status" v-if="isLoggedIn">
<div class="status-indicator" :class="backendStatusClass">
<span class="status-dot"></span>
<span class="status-text">Backend</span>
<span class="status-text">{{ $t('appShell.header.backend') }}</span>
</div>
<div class="status-indicator" :class="daemonStatusClass">
<span class="status-dot"></span>
<span class="status-text">Daemon</span>
<span class="status-text">{{ $t('appShell.header.daemon') }}</span>
</div>
</div>
</div>
@@ -29,11 +42,22 @@
<script>
import { mapGetters } from 'vuex';
import apiClient from '@/utils/axios.js';
export default {
name: 'AppHeader',
data() {
return {
uiLocaleOptions: [
{ value: 'de', labelTr: 'settings.personal.language.de' },
{ value: 'en', labelTr: 'settings.personal.language.en' },
{ value: 'ceb', labelTr: 'settings.personal.language.ceb' },
{ value: 'es', labelTr: 'settings.personal.language.es' },
],
};
},
computed: {
...mapGetters(['isLoggedIn', 'connectionStatus', 'daemonConnectionStatus']),
...mapGetters(['isLoggedIn', 'user', 'language', 'connectionStatus', 'daemonConnectionStatus']),
backendStatusClass() {
return {
'status-connected': this.connectionStatus === 'connected',
@@ -50,7 +74,40 @@ export default {
'status-error': this.daemonConnectionStatus === 'error'
};
}
}
},
methods: {
async onUiLanguageChange(code) {
const supported = ['de', 'en', 'ceb', 'es'];
if (!supported.includes(code)) {
return;
}
await this.$store.dispatch('setLanguage', code);
if (!this.isLoggedIn || !this.user?.id) {
return;
}
try {
const { data } = await apiClient.post('/api/settings/filter', {
userid: this.user.id,
type: 'personal',
});
const langRow = data.find((s) => s.name === 'language');
if (!langRow?.options?.length) {
return;
}
const opt = langRow.options.find((o) => o.value === code);
if (!opt) {
return;
}
await apiClient.post('/api/settings/update', {
userid: this.user.id,
settingId: langRow.id,
value: opt.id,
});
} catch (err) {
console.warn('AppHeader: profile language could not be synced', err);
}
},
},
};
</script>
@@ -144,6 +201,38 @@ export default {
color: #8a5411;
}
.header-lang {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
font-size: 0.72rem;
color: rgba(95, 75, 57, 0.85);
}
.header-lang__label {
font-weight: 600;
white-space: nowrap;
}
.header-lang__select {
min-width: 7.5rem;
max-width: 11rem;
padding: 4px 8px;
border-radius: 8px;
border: 1px solid rgba(93, 64, 55, 0.18);
background: rgba(255, 255, 255, 0.85);
color: #3a2a1b;
font-size: 0.72rem;
font-weight: 600;
cursor: pointer;
}
.header-lang__select:focus {
outline: 2px solid rgba(248, 162, 43, 0.45);
outline-offset: 1px;
}
.connection-status {
display: flex;
align-items: center;

View File

@@ -5,12 +5,12 @@
:data-widget-id="widgetId"
>
<header class="dashboard-widget__titlebar">
<span class="dashboard-widget__drag-handle" title="Verschieben" draggable="true" @dragstart="onDragStart" @dragend="onDragEnd"></span>
<span class="dashboard-widget__drag-handle" :title="$t('widgets.dashboard.dragHandle')" draggable="true" @dragstart="onDragStart" @dragend="onDragEnd"></span>
<span class="dashboard-widget__title">{{ title }}</span>
<slot name="title-actions"></slot>
</header>
<div class="dashboard-widget__frame">
<div v-if="loading" class="dashboard-widget__state">Laden</div>
<div v-if="loading" class="dashboard-widget__state">{{ $t('widgets.dashboard.loading') }}</div>
<div v-else-if="error" class="dashboard-widget__state dashboard-widget__error">{{ error }}</div>
<div v-else class="dashboard-widget__body">
<component :is="widgetComponent" :data="data" />

View File

@@ -9,7 +9,7 @@
{{ message }}
</div>
<div class="dialog-footer">
<button @click="close()" class="dialog-button">Ok</button>
<button @click="close()" class="dialog-button">{{ $t('general.ok') }}</button>
</div>
</div>
</div>

View File

@@ -103,10 +103,6 @@ export default {
type: String,
required: true
}
},
data: {
settings: [],
possibleVisibilities: [],
},
computed: {
...mapGetters(['user']),
@@ -170,6 +166,14 @@ export default {
settingId: settingId,
value: value
});
if (setting?.name === 'language' && Array.isArray(setting.options)) {
const opt = setting.options.find((o) => String(o.id) === String(value));
const code = opt?.value;
const supported = ['de', 'en', 'ceb', 'es'];
if (code && supported.includes(code)) {
this.$store.dispatch('setLanguage', code);
}
}
this.fetchSettings();
} catch (err) {
console.error('Error updating setting:', err);
@@ -238,6 +242,7 @@ export default {
data() {
return {
settings: [],
possibleVisibilities: [],
userEmail: "",
userUsername: "",
};

View File

@@ -1,10 +1,10 @@
<template>
<div class="termine-widget">
<h2>📅 Termine</h2>
<div v-if="loading" class="loading">Lade Termine...</div>
<h2>{{ $t('widgets.appointments.title') }}</h2>
<div v-if="loading" class="loading">{{ $t('widgets.appointments.loading') }}</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else-if="termine.length === 0" class="no-termine">
Keine bevorstehenden Termine
{{ $t('widgets.appointments.empty') }}
</div>
<div v-else class="termine-list">
<div v-for="termin in termine" :key="termin.datum + termin.titel" class="termin-item">
@@ -50,7 +50,7 @@ export default {
this.termine = response.data;
} catch (error) {
console.error('Error loading termine:', error);
this.error = 'Termine konnten nicht geladen werden';
this.error = this.$t('widgets.appointments.loadError');
} finally {
this.loading = false;
}
@@ -64,7 +64,16 @@ export default {
month: 'long',
day: 'numeric'
};
return date.toLocaleDateString('de-DE', options);
return date.toLocaleDateString(this.getDateLocale(), options);
},
getDateLocale() {
const locale = this.$i18n?.locale;
return {
de: 'de-DE',
en: 'en-GB',
es: 'es-ES',
ceb: 'fil-PH'
}[locale] || 'de-DE';
}
}
};

View File

@@ -10,7 +10,7 @@
<th>{{ $t('falukant.branch.sale.quality') }}</th>
<th>{{ $t('falukant.branch.sale.quantity') }}</th>
<th>{{ $t('falukant.branch.sale.sell') }}</th>
<th>Bessere Preise</th>
<th>{{ $t('falukant.branch.revenue.betterPrices') }}</th>
</tr>
</thead>
<tbody>

View File

@@ -165,7 +165,7 @@
});
} catch (err) {
console.error(err);
showError(this, 'Fehler beim Kaufen eines Teils der Lagerkapazität.');
showError(this, this.$t('falukant.branch.storage.buyPartialError'));
}
remainingAmount -= toBuy;
}
@@ -186,7 +186,7 @@
.then(() => this.loadStorageData())
.catch(err => {
console.error(err);
showError(this, 'Fehler beim Verkaufen der Lagerkapazität.');
showError(this, this.$t('falukant.branch.storage.sellError'));
});
},
getCostOfType(labelTr) {

View File

@@ -9,19 +9,19 @@
<div class="birthday-info">
<div class="birthday-name">{{ birthday.username }}</div>
<div class="birthday-date">
<span v-if="birthday.daysUntil === 0" class="birthday-highlight">Heute!</span>
<span v-else-if="birthday.daysUntil === 1">Morgen</span>
<span v-if="birthday.daysUntil === 0" class="birthday-highlight">{{ $t('widgets.birthdays.today') }}</span>
<span v-else-if="birthday.daysUntil === 1">{{ $t('widgets.birthdays.tomorrow') }}</span>
<span v-else>{{ formatDate(birthday.nextDate) }}</span>
<span class="birthday-age">(wird {{ birthday.turningAge }})</span>
<span class="birthday-age">{{ $t('widgets.birthdays.turningAge', { age: birthday.turningAge }) }}</span>
</div>
</div>
<div v-if="birthday.daysUntil > 1" class="birthday-days">
{{ birthday.daysUntil }} Tage
{{ $t('widgets.birthdays.inDays', { count: birthday.daysUntil }) }}
</div>
</div>
</div>
<div v-else class="birthday-empty">
Keine Geburtstage von Freunden sichtbar
{{ $t('widgets.birthdays.empty') }}
</div>
</template>
@@ -37,10 +37,19 @@ export default {
}
},
methods: {
getDateLocale() {
const locale = this.$i18n?.locale;
return {
de: 'de-DE',
en: 'en-GB',
es: 'es-ES',
ceb: 'fil-PH'
}[locale] || 'de-DE';
},
formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
return d.toLocaleDateString('de-DE', {
return d.toLocaleDateString(this.getDateLocale(), {
day: 'numeric',
month: 'short'
});

View File

@@ -21,7 +21,7 @@
</dd>
</template>
</dl>
<span v-else></span>
<span v-else>{{ $t('widgets.falukant.emptyValue') }}</span>
</template>
<script>
@@ -62,7 +62,7 @@ export default {
},
falukantDisplayName() {
const d = this.falukantData;
if (!d) return '—';
if (!d) return this.$t('widgets.falukant.emptyValue');
const titleKey = d.titleLabelTr;
const gender = d.gender;
const nameWithoutTitle = d.nameWithoutTitle ?? d.characterName;
@@ -71,11 +71,11 @@ export default {
const translatedTitle = this.$t(key);
if (translatedTitle !== key) return `${translatedTitle} ${nameWithoutTitle}`.trim();
}
return d.characterName || nameWithoutTitle || '—';
return d.characterName || nameWithoutTitle || this.$t('widgets.falukant.emptyValue');
},
falukantGenderLabel() {
const g = this.falukantData?.gender;
if (g == null || g === '') return '—';
if (g == null || g === '') return this.$t('widgets.falukant.emptyValue');
// Altersabhängige, (auf Wunsch) altertümlichere Bezeichnungen
const years = this._ageYearsFromWidgetValue(this.falukantData?.age);
@@ -93,9 +93,9 @@ export default {
},
falukantAgeLabel() {
const ageValue = this.falukantData?.age;
if (ageValue == null) return '—';
if (ageValue == null) return this.$t('widgets.falukant.emptyValue');
const years = this._ageYearsFromWidgetValue(ageValue);
if (years == null) return '—';
if (years == null) return this.$t('widgets.falukant.emptyValue');
return `${years} ${this.$t('falukant.overview.metadata.years')}`;
}
},
@@ -145,8 +145,17 @@ export default {
},
formatMoney(value) {
const n = Number(value);
if (Number.isNaN(n)) return '—';
return n.toLocaleString('de-DE');
if (Number.isNaN(n)) return this.$t('widgets.falukant.emptyValue');
return n.toLocaleString(this.getNumberLocale());
},
getNumberLocale() {
const locale = this.$i18n?.locale;
return {
de: 'de-DE',
en: 'en-GB',
es: 'es-ES',
ceb: 'fil-PH'
}[locale] || 'de-DE';
}
}
};

View File

@@ -30,21 +30,32 @@ export default {
fallbackText() {
if (this.data == null) return '';
if (Array.isArray(this.data)) {
return this.data.length === 0 ? 'Keine Einträge' : `(${this.data.length} Einträge)`;
return this.data.length === 0
? this.$t('widgets.list.noEntries')
: this.$t('widgets.list.entriesCount', { count: this.data.length });
}
if (typeof this.data === 'object') {
const keys = Object.keys(this.data);
return keys.length === 0 ? '—' : `(${keys.length} Felder)`;
return keys.length === 0 ? '—' : this.$t('widgets.list.fieldsCount', { count: keys.length });
}
return String(this.data);
}
},
methods: {
getDateLocale() {
const locale = this.$i18n?.locale;
return {
de: 'de-DE',
en: 'en-GB',
es: 'es-ES',
ceb: 'fil-PH'
}[locale] || 'de-DE';
},
formatDatum(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
if (Number.isNaN(d.getTime())) return String(dateStr);
return d.toLocaleDateString('de-DE', {
return d.toLocaleDateString(this.getDateLocale(), {
weekday: 'short',
day: 'numeric',
month: 'short',

View File

@@ -7,13 +7,13 @@
rel="noopener noreferrer"
class="dashboard-widget__news-title"
>
{{ article.title || '—' }}
{{ article.title || $t('widgets.news.emptyValue') }}
</a>
<span v-else class="dashboard-widget__title-text">{{ article.title || '—' }}</span>
<span v-else class="dashboard-widget__title-text">{{ article.title || $t('widgets.news.emptyValue') }}</span>
<span v-if="article.pubDate" class="dashboard-widget__date">{{ formatNewsDate(article.pubDate) }}</span>
<p v-if="article.description" class="dashboard-widget__desc">{{ article.description }}</p>
</article>
<span v-else></span>
<span v-else>{{ $t('widgets.news.emptyValue') }}</span>
</template>
<script>
@@ -32,11 +32,20 @@ export default {
}
},
methods: {
getDateLocale() {
const locale = this.$i18n?.locale;
return {
de: 'de-DE',
en: 'en-GB',
es: 'es-ES',
ceb: 'fil-PH'
}[locale] || 'de-DE';
},
formatNewsDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
if (Number.isNaN(d.getTime())) return String(dateStr);
return d.toLocaleDateString('de-DE', {
return d.toLocaleDateString(this.getDateLocale(), {
day: 'numeric',
month: 'short',
year: 'numeric',

View File

@@ -14,16 +14,16 @@
<div class="upcoming-date">
{{ formatDate(event.datum) }}
<span v-if="event.startTime && !event.allDay" class="upcoming-time">
{{ event.startTime }} Uhr
{{ $t('widgets.upcoming.timeAt', { time: event.startTime }) }}
</span>
<span v-if="event.allDay" class="upcoming-allday">Ganztägig</span>
<span v-if="event.allDay" class="upcoming-allday">{{ $t('widgets.upcoming.allDay') }}</span>
</div>
<div v-if="event.beschreibung" class="upcoming-desc">{{ event.beschreibung }}</div>
</div>
</div>
</div>
<div v-else class="upcoming-empty">
Keine anstehenden Termine
{{ $t('widgets.upcoming.empty') }}
</div>
</template>
@@ -50,6 +50,15 @@ export default {
}
},
methods: {
getDateLocale() {
const locale = this.$i18n?.locale;
return {
de: 'de-DE',
en: 'en-GB',
es: 'es-ES',
ceb: 'fil-PH'
}[locale] || 'de-DE';
},
formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
@@ -58,13 +67,13 @@ export default {
tomorrow.setDate(tomorrow.getDate() + 1);
if (d.toDateString() === today.toDateString()) {
return 'Heute';
return this.$t('widgets.upcoming.today');
}
if (d.toDateString() === tomorrow.toDateString()) {
return 'Morgen';
return this.$t('widgets.upcoming.tomorrow');
}
return d.toLocaleDateString('de-DE', {
return d.toLocaleDateString(this.getDateLocale(), {
weekday: 'short',
day: 'numeric',
month: 'short'

View File

@@ -1,6 +1,6 @@
<template>
<DialogWidget ref="dialog" :title="$t('socialnetwork.profile.pretitle')" :isTitleTranslated="isTitleTranslated"
:show-close="true" :buttons="[{ text: 'Ok', action: 'close' }]" :modal="false" @close="closeDialog" height="75%"
:show-close="true" :buttons="profileDialogButtons" :modal="false" @close="closeDialog" height="75%"
name="UserProfileDialog" display="flex">
<div class="activities">
<span>{{ $t(`socialnetwork.friendship.state.${friendshipState}`) }}</span>
@@ -11,7 +11,7 @@
<div class="profile-content">
<div>
<ul class="tab-list">
<li v-for="tab in tabs" :key="tab.name" :class="{ active: activeTab === tab.name }"
<li v-for="tab in profileTabs" :key="tab.name" :class="{ active: activeTab === tab.name }"
@click="selectTab(tab.name)">
{{ tab.label }}
</li>
@@ -42,7 +42,7 @@
</ul>
<ul v-if="images.length > 0" class="image-list">
<li v-for="image in images" :key="image.id" @click="openImageDialog(image)">
<img :src="image.url || image.placeholder" alt="Loading..." />
<img :src="image.url || image.placeholder" :alt="$t('socialnetwork.gallery.imageLoadingAlt')" />
<p>{{ image.title }}</p>
</li>
</ul>
@@ -60,7 +60,7 @@
}}</label>
<input type="file" @change="onFileChange" accept="image/*" />
<div v-if="imagePreview" class="image-preview">
<img :src="imagePreview" alt="Image Preview"
<img :src="imagePreview" :alt="$t('socialnetwork.gallery.imagePreviewAlt')"
style="max-width: 100px; max-height: 100px;" />
</div>
<EditorContent :editor="editor" class="editor" />
@@ -73,7 +73,7 @@
</div>
<div v-else class="guestbook-entries">
<div v-for="entry in guestbookEntries" :key="entry.id" class="guestbook-entry">
<img v-if="entry.image" :src="entry.image.url" alt="Entry Image"
<img v-if="entry.image" :src="entry.image.url" :alt="$t('socialnetwork.profile.guestbook.entryImageAlt')"
style="max-width: 400px; max-height: 400px;" />
<p v-html="sanitizedContent(entry)"></p>
<div class="entry-info">
@@ -117,13 +117,23 @@ export default {
},
computed: {
...mapGetters(['user']),
profileDialogButtons() {
return [{ text: this.$t('general.ok'), action: 'close' }];
},
canOpenEroticPictures() {
return Boolean(
this.userProfile?.username &&
this.user?.username &&
this.userProfile.username !== this.user.username
);
}
},
profileTabs() {
return [
{ name: 'general', label: this.$t('socialnetwork.profile.tab.general') },
{ name: 'images', label: this.$t('socialnetwork.profile.tab.images') },
{ name: 'guestbook', label: this.$t('socialnetwork.profile.tab.guestbook') },
];
},
},
data() {
return {
@@ -141,11 +151,6 @@ export default {
selectedImage: null,
currentPage: 1,
totalPages: 1,
tabs: [
{ name: 'general', label: this.$t('socialnetwork.profile.tab.general') },
{ name: 'images', label: this.$t('socialnetwork.profile.tab.images') },
{ name: 'guestbook', label: this.$t('socialnetwork.profile.tab.guestbook') }
],
apiKey: import.meta.env.VITE_TINYMCE_API_KEY,
editor: null,
hasSendFriendshipRequest: false,

View File

@@ -28,6 +28,7 @@ import cebActivate from './locales/ceb/activate.json';
import cebError from './locales/ceb/error.json';
import cebMessage from './locales/ceb/message.json';
import cebSettings from './locales/ceb/settings.json';
import cebAdmin from './locales/ceb/admin.json';
import cebPasswordReset from './locales/ceb/passwordReset.json';
import cebSocialNetwork from './locales/ceb/socialnetwork.json';
import cebFriends from './locales/ceb/friends.json';
@@ -124,6 +125,7 @@ const messages = {
...cebError,
...cebMessage,
...cebSettings,
...cebAdmin,
...cebPasswordReset,
...cebSocialNetwork,
...cebFriends,
@@ -178,7 +180,10 @@ const messages = {
const i18n = createI18n({
locale: store.state.language,
fallbackLocale: 'de',
fallbackLocale: {
ceb: ['en', 'de'],
default: ['de']
},
messages
});

View File

@@ -0,0 +1,41 @@
{
"admin": {
"interests": {
"title": "[Admin] - Pagdumala sa mga interes"
},
"contacts": {
"title": "[Admin] - Mga hangyo sa pakigkontak",
"date": "Petsa",
"from": "Gikan kang",
"actions": "Mga aksyon",
"open": "Ablihi",
"finished": "Tapusa"
},
"editcontactrequest": {
"title": "[Admin] - Usba ang hangyo sa pakigkontak"
},
"user": {
"name": "Ngalan sa user",
"active": "Aktibo",
"blocked": "Gi-block",
"actions": "Mga aksyon",
"search": "Pangita"
},
"rights": {
"add": "Idugang ang katungod",
"select": "Palihog pagpili",
"current": "Karon nga mga katungod"
},
"forum": {
"title": "[Admin] - Forum",
"currentForums": "Anaa nang mga forum",
"edit": "Usba",
"delete": "Tangtanga",
"createForum": "Paghimo",
"forumName": "Titulo",
"create": "Paghimo",
"selectPermissions": "Palihog pagpili",
"confirmDeleteTitle": "Tangtanga ang forum"
}
}
}

View File

@@ -306,6 +306,10 @@
"sale": {
"runningGuards": "Mga guwardiya"
},
"storage": {
"buyPartialError": "Sayop sa pagpalit sa usa ka bahin sa kapasidad sa storage.",
"sellError": "Sayop sa pagbaligya sa kapasidad sa storage."
},
"production": {
"title": "Produksyon",
"info": "Mga detalye bahin sa produksyon sa branch.",

View File

@@ -1,6 +1,12 @@
{
"friends": {
"kicker": "Komunidad",
"intro": "Mga panaghigala, bukas nga mga hangyo ug nagpadayon nga mga kontak sa usa ka lugar.",
"title": "Mga higala",
"stats": {
"existing": "Naa na",
"open": "Abli"
},
"tabs": {
"existing": "Naa na",
"rejected": "Gibalibaran",

View File

@@ -1,16 +1,16 @@
{
"welcome": "Maayong pag-abot sa YourPart",
"imprint": {
"title": "Imprint",
"button": "Imprint"
"title": "Legal nga pahibalo",
"button": "Legal nga pahibalo"
},
"dataPrivacy": {
"title": "Patakaran sa pagpanalipod sa datos",
"button": "Pagpanalipod sa datos"
},
"contact": {
"title": "Kontak",
"button": "Kontak"
"title": "Pakigkontak",
"button": "Pakigkontak"
},
"error-title": "Sayop",
"warning-title": "Pasidaan",
@@ -30,6 +30,59 @@
"message": {
"close": "Isira"
},
"appShell": {
"header": {
"tagline": "Plataporma sa komunidad",
"beta": "Beta",
"backend": "Backend",
"daemon": "Daemon",
"language": "Pinulongan"
},
"footer": {
"systemLabel": "Sistema",
"noOpenDialogs": "Walay abli nga mga dialog",
"activeWindows": "{count} ka bintana ang aktibo",
"systemReady": "Andam ang sistema",
"systemStatusUnavailable": "Ang status sa sistema dili diretso makita niining view karon."
}
},
"widgets": {
"dashboard": {
"dragHandle": "Balhinon",
"loading": "Nag-load..."
},
"birthdays": {
"today": "Karon!",
"tomorrow": "Ugma",
"turningAge": "(mag-{age})",
"inDays": "{count} ka adlaw",
"empty": "Walay makita nga kaadlawon sa mga higala"
},
"upcoming": {
"today": "Karon",
"tomorrow": "Ugma",
"timeAt": "{time}",
"allDay": "Tibuok adlaw",
"empty": "Walay umaabot nga mga appointment"
},
"appointments": {
"title": "📅 Mga appointment",
"loading": "Nag-load sa mga appointment...",
"empty": "Walay umaabot nga mga appointment",
"loadError": "Dili ma-load ang mga appointment"
},
"list": {
"noEntries": "Walay mga entry",
"entriesCount": "({count} ka entry)",
"fieldsCount": "({count} ka field)"
},
"news": {
"emptyValue": "—"
},
"falukant": {
"emptyValue": "—"
}
},
"gender": {
"male": "Lalaki",
"female": "Babaye",
@@ -63,7 +116,7 @@
},
"titles": {
"friends": "Mga higala",
"guestbook": "Guestbook",
"guestbook": "Libro sa bisita",
"search": "Pagpangita",
"gallery": "Galeriya",
"forum": "Forum",
@@ -97,7 +150,7 @@
"sexualitySettings": "Sekswalidad",
"flirtSettings": "Flirt",
"accountSettings": "Account",
"languageAssistantSettings": "Language assistant",
"languageAssistantSettings": "Katabang sa pinulongan",
"interests": "Mga interes",
"adminInterests": "Pagdumala sa interes",
"adminUsers": "Mga user",

View File

@@ -1,5 +1,54 @@
{
"home": {
"dashboard": {
"kicker": "Imong lugar",
"title": "Maayong pagbalik!",
"subtitle": "Imong personal nga agi-anan padulong sa komunidad, mga appointment, Falukant, ug mga nagpadayon nga kalihokan.",
"edit": "Usba ang dashboard",
"addWidget": "+ Idugang ang widget ...",
"addAgain": "Idugang pag-usab",
"done": "Human",
"sectionTitle": "Imong kinatibuk-ang tan-aw",
"sectionIntro": "Mahimong ibalhin ang mga widget ug usbon sa edit mode.",
"widgetTitlePlaceholder": "Titulo",
"removeWidget": "Tangtanga ang widget",
"remove": "Tangtanga",
"empty": "Wala pay mga widget. I-klik ang “Usba ang dashboard” ug dayon ang “+ Idugang ang widget”.",
"defaultAppointmentsWidget": "Mga appointment",
"loadError": "Dili ma-load ang dashboard.",
"saveError": "Dili masave ang dashboard.",
"widgetLabels": {
"appointments": "Mga appointment",
"falukant": "Falukant",
"news": "Balita",
"birthdays": "Mga adlawng natawhan",
"upcoming": "Umaabot nga mga appointment",
"calendar": "Kalendaryo"
},
"overview": {
"activeWidgetsLabel": "Aktibong mga widget",
"activeWidgetsText": "Modular ang imong dashboard ug mahimo kining usbon ang han-ay bisan kanus-a.",
"availableModulesLabel": "Magamit nga mga module",
"availableModulesText": "Mahimo nimong isagol ang community, kalendaryo, balita, ug Falukant nga mga module.",
"editModeLabel": "Edit mode",
"editModeActive": "Aktibo",
"editModeInactive": "Patay",
"editModeActiveText": "Mahimong dugangan ug usbon karon ang mga widget.",
"editModeInactiveText": "Nagpabiling pokus ug sayon basahon ang sulod."
}
},
"vocabLanding": {
"eyebrow": "Pagkat-on og pinulongan online",
"title": "Ang trainer sa bokabularyo sa YourPart naghiusa sa pagkat-on, mga kurso ug praktis sa usa ka plataporma.",
"lead": "Magtrabaho og interaktibong mga leksiyon, palapdan ang imong bokabularyo ug gamita ang organisado nga sulod para sa makadasig nga agos sa pagkat-on diretso sa browser.",
"cta": "Sugdi nga libre",
"feature1Title": "Interaktibong mga kurso",
"feature1Text": "Ang mga kurso, leksiyon ug ehersisyo makatabang sa sistematikong pagtukod og bag-ong kahanas sa pinulongan.",
"feature2Title": "Nagtutok sa praktis",
"feature2Text": "Ang bokabularyo, gramatika ug balik-balik gituyo para sa adlaw-adlaw nga rutina sa pagkat-on.",
"feature3Title": "Parte sa komunidad",
"feature3Text": "Ang pinulongan nga lugar nahimutang sa mas dako nga plataporma sa komunidad nga adunay blog, forum ug chat."
},
"betaNoticeLabel": "Pahibalo sa beta:",
"betaNoticeText": "Ang YourPart padayon pang gihimo. Adunay mga feature nga wala pa mahuman, adunay impormasyon nga kulang pa, ug posible pa nga adunay mga kausaban.",
"nologin": {

View File

@@ -16,7 +16,7 @@
"eroticChat": "Erotik chat"
},
"m-socialnetwork": {
"guestbook": "Guestbook",
"guestbook": "Libro sa bisita",
"blog": "Blog",
"usersearch": "Pagpangita og user",
"forum": "Forum",
@@ -62,7 +62,7 @@
"interests": "Mga interes",
"notifications": "Mga pahibalo",
"sexuality": "Sekswalidad",
"languageAssistant": "Language assistant"
"languageAssistant": "Katabang sa pinulongan"
},
"m-administration": {
"contactrequests": "Mga hangyo sa kontak",

View File

@@ -2,6 +2,8 @@
"personal": {
"calendar": {
"title": "Kalendaryo",
"kicker": "Pagplano",
"intro": "Mga appointment, mga adlawng natawhan ug imong kaugalingong mga entry sa usa ka organisado nga tan-aw.",
"today": "Karon",
"newEntry": "Bag-ong entry",
"editEntry": "Usba ang entry",

View File

@@ -2,7 +2,7 @@
"register": {
"title": "Pagrehistro sa yourPart",
"email": "E-mail address",
"username": "Username",
"username": "Ngalan sa user",
"password": "Password",
"repeatPassword": "Usba ang password",
"language": "Pinulongan",
@@ -20,7 +20,7 @@
"usernameinuse": "Dili magamit ang username.",
"validation": {
"invalidEmail": "Palihog isulod ang sakto nga email address.",
"usernameTooShort": "Ang username kinahanglan adunay labing menos 3 ka karakter.",
"usernameTooShort": "Ang ngalan sa user kinahanglan adunay labing menos 3 ka karakter.",
"passwordHint": "Kinahanglan labing menos 8 ka karakter.",
"passwordTooShort": "Mubo ra ang password."
}

View File

@@ -11,7 +11,8 @@
"language": {
"de": "Aleman",
"en": "Iningles",
"ceb": "Bisaya"
"ceb": "Bisaya",
"es": "Espanyol"
}
},
"view": {
@@ -22,14 +23,14 @@
},
"account": {
"title": "Account",
"heroEyebrow": "Settings",
"heroIntro": "Atimana ang username, email, password ug visibility sa usa ka lugar.",
"heroEyebrow": "Mga setting",
"heroIntro": "Atimana ang ngalan sa user, email, password ug visibility sa usa ka lugar.",
"language": "Pinulongan",
"username": "Username",
"username": "Ngalan sa user",
"email": "E-mail address",
"newpassword": "Password",
"newpasswordretype": "Usba ang password",
"showinsearch": "Ipakita sa user search",
"showinsearch": "Ipakita sa pagpangita sa user",
"changeaction": "Usba ang datos sa user",
"oldpassword": "Karaan nga password (gikinahanglan)",
"validation": {

View File

@@ -1,11 +1,16 @@
{
"socialnetwork": {
"usersearch": {
"kicker": "Pagpangita sa komunidad",
"intro": "Pangitaa ang angay nga mga kontak sa komunidad pinaagi sa ngalan, edad ug sekso.",
"ageSeparator": "hangtod",
"resultsCount": "{count} ka resulta",
"openProfile": "Ablihi ang profile",
"title": "Pagpangita og user",
"username": "Username",
"username": "Ngalan sa user",
"age_from": "Edad gikan sa",
"age_to": "hangtod",
"gender": "Gender",
"gender": "Sekso",
"search_button": "Pangita",
"no_results": "Walay nakit-an nga resulta",
"results_title": "Mga resulta sa pagpangita:",
@@ -22,8 +27,8 @@
"tab": {
"general": "Kinatibuk-an",
"sexuality": "Sekswalidad",
"images": "Gallery",
"guestbook": "Guestbook"
"images": "Galeriya",
"guestbook": "Libro sa bisita"
},
"values": {
"bool": {
@@ -71,7 +76,8 @@
"hideInput": "Tagoa ang bag-ong entry",
"imageUpload": "Hulagway",
"submit": "Ipadala ang entry",
"noEntries": "Walay entry nga nakit-an"
"noEntries": "Walay entry nga nakit-an",
"entryImageAlt": "Hulagway sa entry sa libro sa bisita"
},
"interestedInGender": "Interesado sa",
"hasChildren": "Naay mga anak",
@@ -80,7 +86,7 @@
"willChildren": "Gusto og anak",
"sexualpreference": "Sekswal nga oryentasyon",
"language": "Pinulongan",
"gender": "Gender",
"gender": "Sekso",
"birthdate": "Petsa sa pagkatawo",
"age": "Edad",
"town": "Lungsod",
@@ -88,7 +94,9 @@
"weight": "Timbang"
},
"gallery": {
"title": "Gallery",
"kicker": "Mga hulagway ug folder",
"intro": "Organisaha ang imong kaugalingong sulod, kontrola ang makita, ug ihan-ay sa mga folder.",
"title": "Galeriya",
"folders": "Mga folder",
"create_folder": "Paghimo og folder",
"upload": {
@@ -130,19 +138,26 @@
},
"show_image_dialog": {
"title": "Hulagway"
}
},
"imagePreviewAlt": "Preview sa hulagway",
"imageLoadingAlt": "Nag-load ang hulagway"
},
"guestbook": {
"title": "Guestbook",
"kicker": "Libro sa bisita",
"intro": "Mga mensahe, feedback ug gagmayng hulagway sa imong network.",
"title": "Libro sa bisita",
"prevPage": "Balik",
"nextPage": "Sunod",
"page": "Panid"
},
"diary": {
"title": "Diary",
"noEntries": "Wala ka pay nahimong diary entries.",
"newEntry": "Bag-ong diary entry",
"editEntry": "Usba ang diary entry",
"kicker": "Personal nga mga entry",
"intro": "Mga hunahuna, nota ug mubo nga update sa usa ka malinawon ug personal nga tan-aw.",
"placeholder": "Isulat dinhi ang imong entry sa talaarawan...",
"title": "Talaarawan",
"noEntries": "Wala ka pay mga entry sa talaarawan.",
"newEntry": "Bag-ong entry sa talaarawan",
"editEntry": "Usba ang entry sa talaarawan",
"save": "I-save",
"update": "I-update",
"cancel": "I-cancel",
@@ -154,12 +169,22 @@
"page": "Panid"
},
"forum": {
"kicker": "Forum sa komunidad",
"intro": "Mga hilisgutan, diskusyon ug bag-ong post sa usa ka organisado nga lugar.",
"createTitle": "Paghimo og bag-ong hilisgutan",
"createIntro": "Ibutang una ang titulo, dayon isulat ang post ug dayon i-publish dayon.",
"cancelCreation": "Kanselahon",
"creationHint": "Kinahanglan mapun-an ang titulo ug sulod.",
"communityFallback": "Komunidad",
"topicIntro": "Mga diskusyon, tubag ug bag-ong post sa usa ka mas nakapokus nga pagbasa.",
"topicCreated": "Malampuson nga nahimo ang hilisgutan.",
"topicCreateError": "Adunay sayop sa paghimo sa hilisgutan",
"title": "Forum",
"showNewTopic": "Paghimo og bag-ong topic",
"hideNewTopic": "I-cancel ang paghimo",
"noTitles": "Walay topic nga available",
"topic": "Topic",
"createNewTopic": "Paghimo og topic",
"topic": "Hilisgutan",
"createNewTopic": "Paghimo og hilisgutan",
"createdBy": "Gihimo ni",
"createdAt": "Gihimo sa",
"reactions": "Reaksiyon",
@@ -190,7 +215,174 @@
"lockedShort": "Magamit ra kini nga area human sa moderator approval.",
"requestVerification": "Mangayo og access",
"requestSent": "Napadala na ang access request.",
"requestError": "Wala mapadala ang access request."
"requestError": "Wala mapadala ang access request.",
"picturesTitle": "Erotic nga mga hulagway",
"picturesIntro": "Ang imong mga sulod magpabiling bulag gikan sa kasagarang galeriya. Dinhi nimo dumalahon ang mga hulagway para sa naablihang erotic area.",
"uploadTitle": "Pag-upload og erotic nga hulagway",
"noimages": "Wala pay hulagway niining erotic nga folder.",
"videosTitle": "Erotic nga mga video",
"videosIntro": "Ang imong kaugalingong mga video dumalahon nga bulag sa normal nga social area. Dinhi nimo organisahon ang uploads, visibility ug moderation status sa usa ka lugar.",
"videoUploadTitle": "Pag-upload og erotic nga video",
"videoUploadHint": "Pag-upload dinhi og mga video para sa imong naablihang erotic area ug pun-a dayon ang titulo ug deskripsiyon.",
"videoDescription": "Deskripsiyon",
"videoFile": "File sa video",
"videoFormats": "MP4, WEBM, OGG, MOV",
"myVideos": "Akong mga video",
"sharedVideos": "Mga gi-share nga video",
"foreignVideosIntro": "Mga gi-share nga video gikan sa adult area.",
"foreignVideosOnlyHint": "Dinhi makita lang nimo ang mga video nga gi-share para nimo sa adult area.",
"sharedVideosIntro": "Makita nga mga video gikan sa gi-share nga adult areas.",
"noSharedVideos": "Wala pay gi-share nga mga video para nimo karon.",
"libraryTitle": "Bibliyoteka",
"libraryIntro": "Ang imong uploads, visibility ug reports sa usa ka lugar.",
"libraryEmptyHint": "Paghimo una sa wala ang imong unang video ug dayon dumalaha kini dinhi sa bibliyoteka.",
"latestUpload": "Pinakabag-ong upload",
"visibleVideos": "Makita nga mga video",
"moderationCases": "Mga kaso sa moderation",
"notesTitle": "Mga pahimangno",
"friendsVisibilityHint": "Makakita ra ang mga higala sa sulod kung hamtong sila ug naablihan para sa adult area.",
"selectedUsersVisibilityHint": "Ang mga tawo nga espesipikong gi-share-an kinahanglan usab nga hamtong ug naablihan.",
"selectedUsersPlaceholder": "anna, bert, clara",
"imagePreviewAlt": "Preview sa hulagway",
"imageLoadingAlt": "Nag-load ang hulagway",
"untitled": "Walay titulo",
"noUploadYet": "Wala pay upload",
"closeEditing": "Isira ang pag-edit",
"editVisibility": "Usba ang visibility",
"reportAction": "Ireport",
"reportHint": "Gamita ang {action} direkta sa maong entry kung ang sulod kinahanglan susihon.",
"reportNote": "Mubo nga nota para sa moderation",
"submitReport": "Ipadala ang report",
"reportSubmitted": "Natala na ang report.",
"reportError": "Dili masave ang report.",
"moderationHidden": "Gitago sa moderation",
"hiddenByModeration": "Kini nga sulod temporaryong gitago sa moderation.",
"reportReasons": {
"suspected_minor": "Suspetsang menor de edad",
"non_consensual": "Sulod nga walay pagtugot",
"violence": "Kapintas o abuso",
"harassment": "Paghasol o pagpugos",
"spam": "Spam o scam",
"other": "Uban pa"
}
},
"vocab": {
"heroEyebrow": "Pagtuon og pinulongan",
"summaryTotalLabel": "Tanan nga pinulongan",
"summaryTotalIntro": "Tanang aktibong pinulongan nga lugar diin naggamit o nagdumala ka og sulod.",
"summaryOwnedLabel": "Imong kaugalingong lugar",
"summaryOwnedIntro": "Dinhi ka mismo maghimo og sulod, kapitulo ug materyal sa pagkat-on.",
"summarySubscribedLabel": "Gi-subscribe",
"summarySubscribedIntro": "Kini nga mga lugar mas para sa pagkat-on ug progreso kaysa pagdumala.",
"taskCreateEyebrow": "Paspas nga sugod",
"taskCreateTitle": "Paghimo og bag-ong pinulongan",
"taskCreateIntro": "Mao kini ang labing maayong sugod kung gusto nimo nga ikaw mismo ang mohikay ug moatiman sa sulod.",
"taskContinueEyebrow": "Padayon sa pagkat-on",
"taskContinueTitle": "Ablihi ang mga kurso ug kapitulo",
"taskContinueIntro": "Sulod dayon sa mga andam nang learning path ug ipadayon ang anaa nang mga kurso.",
"ownedSectionTitle": "Imong mga pinulongan",
"ownedSectionIntro": "Direktang agi-anan sa pag-edit, mga kapitulo ug pagdumala sa kurso.",
"ownedHint": "Pagdumala ug pag-atiman sa sulod",
"ownedEmpty": "Wala pay imong kaugalingong pinulongan nga lugar.",
"subscribedSectionTitle": "Mga gi-subscribe nga pinulongan",
"subscribedSectionIntro": "Maayo para sa paspas nga pagbalik sa pagkat-on nga walay kabug-at sa pagdumala.",
"subscribedHint": "Pagkat-on, praktis ug tan-awa ang progreso",
"subscribedEmpty": "Walay gi-subscribe nga pinulongan karon.",
"languageHeroEyebrow": "Pinulongan",
"languageHeroIntro": "Mga kapitulo, pagpangita ug pagpaambit para niining pinulongana sa usa ka lugar.",
"newLanguageHeroEyebrow": "Trainer sa bokabularyo",
"newLanguageHeroIntro": "Paghimo og bag-ong pinulongan, paghimo og share code ug dayon balhin dayon sa pag-edit.",
"newLanguageNameHint": "Sapat na ang mubo ug klaro nga ngalan sa pinulongan para sa pagsugod.",
"newLanguageNameValidation": "Ang ngalan kinahanglan adunay labing menos 2 ka karakter.",
"subscribeHeroEyebrow": "Trainer sa bokabularyo",
"chapterHeroEyebrow": "Trainer sa bokabularyo",
"chapterHeroIntro": "Susihon ang sulod sa kapitulo, atimana ang bokabularyo ug balhin dayon sa praktis.",
"courses": {
"courseKicker": "Kurso sa pagkat-on",
"courseListKicker": "Mga kurso",
"courseListIntro": "Sal-a ang publiko ug imong kaugalingong mga kurso sa pagkat-on, pangitaa ang angay, ug ipadayon dayon.",
"courseShareCodePlaceholder": "pananglitan abc123def456",
"courseFlowEyebrow": "Agos sa adlaw",
"courseFlowTitle": "Maayong ipadayon karon",
"courseFlowIntro": "Ang sunod-sunod mosunod sa konsepto: una ang angay nang balikon, dayon ang kasamtangang block, unya ang intensive phase ug sa katapusan ang libre nga pagpalalom.",
"courseFlowReviewStat": "Angay balikon: {count}",
"courseFlowBlockStat": "Aktibong block: {block}",
"courseFlowReviewTitle": "Angay nga balik-balikon",
"courseFlowReviewDescription": "Mga leksiyon nga nahuman na ug angay balikon karong adlawa.",
"courseFlowReviewEmpty": "Walay daang leksiyon nga nakamarka isip angay balikon karon.",
"courseFlowBlockTitle": "Kasamtangang block",
"courseFlowBlockDescription": "Dinhi nahimutang ang sunod regular nga lakang sa kurso.",
"courseFlowBlockEmpty": "Nahuman na ang kasamtangang block o walay abli nga leksiyon niini karon.",
"courseFlowIntensiveTitle": "Angay nga intensive phase",
"courseFlowIntensiveDescription": "Mas dikit nga balik-balik sa dihang lig-on na ang nag-una nga block.",
"courseFlowIntensiveEmpty": "Wala pay bag-ong intensive phase nga naablihan karon.",
"courseFlowPracticeTitle": "Libre nga pagpalalom",
"courseFlowPracticeDescription": "Nahuman nang mga leksiyon para sa hinay-hinay nga dugang praktis gawas sa obligadong agi-anan.",
"courseFlowPracticeEmpty": "Sa dihang makahuman ka sa unang mga leksiyon, makita sila dinhi para sa libre nga praktis.",
"practiceInTrainer": "Praktisa sa trainer",
"lessonsCount": "{count} ka leksiyon",
"lessonBlockLabel": "Block {number}",
"lessonIntensiveBadge": "Intensive nga balik-balik",
"addLessonValidation": "Palihog kompletoha ang numero, titulo ug kapitulo.",
"addLessonSuccess": "Malampuson nga nahimo ang leksiyon.",
"addLessonError": "Dili madugang ang leksiyon.",
"createCourseError": "Dili mahimo ang paghimo sa kurso.",
"deleteLessonTitle": "Tangtanga ang leksiyon",
"deleteLessonSuccess": "Malampuson nga natangtang ang leksiyon.",
"deleteLessonError": "Dili matangtang ang leksiyon.",
"enrollCourseError": "Dili makapa-enroll sa kurso.",
"editLessonPending": "Ang tagsa-tagsa nga pag-edit sa mga leksiyon moabot pa.",
"timeToday": "karon",
"timeSinceOneDay": "sukad 1 ka adlaw",
"timeSinceDays": "sukad {count} ka adlaw",
"reviewDueNow": "angay na karon",
"reviewDueTomorrow": "angay ugma",
"reviewDueInDays": "angay sulod sa {count} ka adlaw",
"reviewDueToday": "angay karon",
"reviewDueSinceOneDay": "angay na sukad 1 ka adlaw",
"reviewDueSinceDays": "angay na sukad {count} ka adlaw",
"reviewStageDay1": "Adlaw 1",
"reviewStageDay3": "Adlaw 3",
"reviewStageDay7": "Adlaw 7",
"reviewStageCompleted": "Nahuman ang review",
"phaseQuickstart": "Paspas nga sugod",
"phaseDailyLife": "Adlaw-adlaw",
"phaseStabilization": "Pagpalig-on",
"phaseDefault": "Hugna sa pagkat-on",
"didacticModeCoreInput": "Bag-ong sulod",
"didacticModeGuidedDialogue": "Giyahang dayalogo",
"didacticModeContrastTraining": "Contrast training",
"didacticModePatternDrill": "Pagpraktis sa mga hulma",
"didacticModeRealLifeScenario": "Tinuod nga kahimtang sa adlaw-adlaw",
"didacticModeIntensiveReview": "Hugna sa balik-balik",
"didacticModeCheckpoint": "Checkpoint",
"didacticModeDefault": "Yunit sa pagkat-on",
"didacticModeFocusDefault": "Pokus sa pagkat-on",
"lessonMetaFocus": "Pokus",
"lessonMetaPhase": "Hugna",
"lessonMetaNewUnits": "Bag-ong yunit",
"lessonMetaReview": "Balik-balik",
"intensiveReviewTitle": "Intensive nga hugna sa balik-balik",
"intensiveReviewIntro": "Kini nga leksiyon nag-una sa balik-balik ug pagpalalom. Gipakunhod og tinuyo ang bag-ong sulod aron malig-on ang mga hulma nga nakat-onan na.",
"reviewPriorityTitle": "Hinay-hinay nga gisagol ang balik-balik",
"reviewPriorityIntro": "Sa sinugdan, ang pokus anaa sa bag-ong mga pulong niining leksiyona. Samtang mopadayon ka, hinay-hinay nga masagol ang daan nga bokabularyo.",
"exerciseLockTitle": "Naka-lock pa ang chapter test",
"trainerStartWithReview": "Sugdi sa bag-ong bokabularyo niining leksiyona. Samtang nagpraktis ka, awtomatikong isagol sa trainer ang angay nga balik-balik.",
"startLesson": "Sugdi ang leksiyon",
"trainerProgressNewContent": "Bag-ong sulod: {current}/{target}",
"trainerProgressReview": "Balik-balik: {count}",
"trainerProgressMixShare": "Nasagol nga bahin: {percent}%",
"unknownExerciseTypeNotice": "Kini nga matang sa ehersisyo wala pa ipakita nga interaktibo sa kasamtangang view.",
"unknownExerciseTypeLabel": "Matang: {type}",
"lessonReviewHeadlineDone": "Nakaabot na kini nga leksiyon sa libre nga pagpalalom.",
"lessonReviewHeadlineDue": "Angay na karon kining review wave.",
"lessonReviewHeadlineScheduled": "Gitakda kini nga leksiyon para sa sunod nga review wave.",
"lessonReviewHintDone": "Nahuman na ang 1/3/7 ka adlaw nga balik-balik. Mahimo na nimo kining praktison sa mas luag nga paagi.",
"lessonReviewHintNextDue": "Sunod nga petsa: {due}.",
"reviewTimeNow": "karon",
"reviewTimeTomorrow": "ugma",
"reviewTimeInDays": "sulod sa {count} ka adlaw"
}
}
}
}

View File

@@ -461,7 +461,9 @@
"selectStockType": "Lagertyp auswählen",
"costPerUnit": "Kosten pro Einheit",
"buycost": "Kosten",
"sellincome": "Einnahmen"
"sellincome": "Einnahmen",
"buyPartialError": "Fehler beim Kaufen eines Teils der Lagerkapazität.",
"sellError": "Fehler beim Verkaufen der Lagerkapazität."
},
"vehicles": {
"cargo_cart": "Lastkarren",

View File

@@ -1,6 +1,12 @@
{
"friends": {
"kicker": "Community",
"intro": "Freundschaften, offene Anfragen und laufende Kontakte an einem Ort.",
"title": "Freunde",
"stats": {
"existing": "Bestehend",
"open": "Offen"
},
"tabs": {
"existing": "Bestehende",
"rejected": "Abgelehnte",

View File

@@ -41,6 +41,59 @@
"message": {
"close": "Schließen"
},
"appShell": {
"header": {
"tagline": "Community-Plattform",
"beta": "Beta",
"backend": "Backend",
"daemon": "Daemon",
"language": "Sprache"
},
"footer": {
"systemLabel": "System",
"noOpenDialogs": "Keine offenen Dialoge",
"activeWindows": "{count} Fenster aktiv",
"systemReady": "System bereit",
"systemStatusUnavailable": "Der Systemstatus ist in dieser Ansicht derzeit nicht direkt verfügbar."
}
},
"widgets": {
"dashboard": {
"dragHandle": "Verschieben",
"loading": "Laden..."
},
"birthdays": {
"today": "Heute!",
"tomorrow": "Morgen",
"turningAge": "(wird {age})",
"inDays": "{count} Tage",
"empty": "Keine Geburtstage von Freunden sichtbar"
},
"upcoming": {
"today": "Heute",
"tomorrow": "Morgen",
"timeAt": "{time} Uhr",
"allDay": "Ganztägig",
"empty": "Keine anstehenden Termine"
},
"appointments": {
"title": "📅 Termine",
"loading": "Lade Termine...",
"empty": "Keine bevorstehenden Termine",
"loadError": "Termine konnten nicht geladen werden"
},
"list": {
"noEntries": "Keine Einträge",
"entriesCount": "({count} Einträge)",
"fieldsCount": "({count} Felder)"
},
"news": {
"emptyValue": "—"
},
"falukant": {
"emptyValue": "—"
}
},
"gender": {
"male": "Männlich",
"female": "Weiblich",

View File

@@ -1,5 +1,54 @@
{
"home": {
"dashboard": {
"kicker": "Dein Bereich",
"title": "Willkommen zurück!",
"subtitle": "Dein persönlicher Einstieg in Community, Termine, Falukant und laufende Aktivitäten.",
"edit": "Dashboard bearbeiten",
"addWidget": "+ Widget hinzufügen ...",
"addAgain": "Nochmal hinzufügen",
"done": "Fertig",
"sectionTitle": "Deine Übersicht",
"sectionIntro": "Widgets lassen sich verschieben und im Bearbeitungsmodus anpassen.",
"widgetTitlePlaceholder": "Titel",
"removeWidget": "Widget entfernen",
"remove": "Entfernen",
"empty": "Noch keine Widgets. Klicke auf „Dashboard bearbeiten“ und dann „+ Widget hinzufügen“.",
"defaultAppointmentsWidget": "Termine",
"loadError": "Dashboard konnte nicht geladen werden.",
"saveError": "Dashboard konnte nicht gespeichert werden.",
"widgetLabels": {
"appointments": "Termine",
"falukant": "Falukant",
"news": "News",
"birthdays": "Geburtstage",
"upcoming": "Nächste Termine",
"calendar": "Kalender"
},
"overview": {
"activeWidgetsLabel": "Aktive Widgets",
"activeWidgetsText": "Dein Dashboard ist modular aufgebaut und kann jederzeit umsortiert werden.",
"availableModulesLabel": "Verfügbare Module",
"availableModulesText": "Du kannst Community-, Kalender-, News- und Falukant-Module kombinieren.",
"editModeLabel": "Bearbeitungsmodus",
"editModeActive": "Aktiv",
"editModeInactive": "Aus",
"editModeActiveText": "Widgets können gerade ergänzt und angepasst werden.",
"editModeInactiveText": "Inhalte bleiben fokussiert und ruhig lesbar."
}
},
"vocabLanding": {
"eyebrow": "Sprachen online lernen",
"title": "Der Vokabeltrainer auf YourPart kombiniert Lernen, Kurse und Übungen in einer Plattform.",
"lead": "Arbeite mit interaktiven Lektionen, erweitere deinen Wortschatz und nutze strukturierte Inhalte für einen motivierenden Lernfluss direkt im Browser.",
"cta": "Kostenlos starten",
"feature1Title": "Interaktive Kurse",
"feature1Text": "Kurse, Lektionen und Übungen helfen beim systematischen Aufbau neuer Sprachkenntnisse.",
"feature2Title": "Praxisorientiert",
"feature2Text": "Wortschatz, Grammatik und Wiederholung werden auf eine alltagstaugliche Lernroutine ausgerichtet.",
"feature3Title": "Teil einer Community",
"feature3Text": "Der Sprachbereich ist in eine größere Community-Plattform mit Blogs, Forum und Chat eingebettet."
},
"betaNoticeLabel": "Beta-Hinweis:",
"betaNoticeText": "YourPart befindet sich in aktiver Entwicklung. Funktionen können unvollständig sein, Inhalte fehlen noch und es kann zu Änderungen kommen.",
"nologin": {

View File

@@ -2,6 +2,8 @@
"personal": {
"calendar": {
"title": "Kalender",
"kicker": "Planung",
"intro": "Termine, Geburtstage und eigene Einträge in einer strukturierten Übersicht.",
"today": "Heute",
"newEntry": "Neuer Eintrag",
"editEntry": "Eintrag bearbeiten",

View File

@@ -57,7 +57,8 @@
"language": {
"de": "Deutsch",
"en": "Englisch",
"ceb": "Bisaya"
"ceb": "Bisaya",
"es": "Spanisch"
},
"eyecolor": {
"blue": "Blau",

View File

@@ -1,6 +1,11 @@
{
"socialnetwork": {
"usersearch": {
"kicker": "Community-Suche",
"intro": "Mit Namen, Alter und Geschlecht gezielt passende Kontakte in der Community finden.",
"ageSeparator": "bis",
"resultsCount": "{count} Treffer",
"openProfile": "Profil öffnen",
"title": "Benutzersuche",
"username": "Benutzername",
"age_from": "Alter von",
@@ -120,7 +125,8 @@
"hideInput": "Neuer Eintrag verbergen",
"imageUpload": "Bild",
"submit": "Eintrag absenden",
"noEntries": "Keine Einträge gefunden"
"noEntries": "Keine Einträge gefunden",
"entryImageAlt": "Bild zum Gästebucheintrag"
},
"interestedInGender": "Interessiert an",
"hasChildren": "Hat Kinder",
@@ -147,6 +153,8 @@
"weight": "Gewicht"
},
"gallery": {
"kicker": "Bilder und Ordner",
"intro": "Eigene Inhalte organisieren, sichtbar machen und in Ordnern strukturieren.",
"title": "Gallerie",
"folders": "Ordner",
"create_folder": "Ordner anlegen",
@@ -189,15 +197,22 @@
},
"show_image_dialog": {
"title": "Bild"
}
},
"imagePreviewAlt": "Bildvorschau",
"imageLoadingAlt": "Bild wird geladen"
},
"guestbook": {
"kicker": "Gästebuch",
"intro": "Nachrichten, Rückmeldungen und kleine Einblicke aus deinem Netzwerk.",
"title": "Gästebuch",
"prevPage": "Zurück",
"nextPage": "Weiter",
"page": "Seite"
},
"diary": {
"kicker": "Persönliche Einträge",
"intro": "Gedanken, Notizen und kurze Updates in einer ruhigen, persönlichen Ansicht.",
"placeholder": "Schreibe deinen Tagebucheintrag...",
"title": "Tagebuch",
"noEntries": "Du hast noch keine Tagebucheinträge gemacht.",
"newEntry": "Neuer Tagebucheintrag",
@@ -213,6 +228,16 @@
"page": "Seite"
},
"forum": {
"kicker": "Community-Forum",
"intro": "Themen, Diskussionen und neue Beiträge an einem strukturierten Ort.",
"createTitle": "Neues Thema verfassen",
"createIntro": "Erst Titel setzen, dann den Beitrag schreiben und anschließend direkt veröffentlichen.",
"cancelCreation": "Abbrechen",
"creationHint": "Titel und Inhalt müssen beide ausgefüllt sein.",
"communityFallback": "Community",
"topicIntro": "Diskussionen, Antworten und neue Beiträge in einer fokussierten Lesefläche.",
"topicCreated": "Thema erfolgreich erstellt.",
"topicCreateError": "Fehler beim Erstellen des Themas",
"title": "Forum",
"showNewTopic": "Neues Thema erstellen",
"hideNewTopic": "Erstellen unterbrechen",
@@ -277,9 +302,32 @@
"videoUploadHint": "Lade hier Videos für deinen freigeschalteten Erotikbereich hoch und pflege Titel sowie Beschreibung direkt beim Upload.",
"videoDescription": "Beschreibung",
"videoFile": "Videodatei",
"videoFormats": "MP4, WEBM, OGG, MOV",
"myVideos": "Meine Videos",
"sharedVideos": "Freigegebene Videos",
"foreignVideosIntro": "Freigegebene Videos aus dem Erwachsenenbereich.",
"foreignVideosOnlyHint": "Du siehst hier nur Videos, die dir für den Erwachsenenbereich freigegeben wurden.",
"sharedVideosIntro": "Sichtbare Videos aus freigegebenen Erwachsenenbereichen.",
"noSharedVideos": "Für dich sind aktuell keine freigegebenen Videos vorhanden.",
"libraryTitle": "Bibliothek",
"libraryIntro": "Eigene Uploads, Freigaben und Meldungen an einem Ort.",
"libraryEmptyHint": "Lege links dein erstes Video an und verwalte es danach hier in der Bibliothek.",
"latestUpload": "Letzter Upload",
"visibleVideos": "Sichtbare Videos",
"moderationCases": "Moderationsfälle",
"notesTitle": "Hinweise",
"friendsVisibilityHint": "Freunde sehen Inhalte nur dann, wenn sie volljährig und für den Erwachsenenbereich freigeschaltet sind.",
"selectedUsersVisibilityHint": "Gezielt freigegebene Personen müssen ebenfalls volljährig und freigeschaltet sein.",
"selectedUsersPlaceholder": "anna, bert, clara",
"imagePreviewAlt": "Bildvorschau",
"imageLoadingAlt": "Bild wird geladen",
"untitled": "Ohne Titel",
"noUploadYet": "Noch kein Upload",
"closeEditing": "Bearbeitung schließen",
"editVisibility": "Freigaben bearbeiten",
"noVideos": "Du hast noch keine Erotikvideos hochgeladen.",
"reportAction": "Melden",
"reportHint": "Nutze {action} direkt am jeweiligen Eintrag, wenn Inhalte geprüft werden sollen.",
"reportNote": "Kurze Notiz für die Moderation",
"submitReport": "Meldung absenden",
"reportSubmitted": "Die Meldung wurde aufgenommen.",
@@ -323,6 +371,36 @@
"vocab": {
"title": "Vokabeltrainer",
"description": "Lege Sprachen an (oder abonniere sie) und teile sie mit Freunden.",
"heroEyebrow": "Sprachenlernen",
"summaryTotalLabel": "Sprachen gesamt",
"summaryTotalIntro": "Alle aktiven Sprachbereiche, in denen du Inhalte nutzt oder verwaltest.",
"summaryOwnedLabel": "Eigene Bereiche",
"summaryOwnedIntro": "Hier legst du Inhalte, Kapitel und Lernmaterial aktiv selbst an.",
"summarySubscribedLabel": "Abonniert",
"summarySubscribedIntro": "Diese Bereiche sind eher für Lernen und Fortschritt statt Verwaltung gedacht.",
"taskCreateEyebrow": "Schnellstart",
"taskCreateTitle": "Neue Sprache anlegen",
"taskCreateIntro": "Der beste Einstieg, wenn du Inhalte selbst strukturieren und pflegen willst.",
"taskContinueEyebrow": "Weiterlernen",
"taskContinueTitle": "Kurse und Kapitel öffnen",
"taskContinueIntro": "Springe direkt in bestehende Lernpfade und arbeite mit vorhandenen Kursen weiter.",
"ownedSectionTitle": "Eigene Sprachen",
"ownedSectionIntro": "Direkter Einstieg in Bearbeitung, Kapitel und Kursverwaltung.",
"ownedHint": "Verwalten und Inhalte pflegen",
"ownedEmpty": "Noch keine eigenen Sprachbereiche vorhanden.",
"subscribedSectionTitle": "Abonnierte Sprachen",
"subscribedSectionIntro": "Gut für schnellen Wiedereinstieg ins Lernen ohne Verwaltungsaufwand.",
"subscribedHint": "Lernen, üben und Fortschritt ansehen",
"subscribedEmpty": "Keine abonnierten Sprachen vorhanden.",
"languageHeroEyebrow": "Sprache",
"languageHeroIntro": "Kapitel, Suchfunktionen und Freigaben für diese Sprache an einem Ort.",
"newLanguageHeroEyebrow": "Vokabeltrainer",
"newLanguageHeroIntro": "Neue Sprache anlegen, Freigabecode erzeugen und direkt in die Bearbeitung wechseln.",
"newLanguageNameHint": "Ein kurzer, klarer Sprachname reicht für den Start.",
"newLanguageNameValidation": "Der Name sollte mindestens 2 Zeichen haben.",
"subscribeHeroEyebrow": "Vokabeltrainer",
"chapterHeroEyebrow": "Vokabeltrainer",
"chapterHeroIntro": "Kapitelinhalt durchsuchen, Vokabeln pflegen und direkt in die Übung wechseln.",
"newLanguage": "Neue Sprache",
"newLanguageTitle": "Neue Sprache anlegen",
"languageName": "Name der Sprache",
@@ -570,7 +648,91 @@
"languageAssistantPatternHint": "Nutze dabei besonders dieses Muster",
"languageAssistantPresetPracticeStart": "Lass uns zur Lektion \"{lesson}\" einen kurzen alltagsnahen Dialog üben. Stelle mir bitte Fragen und warte auf meine Antworten.",
"languageAssistantPresetCorrectStart": "Ich möchte eigene Sätze zur Lektion \"{lesson}\" schreiben. Bitte korrigiere meine Antworten knapp und verständlich.",
"thisLesson": "dieser Lektion"
"thisLesson": "dieser Lektion",
"courseKicker": "Lernkurs",
"courseListKicker": "Kurse",
"courseListIntro": "Öffentliche und eigene Lernkurse filtern, finden und direkt weiterlernen.",
"courseShareCodePlaceholder": "z. B. abc123def456",
"courseFlowEyebrow": "Tagesfluss",
"courseFlowTitle": "Heute sinnvoll weitermachen",
"courseFlowIntro": "Die Reihenfolge folgt dem Konzept: fällige Wiederholung zuerst, dann aktueller Block, danach Intensivphase und freie Vertiefung.",
"courseFlowReviewStat": "Fällige Wiederholung: {count}",
"courseFlowBlockStat": "Aktiver Block: {block}",
"courseFlowReviewTitle": "Fällige Wiederholung",
"courseFlowReviewDescription": "Bereits abgeschlossene Lektionen, die heute wieder drankommen sollten.",
"courseFlowReviewEmpty": "Heute ist keine ältere Lektion als fällige Wiederholung markiert.",
"courseFlowBlockTitle": "Aktueller Block",
"courseFlowBlockDescription": "Hier liegt der nächste reguläre Fortschritt im Kurs.",
"courseFlowBlockEmpty": "Der aktuelle Block ist bereits abgeschlossen oder es gibt gerade keine offene Blocklektion.",
"courseFlowIntensiveTitle": "Fällige Intensivphase",
"courseFlowIntensiveDescription": "Verdichtete Wiederholung, sobald der Block davor weitgehend sitzt.",
"courseFlowIntensiveEmpty": "Aktuell ist keine neue Intensivphase freigeschaltet.",
"courseFlowPracticeTitle": "Freie Vertiefung",
"courseFlowPracticeDescription": "Abgeschlossene Lektionen für lockeres Nachtrainieren außerhalb des Pflichtpfads.",
"courseFlowPracticeEmpty": "Sobald du erste Lektionen abgeschlossen hast, erscheinen sie hier für freies Nachtrainieren.",
"practiceInTrainer": "Im Trainer üben",
"lessonsCount": "{count} Lektionen",
"lessonBlockLabel": "Block {number}",
"lessonIntensiveBadge": "Intensive Wiederholung",
"addLessonValidation": "Bitte Nummer, Titel und Kapitel vollständig angeben.",
"addLessonSuccess": "Lektion erfolgreich angelegt.",
"addLessonError": "Fehler beim Hinzufügen der Lektion.",
"createCourseError": "Fehler beim Erstellen des Kurses.",
"deleteLessonTitle": "Lektion löschen",
"deleteLessonSuccess": "Lektion erfolgreich gelöscht.",
"deleteLessonError": "Fehler beim Löschen der Lektion.",
"enrollCourseError": "Fehler beim Einschreiben.",
"editLessonPending": "Die Bearbeitung einzelner Lektionen folgt noch.",
"timeToday": "heute",
"timeSinceOneDay": "seit 1 Tag",
"timeSinceDays": "seit {count} Tagen",
"reviewDueNow": "jetzt fällig",
"reviewDueTomorrow": "morgen fällig",
"reviewDueInDays": "in {count} Tagen fällig",
"reviewDueToday": "heute fällig",
"reviewDueSinceOneDay": "seit 1 Tag fällig",
"reviewDueSinceDays": "seit {count} Tagen fällig",
"reviewStageDay1": "Tag 1",
"reviewStageDay3": "Tag 3",
"reviewStageDay7": "Tag 7",
"reviewStageCompleted": "Review abgeschlossen",
"phaseQuickstart": "Schnellstart",
"phaseDailyLife": "Alltag",
"phaseStabilization": "Stabilisierung",
"phaseDefault": "Lernphase",
"didacticModeCoreInput": "Neuer Stoff",
"didacticModeGuidedDialogue": "Geführter Dialog",
"didacticModeContrastTraining": "Kontrasttraining",
"didacticModePatternDrill": "Mustertraining",
"didacticModeRealLifeScenario": "Alltagsszenario",
"didacticModeIntensiveReview": "Wiederholungsphase",
"didacticModeCheckpoint": "Checkpoint",
"didacticModeDefault": "Lerneinheit",
"didacticModeFocusDefault": "Lernfokus",
"lessonMetaFocus": "Fokus",
"lessonMetaPhase": "Phase",
"lessonMetaNewUnits": "Neue Einheiten",
"lessonMetaReview": "Wiederholung",
"intensiveReviewTitle": "Intensive Wiederholungsphase",
"intensiveReviewIntro": "Diese Lektion priorisiert Wiederholung und Vertiefung. Neuer Stoff wird bewusst reduziert, damit vorhandene Muster stabil werden.",
"reviewPriorityTitle": "Wiederholung läuft schrittweise mit",
"reviewPriorityIntro": "Zuerst liegt der Fokus auf den neuen Begriffen dieser Lektion. Mit deinem Fortschritt fließen ältere Vokabeln dann zunehmend mit ein.",
"exerciseLockTitle": "Kapitel-Prüfung noch gesperrt",
"trainerStartWithReview": "Starte mit den neuen Vokabeln dieser Lektion. Mit fortschreitendem Üben mischt der Trainer automatisch passende Wiederholungen ein.",
"startLesson": "Lektion starten",
"trainerProgressNewContent": "Neue Inhalte: {current}/{target}",
"trainerProgressReview": "Wiederholung: {count}",
"trainerProgressMixShare": "Mischanteil: {percent}%",
"unknownExerciseTypeNotice": "Dieser Übungstyp wird in der aktuellen Ansicht noch nicht interaktiv dargestellt.",
"unknownExerciseTypeLabel": "Typ: {type}",
"lessonReviewHeadlineDone": "Diese Lektion ist in der freien Vertiefung angekommen.",
"lessonReviewHeadlineDue": "Diese Review-Welle ist jetzt fällig.",
"lessonReviewHeadlineScheduled": "Diese Lektion ist für die nächste Review-Welle vorgemerkt.",
"lessonReviewHintDone": "Die 1/3/7-Tage-Wiederholung ist abgeschlossen. Du kannst die Lektion jetzt flexibel weitertrainieren.",
"lessonReviewHintNextDue": "Nächste Fälligkeit: {due}.",
"reviewTimeNow": "jetzt",
"reviewTimeTomorrow": "morgen",
"reviewTimeInDays": "in {count} Tagen"
}
}
}

View File

@@ -381,6 +381,10 @@
"sale": {
"runningGuards": "Guards"
},
"storage": {
"buyPartialError": "Error while buying part of the storage capacity.",
"sellError": "Error while selling storage capacity."
},
"production": {
"title": "Production",
"info": "Details about production in the branch.",

View File

@@ -1,6 +1,12 @@
{
"friends": {
"kicker": "Community",
"intro": "Friendships, open requests, and ongoing contacts in one place.",
"title": "Friends",
"stats": {
"existing": "Existing",
"open": "Open"
},
"tabs": {
"existing": "Existing",
"rejected": "Rejected",

View File

@@ -41,6 +41,59 @@
"message": {
"close": "Close"
},
"appShell": {
"header": {
"tagline": "Community platform",
"beta": "Beta",
"backend": "Backend",
"daemon": "Daemon",
"language": "Language"
},
"footer": {
"systemLabel": "System",
"noOpenDialogs": "No open dialogs",
"activeWindows": "{count} windows active",
"systemReady": "System ready",
"systemStatusUnavailable": "System status is not directly available in this view right now."
}
},
"widgets": {
"dashboard": {
"dragHandle": "Move",
"loading": "Loading..."
},
"birthdays": {
"today": "Today!",
"tomorrow": "Tomorrow",
"turningAge": "(turning {age})",
"inDays": "{count} days",
"empty": "No visible birthdays from friends"
},
"upcoming": {
"today": "Today",
"tomorrow": "Tomorrow",
"timeAt": "{time}",
"allDay": "All day",
"empty": "No upcoming appointments"
},
"appointments": {
"title": "📅 Appointments",
"loading": "Loading appointments...",
"empty": "No upcoming appointments",
"loadError": "Appointments could not be loaded"
},
"list": {
"noEntries": "No entries",
"entriesCount": "({count} entries)",
"fieldsCount": "({count} fields)"
},
"news": {
"emptyValue": "—"
},
"falukant": {
"emptyValue": "—"
}
},
"gender": {
"male": "Male",
"female": "Female",

View File

@@ -1,5 +1,54 @@
{
"home": {
"dashboard": {
"kicker": "Your area",
"title": "Welcome back!",
"subtitle": "Your personal entry point to community, appointments, Falukant, and ongoing activity.",
"edit": "Edit dashboard",
"addWidget": "+ Add widget ...",
"addAgain": "Add again",
"done": "Done",
"sectionTitle": "Your overview",
"sectionIntro": "Widgets can be moved and adjusted in edit mode.",
"widgetTitlePlaceholder": "Title",
"removeWidget": "Remove widget",
"remove": "Remove",
"empty": "No widgets yet. Click “Edit dashboard” and then “+ Add widget”.",
"defaultAppointmentsWidget": "Appointments",
"loadError": "Dashboard could not be loaded.",
"saveError": "Dashboard could not be saved.",
"widgetLabels": {
"appointments": "Appointments",
"falukant": "Falukant",
"news": "News",
"birthdays": "Birthdays",
"upcoming": "Upcoming appointments",
"calendar": "Calendar"
},
"overview": {
"activeWidgetsLabel": "Active widgets",
"activeWidgetsText": "Your dashboard is modular and can be rearranged at any time.",
"availableModulesLabel": "Available modules",
"availableModulesText": "You can combine community, calendar, news, and Falukant modules.",
"editModeLabel": "Edit mode",
"editModeActive": "Active",
"editModeInactive": "Off",
"editModeActiveText": "Widgets can currently be added and adjusted.",
"editModeInactiveText": "Content stays focused and calm to read."
}
},
"vocabLanding": {
"eyebrow": "Learn languages online",
"title": "The vocabulary trainer on YourPart combines learning, courses, and practice in one platform.",
"lead": "Work with interactive lessons, grow your vocabulary, and use structured content for a motivating learning flow right in the browser.",
"cta": "Start for free",
"feature1Title": "Interactive courses",
"feature1Text": "Courses, lessons, and exercises help you build new language skills step by step.",
"feature2Title": "Practice-focused",
"feature2Text": "Vocabulary, grammar, and review are aligned with an everyday learning routine.",
"feature3Title": "Part of a community",
"feature3Text": "The language area is embedded in a larger community platform with blogs, forum, and chat."
},
"betaNoticeLabel": "Beta notice:",
"betaNoticeText": "YourPart is under active development. Features may be incomplete, information may still be missing and things can change.",
"nologin": {

View File

@@ -2,6 +2,8 @@
"personal": {
"calendar": {
"title": "Calendar",
"kicker": "Planning",
"intro": "Appointments, birthdays, and your own entries in one structured overview.",
"today": "Today",
"newEntry": "New Entry",
"editEntry": "Edit Entry",

View File

@@ -57,7 +57,8 @@
"language": {
"de": "German",
"en": "English",
"ceb": "Bisaya"
"ceb": "Bisaya",
"es": "Spanish"
},
"eyecolor": {
"blue": "Blue",

View File

@@ -1,6 +1,11 @@
{
"socialnetwork": {
"usersearch": {
"kicker": "Community search",
"intro": "Find suitable contacts in the community by name, age, and gender.",
"ageSeparator": "to",
"resultsCount": "{count} results",
"openProfile": "Open profile",
"title": "User Search",
"username": "Username",
"age_from": "Age from",
@@ -120,7 +125,8 @@
"hideInput": "Hide new entry",
"imageUpload": "Image",
"submit": "Submit entry",
"noEntries": "No entries found"
"noEntries": "No entries found",
"entryImageAlt": "Guestbook entry image"
},
"interestedInGender": "Interested in",
"hasChildren": "Has children",
@@ -147,6 +153,8 @@
"weight": "Weight"
},
"gallery": {
"kicker": "Images and folders",
"intro": "Organize your own content, control visibility, and structure it in folders.",
"title": "Gallery",
"folders": "Folders",
"create_folder": "Create folder",
@@ -189,15 +197,22 @@
},
"show_image_dialog": {
"title": "Image"
}
},
"imagePreviewAlt": "Image preview",
"imageLoadingAlt": "Loading image"
},
"guestbook": {
"kicker": "Guestbook",
"intro": "Messages, feedback, and small glimpses from your network.",
"title": "Guestbook",
"prevPage": "Back",
"nextPage": "Next",
"page": "Page"
},
"diary": {
"kicker": "Personal entries",
"intro": "Thoughts, notes, and short updates in a calm personal view.",
"placeholder": "Write your diary entry...",
"title": "Diary",
"noEntries": "You haven't made any diary entries yet.",
"newEntry": "New diary entry",
@@ -213,6 +228,16 @@
"page": "Page"
},
"forum": {
"kicker": "Community forum",
"intro": "Topics, discussions, and new posts in one structured place.",
"createTitle": "Write a new topic",
"createIntro": "Set the title first, then write the post and publish it directly.",
"cancelCreation": "Cancel",
"creationHint": "Title and content must both be filled in.",
"communityFallback": "Community",
"topicIntro": "Discussions, replies, and new posts in a focused reading view.",
"topicCreated": "Topic created successfully.",
"topicCreateError": "Error creating topic",
"title": "Forum",
"showNewTopic": "Create new topic",
"hideNewTopic": "Cancel creation",
@@ -277,9 +302,32 @@
"videoUploadHint": "Upload videos for your unlocked erotic area here and maintain title and description directly during upload.",
"videoDescription": "Description",
"videoFile": "Video file",
"videoFormats": "MP4, WEBM, OGG, MOV",
"myVideos": "My videos",
"sharedVideos": "Shared videos",
"foreignVideosIntro": "Shared videos from the adult area.",
"foreignVideosOnlyHint": "You only see videos here that were shared with you for the adult area.",
"sharedVideosIntro": "Visible videos from shared adult areas.",
"noSharedVideos": "There are currently no shared videos available for you.",
"libraryTitle": "Library",
"libraryIntro": "Your uploads, visibility settings, and reports in one place.",
"libraryEmptyHint": "Create your first video on the left and manage it here afterwards in the library.",
"latestUpload": "Latest upload",
"visibleVideos": "Visible videos",
"moderationCases": "Moderation cases",
"notesTitle": "Notes",
"friendsVisibilityHint": "Friends can only see content if they are adults and unlocked for the adult area.",
"selectedUsersVisibilityHint": "Specifically shared people must also be adults and unlocked.",
"selectedUsersPlaceholder": "anna, bert, clara",
"imagePreviewAlt": "Image preview",
"imageLoadingAlt": "Loading image",
"untitled": "Untitled",
"noUploadYet": "No upload yet",
"closeEditing": "Close editing",
"editVisibility": "Edit visibility",
"noVideos": "You have not uploaded any erotic videos yet.",
"reportAction": "Report",
"reportHint": "Use {action} directly on the respective item when content should be reviewed.",
"reportNote": "Short note for moderation",
"submitReport": "Submit report",
"reportSubmitted": "The report has been submitted.",
@@ -323,6 +371,36 @@
"vocab": {
"title": "Vocabulary trainer",
"description": "Create languages (or subscribe to them) and share them with friends.",
"heroEyebrow": "Language learning",
"summaryTotalLabel": "Languages total",
"summaryTotalIntro": "All active language areas where you use or manage content.",
"summaryOwnedLabel": "Owned areas",
"summaryOwnedIntro": "This is where you actively create content, chapters, and learning material yourself.",
"summarySubscribedLabel": "Subscribed",
"summarySubscribedIntro": "These areas are more about learning and progress than administration.",
"taskCreateEyebrow": "Quick start",
"taskCreateTitle": "Create new language",
"taskCreateIntro": "The best starting point if you want to structure and maintain content yourself.",
"taskContinueEyebrow": "Continue learning",
"taskContinueTitle": "Open courses and chapters",
"taskContinueIntro": "Jump straight into existing learning paths and continue with available courses.",
"ownedSectionTitle": "Owned languages",
"ownedSectionIntro": "Direct access to editing, chapters, and course management.",
"ownedHint": "Manage and maintain content",
"ownedEmpty": "No owned language areas yet.",
"subscribedSectionTitle": "Subscribed languages",
"subscribedSectionIntro": "Good for quickly getting back into learning without admin overhead.",
"subscribedHint": "Learn, practice, and review progress",
"subscribedEmpty": "No subscribed languages available.",
"languageHeroEyebrow": "Language",
"languageHeroIntro": "Chapters, search tools, and sharing for this language in one place.",
"newLanguageHeroEyebrow": "Vocabulary trainer",
"newLanguageHeroIntro": "Create a new language, generate a share code, and jump straight into editing.",
"newLanguageNameHint": "A short, clear language name is enough to get started.",
"newLanguageNameValidation": "The name should have at least 2 characters.",
"subscribeHeroEyebrow": "Vocabulary trainer",
"chapterHeroEyebrow": "Vocabulary trainer",
"chapterHeroIntro": "Browse chapter content, maintain vocabulary, and jump directly into practice.",
"newLanguage": "New language",
"newLanguageTitle": "Create new language",
"languageName": "Language name",
@@ -570,7 +648,91 @@
"languageAssistantPatternHint": "Focus especially on this pattern",
"languageAssistantPresetPracticeStart": "Let's practice a short everyday dialogue for the lesson \"{lesson}\". Please ask me questions and wait for my answers.",
"languageAssistantPresetCorrectStart": "I want to write my own sentences for the lesson \"{lesson}\". Please correct my answers briefly and clearly.",
"thisLesson": "this lesson"
"thisLesson": "this lesson",
"courseKicker": "Learning course",
"courseListKicker": "Courses",
"courseListIntro": "Filter public and your own learning courses, find the right one, and continue directly.",
"courseShareCodePlaceholder": "e.g. abc123def456",
"courseFlowEyebrow": "Daily flow",
"courseFlowTitle": "Best next step for today",
"courseFlowIntro": "The order follows the concept: due reviews first, then the current block, then intensive review, and finally free practice.",
"courseFlowReviewStat": "Due review: {count}",
"courseFlowBlockStat": "Active block: {block}",
"courseFlowReviewTitle": "Due review",
"courseFlowReviewDescription": "Lessons already completed that should come back today.",
"courseFlowReviewEmpty": "No older lesson is marked as due for review today.",
"courseFlowBlockTitle": "Current block",
"courseFlowBlockDescription": "This is where the next regular progress step in the course lives.",
"courseFlowBlockEmpty": "The current block is already done or there is no open block lesson right now.",
"courseFlowIntensiveTitle": "Due intensive review",
"courseFlowIntensiveDescription": "Condensed review once the block before it is mostly stable.",
"courseFlowIntensiveEmpty": "No new intensive review is unlocked right now.",
"courseFlowPracticeTitle": "Free practice",
"courseFlowPracticeDescription": "Completed lessons for relaxed extra practice outside the required path.",
"courseFlowPracticeEmpty": "As soon as you complete your first lessons, they will appear here for free practice.",
"practiceInTrainer": "Practice in trainer",
"lessonsCount": "{count} lessons",
"lessonBlockLabel": "Block {number}",
"lessonIntensiveBadge": "Intensive review",
"addLessonValidation": "Please provide number, title, and chapter.",
"addLessonSuccess": "Lesson created successfully.",
"addLessonError": "Could not add the lesson.",
"createCourseError": "Could not create the course.",
"deleteLessonTitle": "Delete lesson",
"deleteLessonSuccess": "Lesson deleted successfully.",
"deleteLessonError": "Could not delete the lesson.",
"enrollCourseError": "Could not enroll in the course.",
"editLessonPending": "Editing individual lessons is still pending.",
"timeToday": "today",
"timeSinceOneDay": "since 1 day",
"timeSinceDays": "since {count} days",
"reviewDueNow": "due now",
"reviewDueTomorrow": "due tomorrow",
"reviewDueInDays": "due in {count} days",
"reviewDueToday": "due today",
"reviewDueSinceOneDay": "due since 1 day",
"reviewDueSinceDays": "due since {count} days",
"reviewStageDay1": "Day 1",
"reviewStageDay3": "Day 3",
"reviewStageDay7": "Day 7",
"reviewStageCompleted": "Review completed",
"phaseQuickstart": "Quick start",
"phaseDailyLife": "Daily life",
"phaseStabilization": "Stabilization",
"phaseDefault": "Learning phase",
"didacticModeCoreInput": "New content",
"didacticModeGuidedDialogue": "Guided dialogue",
"didacticModeContrastTraining": "Contrast training",
"didacticModePatternDrill": "Pattern drill",
"didacticModeRealLifeScenario": "Real-life scenario",
"didacticModeIntensiveReview": "Review phase",
"didacticModeCheckpoint": "Checkpoint",
"didacticModeDefault": "Learning unit",
"didacticModeFocusDefault": "Learning focus",
"lessonMetaFocus": "Focus",
"lessonMetaPhase": "Phase",
"lessonMetaNewUnits": "New units",
"lessonMetaReview": "Review",
"intensiveReviewTitle": "Intensive review phase",
"intensiveReviewIntro": "This lesson prioritizes review and consolidation. New material is intentionally reduced so existing patterns can stabilize.",
"reviewPriorityTitle": "Review is mixed in step by step",
"reviewPriorityIntro": "The focus starts on the new terms of this lesson. As you progress, older vocabulary is gradually mixed in.",
"exerciseLockTitle": "Chapter test still locked",
"trainerStartWithReview": "Start with the new vocabulary from this lesson. As you practice, the trainer will automatically mix in fitting review items.",
"startLesson": "Start lesson",
"trainerProgressNewContent": "New content: {current}/{target}",
"trainerProgressReview": "Review: {count}",
"trainerProgressMixShare": "Mixed share: {percent}%",
"unknownExerciseTypeNotice": "This exercise type is not displayed interactively in the current view yet.",
"unknownExerciseTypeLabel": "Type: {type}",
"lessonReviewHeadlineDone": "This lesson has reached the free practice stage.",
"lessonReviewHeadlineDue": "This review wave is due now.",
"lessonReviewHeadlineScheduled": "This lesson is scheduled for the next review wave.",
"lessonReviewHintDone": "The 1/3/7-day review cycle is complete. You can now continue practicing this lesson freely.",
"lessonReviewHintNextDue": "Next due date: {due}.",
"reviewTimeNow": "now",
"reviewTimeTomorrow": "tomorrow",
"reviewTimeInDays": "in {count} days"
}
}
}

View File

@@ -430,7 +430,9 @@
"selectStockType": "Seleccionar tipo de almacén",
"costPerUnit": "Coste por unidad",
"buycost": "Coste",
"sellincome": "Ingresos"
"sellincome": "Ingresos",
"buyPartialError": "Error al comprar parte de la capacidad del almacén.",
"sellError": "Error al vender la capacidad del almacén."
},
"vehicles": {
"cargo_cart": "Carro de carga",

View File

@@ -1,6 +1,12 @@
{
"friends": {
"kicker": "Comunidad",
"intro": "Amistades, solicitudes abiertas y contactos en curso en un solo lugar.",
"title": "Amigos",
"stats": {
"existing": "Activos",
"open": "Abiertos"
},
"tabs": {
"existing": "Actuales",
"rejected": "Rechazadas",

View File

@@ -41,6 +41,59 @@
"message": {
"close": "Cerrar"
},
"appShell": {
"header": {
"tagline": "Plataforma comunitaria",
"beta": "Beta",
"backend": "Backend",
"daemon": "Daemon",
"language": "Idioma"
},
"footer": {
"systemLabel": "Sistema",
"noOpenDialogs": "No hay diálogos abiertos",
"activeWindows": "{count} ventanas activas",
"systemReady": "Sistema listo",
"systemStatusUnavailable": "El estado del sistema no está disponible directamente en esta vista en este momento."
}
},
"widgets": {
"dashboard": {
"dragHandle": "Mover",
"loading": "Cargando..."
},
"birthdays": {
"today": "¡Hoy!",
"tomorrow": "Mañana",
"turningAge": "(cumple {age})",
"inDays": "{count} días",
"empty": "No hay cumpleaños visibles de amigos"
},
"upcoming": {
"today": "Hoy",
"tomorrow": "Mañana",
"timeAt": "{time} h",
"allDay": "Todo el día",
"empty": "No hay citas próximas"
},
"appointments": {
"title": "📅 Citas",
"loading": "Cargando citas...",
"empty": "No hay citas próximas",
"loadError": "No se pudieron cargar las citas"
},
"list": {
"noEntries": "No hay entradas",
"entriesCount": "({count} entradas)",
"fieldsCount": "({count} campos)"
},
"news": {
"emptyValue": "—"
},
"falukant": {
"emptyValue": "—"
}
},
"gender": {
"male": "Masculino",
"female": "Femenino",

View File

@@ -1,5 +1,54 @@
{
"home": {
"dashboard": {
"kicker": "Tu área",
"title": "¡Bienvenido de nuevo!",
"subtitle": "Tu punto de entrada personal a la comunidad, las citas, Falukant y la actividad en curso.",
"edit": "Editar panel",
"addWidget": "+ Añadir widget ...",
"addAgain": "Añadir de nuevo",
"done": "Listo",
"sectionTitle": "Tu resumen",
"sectionIntro": "Los widgets se pueden mover y ajustar en el modo de edición.",
"widgetTitlePlaceholder": "Título",
"removeWidget": "Eliminar widget",
"remove": "Eliminar",
"empty": "Aún no hay widgets. Haz clic en “Editar panel” y luego en “+ Añadir widget”.",
"defaultAppointmentsWidget": "Citas",
"loadError": "No se pudo cargar el panel.",
"saveError": "No se pudo guardar el panel.",
"widgetLabels": {
"appointments": "Citas",
"falukant": "Falukant",
"news": "Noticias",
"birthdays": "Cumpleaños",
"upcoming": "Próximas citas",
"calendar": "Calendario"
},
"overview": {
"activeWidgetsLabel": "Widgets activos",
"activeWidgetsText": "Tu panel es modular y se puede reorganizar en cualquier momento.",
"availableModulesLabel": "Módulos disponibles",
"availableModulesText": "Puedes combinar módulos de comunidad, calendario, noticias y Falukant.",
"editModeLabel": "Modo de edición",
"editModeActive": "Activo",
"editModeInactive": "Desactivado",
"editModeActiveText": "Ahora mismo se pueden añadir y ajustar widgets.",
"editModeInactiveText": "El contenido sigue siendo claro y fácil de leer."
}
},
"vocabLanding": {
"eyebrow": "Aprende idiomas en línea",
"title": "El entrenador de vocabulario en YourPart combina aprendizaje, cursos y ejercicios en una sola plataforma.",
"lead": "Trabaja con lecciones interactivas, amplía tu vocabulario y usa contenido estructurado para un flujo de aprendizaje motivador directamente en el navegador.",
"cta": "Empezar gratis",
"feature1Title": "Cursos interactivos",
"feature1Text": "Cursos, lecciones y ejercicios ayudan a construir nuevas competencias lingüísticas de forma sistemática.",
"feature2Title": "Orientado a la práctica",
"feature2Text": "Vocabulario, gramática y repaso se adaptan a una rutina de aprendizaje cotidiana.",
"feature3Title": "Parte de una comunidad",
"feature3Text": "El área de idiomas está integrada en una plataforma comunitaria más amplia con blogs, foro y chat."
},
"betaNoticeLabel": "Aviso beta:",
"betaNoticeText": "YourPart está en desarrollo activo. Algunas funciones pueden estar incompletas, pueden faltar contenidos y puede haber cambios.",
"nologin": {

View File

@@ -2,6 +2,8 @@
"personal": {
"calendar": {
"title": "Calendario",
"kicker": "Planificación",
"intro": "Citas, cumpleaños y tus propias entradas en una vista estructurada.",
"today": "Hoy",
"newEntry": "Nueva entrada",
"editEntry": "Editar entrada",

View File

@@ -57,7 +57,8 @@
"language": {
"de": "Alemán",
"en": "Inglés",
"ceb": "Bisaya"
"ceb": "Bisaya",
"es": "Español"
},
"eyecolor": {
"blue": "Azul",

View File

@@ -1,6 +1,11 @@
{
"socialnetwork": {
"usersearch": {
"kicker": "Búsqueda en la comunidad",
"intro": "Encuentra contactos adecuados en la comunidad por nombre, edad y género.",
"ageSeparator": "hasta",
"resultsCount": "{count} resultados",
"openProfile": "Abrir perfil",
"title": "Búsqueda de usuarios",
"username": "Nombre de usuario",
"age_from": "Edad desde",
@@ -120,7 +125,8 @@
"hideInput": "Ocultar nueva entrada",
"imageUpload": "Imagen",
"submit": "Enviar entrada",
"noEntries": "No se han encontrado entradas"
"noEntries": "No se han encontrado entradas",
"entryImageAlt": "Imagen de la entrada del libro de visitas"
},
"interestedInGender": "Interesado/a en",
"hasChildren": "Tiene hijos",
@@ -147,6 +153,8 @@
"weight": "Peso"
},
"gallery": {
"kicker": "Imágenes y carpetas",
"intro": "Organiza tu propio contenido, controla su visibilidad y ordénalo en carpetas.",
"title": "Galería",
"folders": "Carpetas",
"create_folder": "Crear carpeta",
@@ -189,15 +197,22 @@
},
"show_image_dialog": {
"title": "Imagen"
}
},
"imagePreviewAlt": "Vista previa de la imagen",
"imageLoadingAlt": "Cargando imagen"
},
"guestbook": {
"kicker": "Libro de visitas",
"intro": "Mensajes, comentarios y pequeños vistazos desde tu red.",
"title": "Libro de visitas",
"prevPage": "Atrás",
"nextPage": "Siguiente",
"page": "Página"
},
"diary": {
"kicker": "Entradas personales",
"intro": "Pensamientos, notas y pequeñas actualizaciones en una vista tranquila y personal.",
"placeholder": "Escribe tu entrada del diario...",
"title": "Diario",
"noEntries": "Aún no has escrito ninguna entrada en el diario.",
"newEntry": "Nueva entrada",
@@ -213,6 +228,16 @@
"page": "Página"
},
"forum": {
"kicker": "Foro de la comunidad",
"intro": "Temas, debates y nuevas publicaciones en un lugar estructurado.",
"createTitle": "Redactar un nuevo tema",
"createIntro": "Primero pon el título, luego escribe la publicación y después publícala directamente.",
"cancelCreation": "Cancelar",
"creationHint": "El título y el contenido deben estar completos.",
"communityFallback": "Comunidad",
"topicIntro": "Debates, respuestas y nuevas publicaciones en una vista de lectura enfocada.",
"topicCreated": "Tema creado correctamente.",
"topicCreateError": "Error al crear el tema",
"title": "Forum",
"showNewTopic": "Crear nuevo tema",
"hideNewTopic": "Cancelar creación",
@@ -277,9 +302,32 @@
"videoUploadHint": "Sube aquí vídeos para tu área erótica desbloqueada y completa título y descripción directamente durante la subida.",
"videoDescription": "Descripción",
"videoFile": "Archivo de vídeo",
"videoFormats": "MP4, WEBM, OGG, MOV",
"myVideos": "Mis vídeos",
"sharedVideos": "Vídeos compartidos",
"foreignVideosIntro": "Vídeos compartidos del área para adultos.",
"foreignVideosOnlyHint": "Aquí solo ves vídeos que han sido compartidos contigo para el área para adultos.",
"sharedVideosIntro": "Vídeos visibles de áreas para adultos compartidas.",
"noSharedVideos": "Ahora mismo no hay vídeos compartidos disponibles para ti.",
"libraryTitle": "Biblioteca",
"libraryIntro": "Tus subidas, permisos y reportes en un solo lugar.",
"libraryEmptyHint": "Crea a la izquierda tu primer vídeo y luego administraciónalo aquí en la biblioteca.",
"latestUpload": "Última subida",
"visibleVideos": "Vídeos visibles",
"moderationCases": "Casos de moderación",
"notesTitle": "Notas",
"friendsVisibilityHint": "Los amigos solo ven el contenido si son mayores de edad y están autorizados para el área para adultos.",
"selectedUsersVisibilityHint": "Las personas autorizadas de forma específica también deben ser mayores de edad y estar autorizadas.",
"selectedUsersPlaceholder": "anna, bert, clara",
"imagePreviewAlt": "Vista previa de la imagen",
"imageLoadingAlt": "Cargando imagen",
"untitled": "Sin título",
"noUploadYet": "Aún no hay subida",
"closeEditing": "Cerrar edición",
"editVisibility": "Editar permisos",
"noVideos": "Todavía no has subido vídeos eróticos.",
"reportAction": "Denunciar",
"reportHint": "Usa {action} directamente en el elemento correspondiente si el contenido debe revisarse.",
"reportNote": "Nota breve para moderación",
"submitReport": "Enviar denuncia",
"reportSubmitted": "La denuncia fue enviada.",
@@ -323,6 +371,36 @@
"vocab": {
"title": "Entrenador de vocabulario",
"description": "Crea idiomas (o suscríbete) y compártelos con tus amigos.",
"heroEyebrow": "Aprendizaje de idiomas",
"summaryTotalLabel": "Idiomas en total",
"summaryTotalIntro": "Todas las áreas de idioma activas donde usas o administras contenido.",
"summaryOwnedLabel": "Áreas propias",
"summaryOwnedIntro": "Aquí creas por tu cuenta contenido, capítulos y material de aprendizaje.",
"summarySubscribedLabel": "Suscrito",
"summarySubscribedIntro": "Estas áreas están pensadas más para aprender y avanzar que para administrar.",
"taskCreateEyebrow": "Inicio rápido",
"taskCreateTitle": "Crear nuevo idioma",
"taskCreateIntro": "La mejor entrada si quieres estructurar y mantener el contenido tú mismo.",
"taskContinueEyebrow": "Seguir aprendiendo",
"taskContinueTitle": "Abrir cursos y capítulos",
"taskContinueIntro": "Entra directamente en rutas de aprendizaje ya existentes y sigue con los cursos disponibles.",
"ownedSectionTitle": "Idiomas propios",
"ownedSectionIntro": "Acceso directo a edición, capítulos y gestión de cursos.",
"ownedHint": "Administrar y mantener contenido",
"ownedEmpty": "Todavía no hay áreas de idioma propias.",
"subscribedSectionTitle": "Idiomas suscritos",
"subscribedSectionIntro": "Ideal para volver rápido al aprendizaje sin carga administrativa.",
"subscribedHint": "Aprender, practicar y ver el progreso",
"subscribedEmpty": "No hay idiomas suscritos disponibles.",
"languageHeroEyebrow": "Idioma",
"languageHeroIntro": "Capítulos, búsqueda y compartición de este idioma en un solo lugar.",
"newLanguageHeroEyebrow": "Entrenador de vocabulario",
"newLanguageHeroIntro": "Crea un nuevo idioma, genera un código para compartir y pasa directamente a la edición.",
"newLanguageNameHint": "Basta con un nombre corto y claro para empezar.",
"newLanguageNameValidation": "El nombre debe tener al menos 2 caracteres.",
"subscribeHeroEyebrow": "Entrenador de vocabulario",
"chapterHeroEyebrow": "Entrenador de vocabulario",
"chapterHeroIntro": "Explora el contenido del capítulo, mantén el vocabulario y pasa directamente a la práctica.",
"newLanguage": "Nuevo idioma",
"newLanguageTitle": "Crear nuevo idioma",
"languageName": "Nombre del idioma",
@@ -568,7 +646,91 @@
"languageAssistantPatternHint": "Concéntrate especialmente en este patrón",
"languageAssistantPresetPracticeStart": "Practiquemos un diálogo cotidiano corto para la lección \"{lesson}\". Hazme preguntas y espera mis respuestas.",
"languageAssistantPresetCorrectStart": "Quiero escribir mis propias frases para la lección \"{lesson}\". Corrige mis respuestas de forma breve y clara.",
"thisLesson": "esta lección"
"thisLesson": "esta lección",
"courseKicker": "Curso de aprendizaje",
"courseListKicker": "Cursos",
"courseListIntro": "Filtra cursos públicos y propios, encuentra el adecuado y continúa directamente.",
"courseShareCodePlaceholder": "p. ej. abc123def456",
"courseFlowEyebrow": "Flujo del día",
"courseFlowTitle": "La mejor continuación para hoy",
"courseFlowIntro": "El orden sigue el concepto: primero los repasos pendientes, luego el bloque actual, después la fase intensiva y al final la práctica libre.",
"courseFlowReviewStat": "Repaso pendiente: {count}",
"courseFlowBlockStat": "Bloque activo: {block}",
"courseFlowReviewTitle": "Repaso pendiente",
"courseFlowReviewDescription": "Lecciones ya completadas que deberían volver hoy.",
"courseFlowReviewEmpty": "Hoy no hay ninguna lección antigua marcada como repaso pendiente.",
"courseFlowBlockTitle": "Bloque actual",
"courseFlowBlockDescription": "Aquí está el siguiente progreso regular dentro del curso.",
"courseFlowBlockEmpty": "El bloque actual ya está terminado o ahora mismo no hay ninguna lección abierta del bloque.",
"courseFlowIntensiveTitle": "Fase intensiva pendiente",
"courseFlowIntensiveDescription": "Repaso concentrado cuando el bloque anterior ya está bastante asentado.",
"courseFlowIntensiveEmpty": "Ahora mismo no hay ninguna nueva fase intensiva desbloqueada.",
"courseFlowPracticeTitle": "Práctica libre",
"courseFlowPracticeDescription": "Lecciones completadas para repasar con calma fuera del camino obligatorio.",
"courseFlowPracticeEmpty": "En cuanto completes tus primeras lecciones, aparecerán aquí para práctica libre.",
"practiceInTrainer": "Practicar en el entrenador",
"lessonsCount": "{count} lecciones",
"lessonBlockLabel": "Bloque {number}",
"lessonIntensiveBadge": "Repaso intensivo",
"addLessonValidation": "Indica por completo el número, el título y el capítulo.",
"addLessonSuccess": "Lección creada correctamente.",
"addLessonError": "No se pudo añadir la lección.",
"createCourseError": "No se pudo crear el curso.",
"deleteLessonTitle": "Eliminar lección",
"deleteLessonSuccess": "Lección eliminada correctamente.",
"deleteLessonError": "No se pudo eliminar la lección.",
"enrollCourseError": "No se pudo inscribirse en el curso.",
"editLessonPending": "La edición individual de lecciones llegará después.",
"timeToday": "hoy",
"timeSinceOneDay": "desde hace 1 día",
"timeSinceDays": "desde hace {count} días",
"reviewDueNow": "vence ahora",
"reviewDueTomorrow": "vence mañana",
"reviewDueInDays": "vence en {count} días",
"reviewDueToday": "vence hoy",
"reviewDueSinceOneDay": "vence desde hace 1 día",
"reviewDueSinceDays": "vence desde hace {count} días",
"reviewStageDay1": "Día 1",
"reviewStageDay3": "Día 3",
"reviewStageDay7": "Día 7",
"reviewStageCompleted": "Repaso completado",
"phaseQuickstart": "Inicio rápido",
"phaseDailyLife": "Vida diaria",
"phaseStabilization": "Estabilización",
"phaseDefault": "Fase de aprendizaje",
"didacticModeCoreInput": "Contenido nuevo",
"didacticModeGuidedDialogue": "Diálogo guiado",
"didacticModeContrastTraining": "Entrenamiento por contraste",
"didacticModePatternDrill": "Entrenamiento de patrones",
"didacticModeRealLifeScenario": "Escenario cotidiano",
"didacticModeIntensiveReview": "Fase de repaso",
"didacticModeCheckpoint": "Checkpoint",
"didacticModeDefault": "Unidad de aprendizaje",
"didacticModeFocusDefault": "Foco de aprendizaje",
"lessonMetaFocus": "Enfoque",
"lessonMetaPhase": "Fase",
"lessonMetaNewUnits": "Nuevas unidades",
"lessonMetaReview": "Repaso",
"intensiveReviewTitle": "Fase de repaso intensivo",
"intensiveReviewIntro": "Esta lección da prioridad al repaso y la consolidación. El material nuevo se reduce de forma consciente para estabilizar los patrones ya conocidos.",
"reviewPriorityTitle": "El repaso se mezcla paso a paso",
"reviewPriorityIntro": "Primero el foco está en los términos nuevos de esta lección. Con tu progreso se van mezclando cada vez más vocablos anteriores.",
"exerciseLockTitle": "La prueba del capítulo sigue bloqueada",
"trainerStartWithReview": "Empieza con el vocabulario nuevo de esta lección. A medida que avances, el entrenador mezclará automáticamente repasos adecuados.",
"startLesson": "Empezar lección",
"trainerProgressNewContent": "Contenido nuevo: {current}/{target}",
"trainerProgressReview": "Repaso: {count}",
"trainerProgressMixShare": "Parte mezclada: {percent}%",
"unknownExerciseTypeNotice": "Este tipo de ejercicio todavía no se muestra de forma interactiva en la vista actual.",
"unknownExerciseTypeLabel": "Tipo: {type}",
"lessonReviewHeadlineDone": "Esta lección ya ha llegado a la fase de práctica libre.",
"lessonReviewHeadlineDue": "Esta ola de repaso está pendiente ahora mismo.",
"lessonReviewHeadlineScheduled": "Esta lección está prevista para la siguiente ola de repaso.",
"lessonReviewHintDone": "El ciclo de repaso de 1/3/7 días está completado. Ahora puedes seguir practicando esta lección libremente.",
"lessonReviewHintNextDue": "Próximo vencimiento: {due}.",
"reviewTimeNow": "ahora",
"reviewTimeTomorrow": "mañana",
"reviewTimeInDays": "en {count} días"
}
}
}

View File

@@ -51,12 +51,26 @@ function getBrowserLanguage() {
return 'en';
}
const SUPPORTED_UI_LOCALES = ['de', 'en', 'ceb', 'es'];
function getInitialAppLanguage() {
try {
const saved = localStorage.getItem('uiLanguage');
if (saved && SUPPORTED_UI_LOCALES.includes(saved)) {
return saved;
}
} catch (_) {
/* ignore */
}
return getBrowserLanguage();
}
const vuetify = createVuetify({
components,
directives,
});
store.dispatch('setLanguage', getBrowserLanguage());
store.dispatch('setLanguage', getInitialAppLanguage());
const app = createApp(App);

View File

@@ -30,6 +30,8 @@ function clearAuthStorage() {
});
}
const SUPPORTED_UI_LOCALES = ['de', 'en', 'ceb', 'es'];
function persistAuthStorage(user, rememberMe) {
const targetStorage = rememberMe ? localStorage : sessionStorage;
clearAuthStorage();
@@ -38,6 +40,15 @@ function persistAuthStorage(user, rememberMe) {
targetStorage.setItem('userid', user?.id || '');
}
function readPersistedUiLanguage() {
try {
const saved = localStorage.getItem('uiLanguage');
return saved && SUPPORTED_UI_LOCALES.includes(saved) ? saved : null;
} catch (_) {
return null;
}
}
const store = createStore({
state: {
isLoggedIn: getStoredValue('isLoggedIn') === 'true',
@@ -52,7 +63,10 @@ const store = createStore({
backendConnecting: false,
daemonConnecting: false,
language: (() => {
// Verwende die gleiche Logik wie in main.js
const persisted = readPersistedUiLanguage();
if (persisted) {
return persisted;
}
const browserLanguage = navigator.language || navigator.languages[0];
if (browserLanguage.startsWith('ceb') || browserLanguage.startsWith('bis')) {
@@ -99,6 +113,15 @@ const store = createStore({
state.user = user;
persistAuthStorage(user, rememberMe);
state.menuNeedsUpdate = true;
const langParam = user.param?.find((p) => p.name === 'language');
if (langParam?.value && SUPPORTED_UI_LOCALES.includes(langParam.value)) {
state.language = langParam.value;
try {
localStorage.setItem('uiLanguage', langParam.value);
} catch (_) {
/* ignore */
}
}
if (user.param.filter(param => ['birthdate', 'gender'].includes(param.name)).length < 2) {
router.push({ path: '/settings/personal' });
}
@@ -110,6 +133,11 @@ const store = createStore({
localStorage.removeItem('menu');
state.menuNeedsUpdate = false;
try {
localStorage.removeItem('uiLanguage');
} catch (_) {
/* ignore */
}
// Setze die Sprache auf die Browser-Sprache zurück
const browserLanguage = navigator.language || navigator.languages[0];
@@ -150,6 +178,11 @@ const store = createStore({
},
setLanguage(state, language) {
state.language = language;
try {
localStorage.setItem('uiLanguage', language);
} catch (_) {
/* ignore */
}
},
setMenu(state, menu) {
state.menu = menu;

View File

@@ -2,10 +2,10 @@
<div class="home-logged-in">
<section class="dashboard-hero surface-card">
<div class="dashboard-hero__copy">
<span class="dashboard-kicker">Dein Bereich</span>
<h1>Willkommen zurück!</h1>
<span class="dashboard-kicker">{{ $t('home.dashboard.kicker') }}</span>
<h1>{{ $t('home.dashboard.title') }}</h1>
<p class="dashboard-subtitle">
Dein persönlicher Einstieg in Community, Termine, Falukant und laufende Aktivitäten.
{{ $t('home.dashboard.subtitle') }}
</p>
</div>
<div class="dashboard-toolbar surface-card">
@@ -15,7 +15,7 @@
class="btn-edit"
@click="editMode = true"
>
Dashboard bearbeiten
{{ $t('home.dashboard.edit') }}
</button>
<template v-else>
<div class="widget-add-row">
@@ -24,7 +24,7 @@
class="widget-type-select"
@change="onSelectWidgetType"
>
<option value="">+ Widget hinzufügen </option>
<option value="">{{ $t('home.dashboard.addWidget') }}</option>
<option
v-for="wt in widgetTypeOptions"
:key="wt.id"
@@ -39,11 +39,11 @@
class="btn-add-again"
@click="addSameWidgetType"
>
Nochmal hinzufügen
{{ $t('home.dashboard.addAgain') }}
</button>
</div>
<button type="button" class="btn-done" @click="doneEditing">
Fertig
{{ $t('home.dashboard.done') }}
</button>
</template>
</div>
@@ -51,19 +51,19 @@
<section class="dashboard-overview">
<article class="overview-card surface-card">
<span class="overview-card__label">Aktive Widgets</span>
<span class="overview-card__label">{{ $t('home.dashboard.overview.activeWidgetsLabel') }}</span>
<strong>{{ widgets.length }}</strong>
<p>Dein Dashboard ist modular aufgebaut und kann jederzeit umsortiert werden.</p>
<p>{{ $t('home.dashboard.overview.activeWidgetsText') }}</p>
</article>
<article class="overview-card surface-card">
<span class="overview-card__label">Verfügbare Module</span>
<span class="overview-card__label">{{ $t('home.dashboard.overview.availableModulesLabel') }}</span>
<strong>{{ widgetTypeOptions.length }}</strong>
<p>Du kannst Community-, Kalender-, News- und Falukant-Module kombinieren.</p>
<p>{{ $t('home.dashboard.overview.availableModulesText') }}</p>
</article>
<article class="overview-card surface-card">
<span class="overview-card__label">Bearbeitungsmodus</span>
<strong>{{ editMode ? 'Aktiv' : 'Aus' }}</strong>
<p>{{ editMode ? 'Widgets können gerade ergänzt und angepasst werden.' : 'Inhalte bleiben fokussiert und ruhig lesbar.' }}</p>
<span class="overview-card__label">{{ $t('home.dashboard.overview.editModeLabel') }}</span>
<strong>{{ editMode ? $t('home.dashboard.overview.editModeActive') : $t('home.dashboard.overview.editModeInactive') }}</strong>
<p>{{ editMode ? $t('home.dashboard.overview.editModeActiveText') : $t('home.dashboard.overview.editModeInactiveText') }}</p>
</article>
</section>
@@ -85,8 +85,8 @@
>
<div class="dashboard-shell__header">
<div>
<h2>Deine Übersicht</h2>
<p>Widgets lassen sich verschieben und im Bearbeitungsmodus anpassen.</p>
<h2>{{ $t('home.dashboard.sectionTitle') }}</h2>
<p>{{ $t('home.dashboard.sectionIntro') }}</p>
</div>
</div>
<div
@@ -120,17 +120,17 @@
<input
v-model="w.title"
type="text"
placeholder="Titel"
:placeholder="$t('home.dashboard.widgetTitlePlaceholder')"
class="widget-edit-input"
/>
</div>
<button
type="button"
class="btn-remove"
title="Widget entfernen"
:title="$t('home.dashboard.removeWidget')"
@click="removeWidget(index)"
>
Entfernen
{{ $t('home.dashboard.remove') }}
</button>
</div>
</div>
@@ -139,7 +139,7 @@
</div>
<div v-if="widgets.length === 0 && !loading" class="dashboard-empty">
<p>Noch keine Widgets. Klicke auf „Dashboard bearbeiten“ und dann „+ Widget hinzufügen“.</p>
<p>{{ $t('home.dashboard.empty') }}</p>
</div>
<div class="actions">
@@ -164,7 +164,7 @@ export default {
computed: {
widgetTypeOptions() {
if (this.availableWidgets.length > 0) return this.availableWidgets;
return [{ id: 'default', label: 'Termine', endpoint: '/api/termine' }];
return [{ id: 'default', label: this.$t('home.dashboard.defaultAppointmentsWidget'), endpoint: '/api/termine' }];
}
},
data() {
@@ -185,6 +185,41 @@ export default {
this.loadAvailableWidgets();
},
methods: {
getLocalizedWidgetLabel(endpoint, fallbackLabel = '') {
const key = {
'/api/termine': 'home.dashboard.widgetLabels.appointments',
'/api/falukant/dashboard-widget': 'home.dashboard.widgetLabels.falukant',
'/api/news': 'home.dashboard.widgetLabels.news',
'/api/calendar/widget/birthdays': 'home.dashboard.widgetLabels.birthdays',
'/api/calendar/widget/upcoming': 'home.dashboard.widgetLabels.upcoming',
'/api/calendar/widget/mini': 'home.dashboard.widgetLabels.calendar'
}[endpoint];
return key ? this.$t(key) : fallbackLabel;
},
normalizeWidgetType(widgetType) {
return {
...widgetType,
label: this.getLocalizedWidgetLabel(widgetType?.endpoint, widgetType?.label || '')
};
},
normalizeWidgetConfig(widget) {
const localizedLabel = this.getLocalizedWidgetLabel(widget?.endpoint, '');
const title = String(widget?.title || '').trim();
const knownDefaultLabels = [
'Termine',
'Falukant',
'News',
'Geburtstage',
'Nächste Termine',
'Kalender'
];
return {
...widget,
title: !title || knownDefaultLabels.includes(title)
? (localizedLabel || title)
: title
};
},
/** Endpoint aus Widget-Typ (anhand gespeichertem endpoint gematcht), sonst w.endpoint. */
effectiveEndpoint(w) {
if (!w?.endpoint) return '';
@@ -204,7 +239,7 @@ export default {
async loadAvailableWidgets() {
try {
const { data } = await apiClient.get('/api/dashboard/widgets');
this.availableWidgets = Array.isArray(data) ? data : [];
this.availableWidgets = Array.isArray(data) ? data.map(widget => this.normalizeWidgetType(widget)) : [];
} catch (e) {
this.availableWidgets = [];
}
@@ -216,14 +251,14 @@ export default {
const { data } = await apiClient.get('/api/dashboard/config');
let list = Array.isArray(data?.widgets) ? [...data.widgets] : [];
if (list.length === 0) {
list = [{ id: generateId(), title: 'Termine', endpoint: '/api/termine' }];
list = [{ id: generateId(), title: this.$t('home.dashboard.defaultAppointmentsWidget'), endpoint: '/api/termine' }];
this.widgets = list;
await this.saveConfig();
} else {
this.widgets = list;
this.widgets = list.map(widget => this.normalizeWidgetConfig(widget));
}
} catch (e) {
this.loadError = e.response?.data?.error || e.message || 'Dashboard konnte nicht geladen werden.';
this.loadError = e.response?.data?.error || e.message || this.$t('home.dashboard.loadError');
} finally {
this.loading = false;
}
@@ -239,7 +274,7 @@ export default {
await apiClient.put('/api/dashboard/config', { widgets: payload });
} catch (e) {
console.error('Dashboard speichern fehlgeschlagen:', e);
this.saveError = e.response?.data?.error || e.message || 'Dashboard konnte nicht gespeichert werden.';
this.saveError = e.response?.data?.error || e.message || this.$t('home.dashboard.saveError');
}
},
addWidgetFromType(wt) {

View File

@@ -2,9 +2,9 @@
<div class="calendar-view">
<section class="calendar-hero surface-card">
<div>
<span class="calendar-kicker">Planung</span>
<span class="calendar-kicker">{{ $t('personal.calendar.kicker') }}</span>
<h2>{{ $t('personal.calendar.title') }}</h2>
<p>Termine, Geburtstage und eigene Einträge in einer strukturierten Übersicht.</p>
<p>{{ $t('personal.calendar.intro') }}</p>
</div>
</section>

View File

@@ -1,32 +1,37 @@
<template>
<section class="marketing-page">
<div class="hero">
<p class="eyebrow">Sprachen online lernen</p>
<h1>Der Vokabeltrainer auf YourPart kombiniert Lernen, Kurse und Übungen in einer Plattform.</h1>
<p class="eyebrow">{{ $t('home.vocabLanding.eyebrow') }}</p>
<h1>{{ $t('home.vocabLanding.title') }}</h1>
<p class="lead">
Arbeite mit interaktiven Lektionen, erweitere deinen Wortschatz und nutze strukturierte Inhalte für einen
motivierenden Lernfluss direkt im Browser.
{{ $t('home.vocabLanding.lead') }}
</p>
<router-link class="cta" to="/">Kostenlos starten</router-link>
<router-link class="cta" to="/">{{ $t('home.vocabLanding.cta') }}</router-link>
</div>
<div class="features">
<article>
<h2>Interaktive Kurse</h2>
<p>Kurse, Lektionen und Übungen helfen beim systematischen Aufbau neuer Sprachkenntnisse.</p>
<h2>{{ $t('home.vocabLanding.feature1Title') }}</h2>
<p>{{ $t('home.vocabLanding.feature1Text') }}</p>
</article>
<article>
<h2>Praxisorientiert</h2>
<p>Wortschatz, Grammatik und Wiederholung werden auf eine alltagstaugliche Lernroutine ausgerichtet.</p>
<h2>{{ $t('home.vocabLanding.feature2Title') }}</h2>
<p>{{ $t('home.vocabLanding.feature2Text') }}</p>
</article>
<article>
<h2>Teil einer Community</h2>
<p>Der Sprachbereich ist in eine größere Community-Plattform mit Blogs, Forum und Chat eingebettet.</p>
<h2>{{ $t('home.vocabLanding.feature3Title') }}</h2>
<p>{{ $t('home.vocabLanding.feature3Text') }}</p>
</article>
</div>
</section>
</template>
<script>
export default {
name: 'VocabLandingView'
};
</script>
<style scoped>
.marketing-page {
max-width: 1100px;

View File

@@ -228,6 +228,11 @@ export default {
display: grid;
gap: 18px;
max-width: 960px;
/* Flex-Kind von .app-content__inner: ohne overflow wird Inhalt bei min-height:0 abgeschnitten */
min-height: 0;
overflow-y: auto;
align-content: start;
-webkit-overflow-scrolling: touch;
}
.account-settings__hero,

View File

@@ -2,15 +2,15 @@
<div class="diary-view">
<section class="diary-hero surface-card">
<div>
<span class="diary-kicker">Persönliche Einträge</span>
<span class="diary-kicker">{{ $t('socialnetwork.diary.kicker') }}</span>
<h2>{{ $t('socialnetwork.diary.title') }}</h2>
<p>Gedanken, Notizen und kurze Updates in einer ruhigen, persönlichen Ansicht.</p>
<p>{{ $t('socialnetwork.diary.intro') }}</p>
</div>
</section>
<section class="new-entry-section surface-card">
<h3>{{ isEditing ? $t('socialnetwork.diary.editEntry') : $t('socialnetwork.diary.newEntry') }}</h3>
<textarea v-model="newEntryText" placeholder="Write your diary entry..."></textarea>
<textarea v-model="newEntryText" :placeholder="$t('socialnetwork.diary.placeholder')"></textarea>
<div class="form-actions">
<button @click="saveEntry">{{ isEditing ? $t('socialnetwork.diary.update') : $t('socialnetwork.diary.save')
}}</button>

View File

@@ -45,7 +45,7 @@
<label for="imageFile">{{ $t('socialnetwork.gallery.upload.image_file') }}</label>
<input type="file" accept="image/*" required @change="onFileChange" />
<div v-if="imagePreview" class="image-preview">
<img :src="imagePreview" alt="Image Preview" style="max-width: 150px; max-height: 150px;" />
<img :src="imagePreview" :alt="$t('socialnetwork.erotic.imagePreviewAlt')" style="max-width: 150px; max-height: 150px;" />
</div>
</div>
@@ -79,7 +79,7 @@
id="selectedUsers"
v-model="selectedUsernamesText"
type="text"
placeholder="anna, bert, clara"
:placeholder="$t('socialnetwork.erotic.selectedUsersPlaceholder')"
/>
</div>
@@ -95,7 +95,7 @@
<ul v-if="images.length > 0" class="image-grid">
<li v-for="image in images" :key="image.id" class="erotic-image-card">
<div class="erotic-image-card__preview" @click="!image.isModeratedHidden && openImageDialog(image)">
<img v-if="!image.isModeratedHidden" :src="image.url || image.placeholder" alt="Loading..." />
<img v-if="!image.isModeratedHidden" :src="image.url || image.placeholder" :alt="$t('socialnetwork.erotic.imageLoadingAlt')" />
<div v-else class="erotic-image-card__hidden">
{{ $t('socialnetwork.erotic.hiddenByModeration') }}
</div>

View File

@@ -6,7 +6,7 @@
<div>
<span class="erotic-videos-eyebrow">{{ $t('socialnetwork.erotic.eyebrow') }}</span>
<h2>{{ isForeignView ? `${$t('socialnetwork.erotic.videosTitle')} · ${viewUsername}` : $t('socialnetwork.erotic.videosTitle') }}</h2>
<p>{{ isForeignView ? 'Freigegebene Videos aus dem Erwachsenenbereich.' : $t('socialnetwork.erotic.videosIntro') }}</p>
<p>{{ isForeignView ? $t('socialnetwork.erotic.foreignVideosIntro') : $t('socialnetwork.erotic.videosIntro') }}</p>
</div>
</section>
@@ -53,14 +53,14 @@
</label>
<label v-if="requiresSelectedUsers">
<span>{{ $t('socialnetwork.gallery.visibility.selected-users') }}</span>
<input v-model="selectedUsernamesText" type="text" placeholder="anna, bert, clara" />
<input v-model="selectedUsernamesText" type="text" :placeholder="$t('socialnetwork.erotic.selectedUsersPlaceholder')" />
</label>
<label>
<span>{{ $t('socialnetwork.erotic.videoFile') }}</span>
<input type="file" accept="video/mp4,video/webm,video/ogg,video/quicktime" required @change="onFileChange" />
</label>
<div class="erotic-videos-upload__meta">
<span>MP4, WEBM, OGG, MOV</span>
<span>{{ $t('socialnetwork.erotic.videoFormats') }}</span>
<span v-if="fileToUpload">{{ fileToUpload.name }}</span>
</div>
<button type="submit">{{ $t('socialnetwork.gallery.upload.upload_button') }}</button>
@@ -68,34 +68,34 @@
</section>
<section class="erotic-videos-panel surface-card">
<h3>Bibliothek</h3>
<h3>{{ $t('socialnetwork.erotic.libraryTitle') }}</h3>
<div class="erotic-videos-panel__list">
<div class="erotic-videos-panel__item">
<span>{{ isForeignView ? 'Freigegebene Videos' : $t('socialnetwork.erotic.myVideos') }}</span>
<span>{{ isForeignView ? $t('socialnetwork.erotic.sharedVideos') : $t('socialnetwork.erotic.myVideos') }}</span>
<strong>{{ videos.length }}</strong>
</div>
<div class="erotic-videos-panel__item">
<span>Letzter Upload</span>
<span>{{ $t('socialnetwork.erotic.latestUpload') }}</span>
<strong>{{ latestVideoTitle }}</strong>
</div>
<div class="erotic-videos-panel__item">
<span>Sichtbare Videos</span>
<span>{{ $t('socialnetwork.erotic.visibleVideos') }}</span>
<strong>{{ visibleVideosCount }}</strong>
</div>
<div class="erotic-videos-panel__item">
<span>Moderationsfälle</span>
<span>{{ $t('socialnetwork.erotic.moderationCases') }}</span>
<strong>{{ hiddenVideosCount }}</strong>
</div>
</div>
</section>
<section class="erotic-videos-panel surface-card">
<h3>Hinweise</h3>
<h3>{{ $t('socialnetwork.erotic.notesTitle') }}</h3>
<ul class="erotic-videos-checklist">
<li>{{ isForeignView ? 'Du siehst hier nur Videos, die dir für den Erwachsenenbereich freigegeben wurden.' : $t('socialnetwork.erotic.videoUploadHint') }}</li>
<li>Freunde sehen Inhalte nur dann, wenn sie volljährig und für den Erwachsenenbereich freigeschaltet sind.</li>
<li>Gezielt freigegebene Personen müssen ebenfalls volljährig und freigeschaltet sein.</li>
<li>Nutze {{ $t('socialnetwork.erotic.reportAction') }} direkt am jeweiligen Eintrag, wenn Inhalte geprüft werden sollen.</li>
<li>{{ isForeignView ? $t('socialnetwork.erotic.foreignVideosOnlyHint') : $t('socialnetwork.erotic.videoUploadHint') }}</li>
<li>{{ $t('socialnetwork.erotic.friendsVisibilityHint') }}</li>
<li>{{ $t('socialnetwork.erotic.selectedUsersVisibilityHint') }}</li>
<li>{{ $t('socialnetwork.erotic.reportHint', { action: $t('socialnetwork.erotic.reportAction') }) }}</li>
</ul>
</section>
</aside>
@@ -103,15 +103,15 @@
<section class="erotic-videos-library surface-card">
<div class="erotic-videos-library__header">
<div>
<h3>{{ isForeignView ? 'Freigegebene Videos' : $t('socialnetwork.erotic.myVideos') }}</h3>
<p>{{ isForeignView ? 'Sichtbare Videos aus freigegebenen Erwachsenenbereichen.' : 'Eigene Uploads, Freigaben und Meldungen an einem Ort.' }}</p>
<h3>{{ isForeignView ? $t('socialnetwork.erotic.sharedVideos') : $t('socialnetwork.erotic.myVideos') }}</h3>
<p>{{ isForeignView ? $t('socialnetwork.erotic.sharedVideosIntro') : $t('socialnetwork.erotic.libraryIntro') }}</p>
</div>
<span class="erotic-videos-library__count">{{ videos.length }}</span>
</div>
<div v-if="videos.length === 0" class="erotic-videos-empty">
<strong>{{ $t('socialnetwork.erotic.noVideos') }}</strong>
<span>{{ isForeignView ? 'Für dich sind aktuell keine freigegebenen Videos vorhanden.' : 'Lege links dein erstes Video an und verwalte es danach hier in der Bibliothek.' }}</span>
<span>{{ isForeignView ? $t('socialnetwork.erotic.noSharedVideos') : $t('socialnetwork.erotic.libraryEmptyHint') }}</span>
</div>
<div v-else class="erotic-videos-library__scroll">
@@ -122,7 +122,7 @@
{{ $t('socialnetwork.erotic.hiddenByModeration') }}
</div>
<div class="erotic-videos-card__meta">
<strong>{{ video.title || 'Ohne Titel' }}</strong>
<strong>{{ video.title || $t('socialnetwork.erotic.untitled') }}</strong>
<span v-if="video.createdAtLabel">{{ video.createdAtLabel }}</span>
</div>
<div v-if="video.visibilities?.length" class="erotic-videos-card__visibility">
@@ -136,7 +136,7 @@
<p v-if="video.description">{{ video.description }}</p>
<div v-if="!isForeignView" class="erotic-videos-card__edit">
<button type="button" class="secondary" @click="toggleEditor(video.id)">
{{ editingVideoId === video.id ? 'Bearbeitung schließen' : 'Freigaben bearbeiten' }}
{{ editingVideoId === video.id ? $t('socialnetwork.erotic.closeEditing') : $t('socialnetwork.erotic.editVisibility') }}
</button>
<div v-if="editingVideoId === video.id" class="erotic-videos-editor">
<label>
@@ -173,10 +173,10 @@
</label>
<label v-if="editRequiresSelectedUsers">
<span>{{ $t('socialnetwork.gallery.visibility.selected-users') }}</span>
<input v-model="editForm.selectedUsernamesText" type="text" placeholder="anna, bert, clara" />
<input v-model="editForm.selectedUsernamesText" type="text" :placeholder="$t('socialnetwork.erotic.selectedUsersPlaceholder')" />
</label>
<div class="erotic-videos-editor__actions">
<button type="button" @click="saveVideo(video.id)">Speichern</button>
<button type="button" @click="saveVideo(video.id)">{{ $t('common.save') }}</button>
</div>
</div>
</div>
@@ -265,7 +265,7 @@ export default {
return this.videos.filter((video) => video.isModeratedHidden).length;
},
latestVideoTitle() {
return this.videos[0]?.title || 'Noch kein Upload';
return this.videos[0]?.title || this.$t('socialnetwork.erotic.noUploadYet');
}
},
async mounted() {

View File

@@ -4,7 +4,7 @@
<div>
<div class="forum-topic-back link" @click="openForum()">{{ $t('socialnetwork.forum.title') }} {{ forumName }}</div>
<h2 v-if="forumTopic">{{ forumTopic }}</h2>
<p>Diskussionen, Antworten und neue Beiträge in einer fokussierten Lesefläche.</p>
<p>{{ $t('socialnetwork.forum.topicIntro') }}</p>
</div>
</section>

View File

@@ -2,9 +2,9 @@
<div class="forum-view">
<section class="forum-hero surface-card">
<div>
<span class="forum-kicker">Community-Forum</span>
<span class="forum-kicker">{{ $t('socialnetwork.forum.kicker') }}</span>
<h2>{{ $t('socialnetwork.forum.title') }} {{ forumName }}</h2>
<p>Themen, Diskussionen und neue Beiträge an einem strukturierten Ort.</p>
<p>{{ $t('socialnetwork.forum.intro') }}</p>
</div>
<div class="creationtoggler">
<button @click="toggleCreation">
@@ -18,10 +18,10 @@
<section v-if="inCreation" class="forum-creation surface-card">
<div class="forum-creation__header">
<div>
<h3>Neues Thema verfassen</h3>
<p>Erst Titel setzen, dann den Beitrag schreiben und anschließend direkt veröffentlichen.</p>
<h3>{{ $t('socialnetwork.forum.createTitle') }}</h3>
<p>{{ $t('socialnetwork.forum.createIntro') }}</p>
</div>
<button type="button" class="button-secondary" @click="cancelCreation">Abbrechen</button>
<button type="button" class="button-secondary" @click="cancelCreation">{{ $t('socialnetwork.forum.cancelCreation') }}</button>
</div>
<label class="newtitle">
<span>{{ $t('socialnetwork.forum.topic') }}</span>
@@ -34,7 +34,7 @@
<button :disabled="!canSaveTopic" @click="saveNewTopic">
{{ $t('socialnetwork.forum.createNewTopic') }}
</button>
<span class="forum-creation__hint">Titel und Inhalt müssen beide ausgefüllt sein.</span>
<span class="forum-creation__hint">{{ $t('socialnetwork.forum.creationHint') }}</span>
</div>
</section>
@@ -44,7 +44,7 @@
<button type="button" class="topic-card__main" @click="openTopic(topic.id)">
<strong>{{ topic.title }}</strong>
<span class="topic-card__meta">
{{ topic.user?.username || topic.owner?.username || 'Community' }}
{{ topic.user?.username || topic.owner?.username || $t('socialnetwork.forum.communityFallback') }}
</span>
</button>
</li>
@@ -175,10 +175,10 @@ export default {
this.inCreation = false
this.newTitle = ''
this.editor?.commands.setContent('')
showSuccess(this, 'Thema erfolgreich erstellt.')
showSuccess(this, this.$t('socialnetwork.forum.topicCreated'))
} catch (err) {
console.error('Fehler beim Erstellen des Themas', err)
showApiError(this, err, 'Fehler beim Erstellen des Themas')
showApiError(this, err, this.$t('socialnetwork.forum.topicCreateError'))
}
},
goToPage(page) {

View File

@@ -2,18 +2,18 @@
<div class="friends-view">
<section class="friends-hero surface-card">
<div>
<span class="friends-kicker">Community</span>
<span class="friends-kicker">{{ $t('friends.kicker') }}</span>
<h2>{{ $t('friends.title') }}</h2>
<p>Freundschaften, offene Anfragen und laufende Kontakte an einem Ort.</p>
<p>{{ $t('friends.intro') }}</p>
</div>
<div class="friends-stats">
<div class="friends-stat surface-card">
<strong>{{ tabs[0].data.length }}</strong>
<span>Bestehend</span>
<span>{{ $t('friends.stats.existing') }}</span>
</div>
<div class="friends-stat surface-card">
<strong>{{ tabs[1].data.length + tabs[2].data.length }}</strong>
<span>Offen</span>
<span>{{ $t('friends.stats.open') }}</span>
</div>
</div>
</section>

View File

@@ -2,9 +2,9 @@
<div class="gallery-page">
<section class="gallery-hero surface-card">
<div>
<span class="gallery-kicker">Bilder und Ordner</span>
<span class="gallery-kicker">{{ $t('socialnetwork.gallery.kicker') }}</span>
<h2>{{ $t('socialnetwork.gallery.title') }}</h2>
<p>Eigene Inhalte organisieren, sichtbar machen und in Ordnern strukturieren.</p>
<p>{{ $t('socialnetwork.gallery.intro') }}</p>
</div>
</section>
<div class="gallery-view">
@@ -39,7 +39,7 @@
<label for="imageFile">{{ $t('socialnetwork.gallery.upload.image_file') }}</label>
<input type="file" @change="onFileChange" accept="image/*" required />
<div v-if="imagePreview" class="image-preview">
<img :src="imagePreview" alt="Image Preview"
<img :src="imagePreview" :alt="$t('socialnetwork.gallery.imagePreviewAlt')"
style="max-width: 150px; max-height: 150px;" />
</div>
</div>
@@ -74,7 +74,7 @@
<h3>{{ $t('socialnetwork.gallery.images') }}</h3>
<ul v-if="images.length > 0" class="image-grid">
<li v-for="image in images" :key="image.id" @click="openImageDialog(image)">
<img :src="image.url || image.placeholder" alt="Loading..." />
<img :src="image.url || image.placeholder" :alt="$t('socialnetwork.gallery.imageLoadingAlt')" />
<p>{{ image.title }}</p>
</li>
</ul>

View File

@@ -2,9 +2,9 @@
<div class="guestbook-view">
<section class="guestbook-hero surface-card">
<div>
<span class="guestbook-kicker">Gästebuch</span>
<span class="guestbook-kicker">{{ $t('socialnetwork.guestbook.kicker') }}</span>
<h2>{{ $t('socialnetwork.guestbook.title') }}</h2>
<p>Nachrichten, Rückmeldungen und kleine Einblicke aus deinem Netzwerk.</p>
<p>{{ $t('socialnetwork.guestbook.intro') }}</p>
</div>
</section>
<div v-if="guestbookEntries.length === 0" class="guestbook-empty surface-card">{{ $t('socialnetwork.profile.guestbook.noEntries') }}

View File

@@ -2,9 +2,9 @@
<div class="search-view">
<section class="search-hero surface-card">
<div>
<span class="search-kicker">Community-Suche</span>
<span class="search-kicker">{{ $t('socialnetwork.usersearch.kicker') }}</span>
<h2>{{ $t('socialnetwork.usersearch.title') }}</h2>
<p>Mit Namen, Alter und Geschlecht gezielt passende Kontakte in der Community finden.</p>
<p>{{ $t('socialnetwork.usersearch.intro') }}</p>
</div>
</section>
@@ -22,7 +22,7 @@
<div class="age-range">
<input type="number" id="ageFrom" v-model="searchCriteria.ageFrom" :min="14" :max="150"
:placeholder="$t('socialnetwork.usersearch.age_from')" class="age-input" />
<span class="age-separator">bis</span>
<span class="age-separator">{{ $t('socialnetwork.usersearch.ageSeparator') }}</span>
<input type="number" id="ageTo" v-model="searchCriteria.ageTo" :min="14" :max="150"
:placeholder="$t('socialnetwork.usersearch.age_to')" class="age-input" />
</div>
@@ -45,7 +45,7 @@
<section class="search-results surface-card" v-if="searchResults.length">
<div class="results-header">
<h3>{{ $t('socialnetwork.usersearch.results_title') }}</h3>
<span class="results-count">{{ searchResults.length }} Treffer</span>
<span class="results-count">{{ $t('socialnetwork.usersearch.resultsCount', { count: searchResults.length }) }}</span>
</div>
<div class="result-cards">
<article v-for="result in searchResults" :key="result.id" class="result-card">
@@ -57,7 +57,7 @@
</div>
</div>
<button type="button" class="button-secondary" @click="openUserProfile(result.id)">
Profil öffnen
{{ $t('socialnetwork.usersearch.openProfile') }}
</button>
</article>
</div>

View File

@@ -1,9 +1,9 @@
<template>
<div class="vocab-chapter-view">
<section class="vocab-chapter-hero surface-card">
<span class="vocab-chapter-hero__eyebrow">Vokabeltrainer</span>
<span class="vocab-chapter-hero__eyebrow">{{ $t('socialnetwork.vocab.chapterHeroEyebrow') }}</span>
<h2>{{ $t('socialnetwork.vocab.chapterTitle', { title: chapter?.title || '' }) }}</h2>
<p>Kapitelinhalt durchsuchen, Vokabeln pflegen und direkt in die Übung wechseln.</p>
<p>{{ $t('socialnetwork.vocab.chapterHeroIntro') }}</p>
</section>
<section class="box surface-card">

View File

@@ -2,9 +2,9 @@
<div class="vocab-course-list">
<section class="vocab-courses-hero surface-card">
<div>
<span class="vocab-courses-kicker">Kurse</span>
<span class="vocab-courses-kicker">{{ $t('socialnetwork.vocab.courses.courseListKicker') }}</span>
<h2>{{ $t('socialnetwork.vocab.courses.title') }}</h2>
<p>Öffentliche und eigene Lernkurse filtern, finden und direkt weiterlernen.</p>
<p>{{ $t('socialnetwork.vocab.courses.courseListIntro') }}</p>
</div>
</section>
@@ -88,7 +88,7 @@
<form @submit.prevent="findCourseByCode">
<div class="form-group">
<label>{{ $t('socialnetwork.vocab.courses.shareCode') }}</label>
<input v-model="shareCode" placeholder="z.B. abc123def456" required />
<input v-model="shareCode" :placeholder="$t('socialnetwork.vocab.courses.courseShareCodePlaceholder')" required />
</div>
<div class="form-actions">
<button type="submit">{{ $t('general.search') }}</button>
@@ -329,7 +329,7 @@ export default {
await this.loadAllCourses();
} catch (e) {
console.error('Fehler beim Erstellen des Kurses:', e);
showApiError(this, e, 'Fehler beim Erstellen des Kurses');
showApiError(this, e, this.$t('socialnetwork.vocab.courses.createCourseError'));
}
},
async enroll(courseId) {
@@ -339,7 +339,7 @@ export default {
this.openCourse(courseId);
} catch (e) {
console.error('Fehler beim Einschreiben:', e);
showApiError(this, e, 'Fehler beim Einschreiben');
showApiError(this, e, this.$t('socialnetwork.vocab.courses.enrollCourseError'));
}
},
openCourse(courseId) {

View File

@@ -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() {

View File

@@ -4,9 +4,9 @@
<div v-if="loading">{{ $t('general.loading') }}</div>
<div v-else-if="!language">{{ $t('socialnetwork.vocab.notFound') }}</div>
<div v-else>
<span class="vocab-language-kicker">Sprache</span>
<span class="vocab-language-kicker">{{ $t('socialnetwork.vocab.languageHeroEyebrow') }}</span>
<h2>{{ $t('socialnetwork.vocab.languageTitle', { name: language?.name || '' }) }}</h2>
<p>Kapitel, Suchfunktionen und Freigaben für diese Sprache an einem Ort.</p>
<p>{{ $t('socialnetwork.vocab.languageHeroIntro') }}</p>
</div>
</section>

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;

View File

@@ -1,17 +1,17 @@
<template>
<div class="vocab-new-language-view">
<section class="vocab-new-language-hero surface-card">
<span class="vocab-new-language-hero__eyebrow">Vokabeltrainer</span>
<span class="vocab-new-language-hero__eyebrow">{{ $t('socialnetwork.vocab.newLanguageHeroEyebrow') }}</span>
<h2>{{ $t('socialnetwork.vocab.newLanguageTitle') }}</h2>
<p>Neue Sprache anlegen, Freigabecode erzeugen und direkt in die Bearbeitung wechseln.</p>
<p>{{ $t('socialnetwork.vocab.newLanguageHeroIntro') }}</p>
</section>
<section class="box surface-card">
<label class="label form-field">
<span>{{ $t('socialnetwork.vocab.languageName') }}</span>
<input v-model="name" type="text" :class="{ 'field-error': nameTouched && !canSave }" />
<span class="form-hint">Ein kurzer, klarer Sprachname reicht für den Start.</span>
<span v-if="nameTouched && !canSave" class="form-error">Der Name sollte mindestens 2 Zeichen haben.</span>
<span class="form-hint">{{ $t('socialnetwork.vocab.newLanguageNameHint') }}</span>
<span v-if="nameTouched && !canSave" class="form-error">{{ $t('socialnetwork.vocab.newLanguageNameValidation') }}</span>
</label>
<div class="actions form-actions-row">

View File

@@ -1,7 +1,7 @@
<template>
<div class="vocab-subscribe-view">
<section class="vocab-subscribe-hero surface-card">
<span class="vocab-subscribe-hero__eyebrow">Vokabeltrainer</span>
<span class="vocab-subscribe-hero__eyebrow">{{ $t('socialnetwork.vocab.subscribeHeroEyebrow') }}</span>
<h2>{{ $t('socialnetwork.vocab.subscribeTitle') }}</h2>
<p>{{ $t('socialnetwork.vocab.subscribeHint') }}</p>
</section>

View File

@@ -2,7 +2,7 @@
<div class="vocab-view">
<section class="vocab-hero surface-card">
<div>
<span class="vocab-kicker">Sprachenlernen</span>
<span class="vocab-kicker">{{ $t('socialnetwork.vocab.heroEyebrow') }}</span>
<h2>{{ $t('socialnetwork.vocab.title') }}</h2>
<p>{{ $t('socialnetwork.vocab.description') }}</p>
</div>
@@ -14,33 +14,33 @@
<section class="vocab-summary-grid">
<article class="summary-card surface-card">
<span class="summary-card__label">Sprachen gesamt</span>
<span class="summary-card__label">{{ $t('socialnetwork.vocab.summaryTotalLabel') }}</span>
<strong>{{ languages.length }}</strong>
<p>Alle aktiven Sprachbereiche, in denen du Inhalte nutzt oder verwaltest.</p>
<p>{{ $t('socialnetwork.vocab.summaryTotalIntro') }}</p>
</article>
<article class="summary-card surface-card">
<span class="summary-card__label">Eigene Bereiche</span>
<span class="summary-card__label">{{ $t('socialnetwork.vocab.summaryOwnedLabel') }}</span>
<strong>{{ ownedLanguages.length }}</strong>
<p>Hier legst du Inhalte, Kapitel und Lernmaterial aktiv selbst an.</p>
<p>{{ $t('socialnetwork.vocab.summaryOwnedIntro') }}</p>
</article>
<article class="summary-card surface-card">
<span class="summary-card__label">Abonniert</span>
<span class="summary-card__label">{{ $t('socialnetwork.vocab.summarySubscribedLabel') }}</span>
<strong>{{ subscribedLanguages.length }}</strong>
<p>Diese Bereiche sind eher für Lernen und Fortschritt statt Verwaltung gedacht.</p>
<p>{{ $t('socialnetwork.vocab.summarySubscribedIntro') }}</p>
</article>
</section>
<section class="vocab-task-grid">
<article class="task-card surface-card">
<span class="task-card__eyebrow">Schnellstart</span>
<h3>Neue Sprache anlegen</h3>
<p>Der beste Einstieg, wenn du Inhalte selbst strukturieren und pflegen willst.</p>
<span class="task-card__eyebrow">{{ $t('socialnetwork.vocab.taskCreateEyebrow') }}</span>
<h3>{{ $t('socialnetwork.vocab.taskCreateTitle') }}</h3>
<p>{{ $t('socialnetwork.vocab.taskCreateIntro') }}</p>
<button type="button" @click="goNewLanguage">{{ $t('socialnetwork.vocab.newLanguage') }}</button>
</article>
<article class="task-card surface-card">
<span class="task-card__eyebrow">Weiterlernen</span>
<h3>Kurse und Kapitel öffnen</h3>
<p>Springe direkt in bestehende Lernpfade und arbeite mit vorhandenen Kursen weiter.</p>
<span class="task-card__eyebrow">{{ $t('socialnetwork.vocab.taskContinueEyebrow') }}</span>
<h3>{{ $t('socialnetwork.vocab.taskContinueTitle') }}</h3>
<p>{{ $t('socialnetwork.vocab.taskContinueIntro') }}</p>
<button type="button" class="button-secondary" @click="goCourses">{{ $t('socialnetwork.vocab.courses.title') }}</button>
</article>
</section>
@@ -54,8 +54,8 @@
<section class="language-section">
<div class="language-section__header">
<div>
<h3>Eigene Sprachen</h3>
<p>Direkter Einstieg in Bearbeitung, Kapitel und Kursverwaltung.</p>
<h3>{{ $t('socialnetwork.vocab.ownedSectionTitle') }}</h3>
<p>{{ $t('socialnetwork.vocab.ownedSectionIntro') }}</p>
</div>
<span class="language-section__count">{{ ownedLanguages.length }}</span>
</div>
@@ -64,20 +64,20 @@
<button type="button" class="language-card__main" @click="openLanguage(l.id)">
<div class="language-card__info">
<span class="langname">{{ l.name }}</span>
<span class="language-card__hint">Verwalten und Inhalte pflegen</span>
<span class="language-card__hint">{{ $t('socialnetwork.vocab.ownedHint') }}</span>
</div>
<span class="role">{{ $t('socialnetwork.vocab.owner') }}</span>
</button>
</li>
</ul>
<p v-else class="language-empty">Noch keine eigenen Sprachbereiche vorhanden.</p>
<p v-else class="language-empty">{{ $t('socialnetwork.vocab.ownedEmpty') }}</p>
</section>
<section class="language-section">
<div class="language-section__header">
<div>
<h3>Abonnierte Sprachen</h3>
<p>Gut für schnellen Wiedereinstieg ins Lernen ohne Verwaltungsaufwand.</p>
<h3>{{ $t('socialnetwork.vocab.subscribedSectionTitle') }}</h3>
<p>{{ $t('socialnetwork.vocab.subscribedSectionIntro') }}</p>
</div>
<span class="language-section__count">{{ subscribedLanguages.length }}</span>
</div>
@@ -86,13 +86,13 @@
<button type="button" class="language-card__main" @click="openLanguage(l.id)">
<div class="language-card__info">
<span class="langname">{{ l.name }}</span>
<span class="language-card__hint">Lernen, üben und Fortschritt ansehen</span>
<span class="language-card__hint">{{ $t('socialnetwork.vocab.subscribedHint') }}</span>
</div>
<span class="role">{{ $t('socialnetwork.vocab.subscribed') }}</span>
</button>
</li>
</ul>
<p v-else class="language-empty">Keine abonnierten Sprachen vorhanden.</p>
<p v-else class="language-empty">{{ $t('socialnetwork.vocab.subscribedEmpty') }}</p>
</section>
</div>
</section>