feat(i18n): add French language support and enhance localization
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s

- Introduced French as a supported language across the application, updating locale files and adding translations for various components.
- Enhanced language handling logic to accommodate French, ensuring proper detection and fallback mechanisms.
- Updated UI elements to include French language options, improving accessibility for French-speaking users.
- Refactored SEO handling to include French in hreflang links, enhancing search engine indexing for multilingual content.
- Added new scripts for managing French translations and ensuring consistency across language files.
This commit is contained in:
Torsten Schulz (local)
2026-04-07 18:04:03 +02:00
parent f715c6125d
commit f7030bbabe
56 changed files with 5220 additions and 175 deletions

View File

@@ -4,9 +4,9 @@
<div class="falukant-branch">
<section class="branch-hero surface-card">
<div>
<span class="branch-kicker">Niederlassung</span>
<span class="branch-kicker">{{ $t('falukant.branch.heroEyebrow') }}</span>
<h2>{{ $t('falukant.branch.title') }}</h2>
<p>Produktion, Lager, Verkauf und Transport in einer spielweltbezogenen Steuerfläche.</p>
<p>{{ $t('falukant.branch.heroIntro') }}</p>
<div class="branch-hero__meta">
<span class="branch-hero__badge">
{{ $t('falukant.branch.currentCertificate') }}: {{ currentCertificate ?? '---' }}
@@ -880,14 +880,14 @@ export default {
conditionLabel(value) {
const v = Number(value) || 0;
if (v >= 95) return 'Ausgezeichnet'; // 95100
if (v >= 72) return 'Sehr gut'; // 7294
if (v >= 54) return 'Gut'; // 5471
if (v >= 39) return 'Mäßig'; // 3953
if (v >= 22) return 'Schlecht'; // 2238
if (v >= 6) return 'Sehr schlecht'; // 621
if (v >= 1) return 'Katastrophal'; // 15
return 'Unbekannt';
if (v >= 95) return this.$t('falukant.conditionBand.excellent');
if (v >= 72) return this.$t('falukant.conditionBand.veryGood');
if (v >= 54) return this.$t('falukant.conditionBand.good');
if (v >= 39) return this.$t('falukant.conditionBand.moderate');
if (v >= 22) return this.$t('falukant.conditionBand.bad');
if (v >= 6) return this.$t('falukant.conditionBand.veryBad');
if (v >= 1) return this.$t('falukant.conditionBand.catastrophic');
return this.$t('falukant.conditionBand.unknown');
},
speedLabel(value) {

View File

@@ -5,7 +5,7 @@
<div class="family-content">
<section class="family-hero surface-card">
<div>
<span class="family-kicker">Familie</span>
<span class="family-kicker">{{ $t('falukant.family.title') }}</span>
<h2>{{ $t('falukant.family.title') }}</h2>
<p>{{ $t('falukant.family.heroIntro') }}</p>
</div>

View File

@@ -213,14 +213,14 @@ export default {
},
conditionLabel(value) {
const v = Number(value) || 0;
if (v >= 95) return 'Ausgezeichnet'; // 95100
if (v >= 72) return 'Sehr gut'; // 7294
if (v >= 54) return 'Gut'; // 5471
if (v >= 39) return 'Mäßig'; // 3953
if (v >= 22) return 'Schlecht'; // 2238
if (v >= 6) return 'Sehr schlecht'; // 621
if (v >= 1) return 'Katastrophal'; // 15
return 'Unbekannt';
if (v >= 95) return this.$t('falukant.conditionBand.excellent');
if (v >= 72) return this.$t('falukant.conditionBand.veryGood');
if (v >= 54) return this.$t('falukant.conditionBand.good');
if (v >= 39) return this.$t('falukant.conditionBand.moderate');
if (v >= 22) return this.$t('falukant.conditionBand.bad');
if (v >= 6) return this.$t('falukant.conditionBand.veryBad');
if (v >= 1) return this.$t('falukant.conditionBand.catastrophic');
return this.$t('falukant.conditionBand.unknown');
},
houseStyle(position, picSize) {
const columns = 3;

View File

@@ -45,7 +45,7 @@
</p>
</div>
<div v-else class="advance-section">
<p style="color: red;">Fehler: Keine nächste Titel-Information verfügbar. Bitte Seite neu laden.</p>
<p style="color: red;">{{ $t('falukant.nobility.advanceNoNext') }}</p>
</div>
</div>
@@ -149,10 +149,10 @@
const base = this.$t('falukant.nobility.errors.unmet');
this.$root.$refs.errorDialog?.open(`${base}\n${items}`);
} else {
this.$root.$refs.errorDialog?.open('tr:falukant.nobility.errors.generic');
this.$root.$refs.errorDialog?.open(this.$t('falukant.nobility.errors.generic'));
}
} else {
this.$root.$refs.errorDialog?.open('tr:falukant.nobility.errors.generic');
this.$root.$refs.errorDialog?.open(this.$t('falukant.nobility.errors.generic'));
}
} finally {
this.isAdvancing = false;
@@ -171,35 +171,20 @@
const amount = ['money', 'cost'].includes(type)
? this.formatCost(numericValue)
: rawValue;
if (type === 'house_position') {
const label = this.housePositionLabel(numericValue);
return this.$t('falukant.nobility.requirement.house_position', { label });
}
if (type === 'house_condition') {
const quality = this.formatHouseCondition(numericValue);
return this.$t('falukant.nobility.requirement.house_condition', { quality });
}
const key = `falukant.nobility.requirement.${type}`;
const translated = this.$t(key, { amount });
if (translated && translated !== key && !['house_position', 'house_condition'].includes(type)) {
if (translated && translated !== key) {
return translated;
}
switch (type) {
case 'money':
return `Vermögen mindestens ${amount}`;
case 'cost':
return `Kosten: ${amount}`;
case 'branches':
return `Mindestens ${amount} Niederlassungen`;
case 'reputation':
return `Beliebtheit mindestens ${amount}`;
case 'house_position':
return `Hausstand mindestens ${this.getHousePositionLabel(numericValue)}`;
case 'house_condition':
return `Hauszustand mindestens ${this.formatHouseCondition(numericValue)}`;
case 'office_rank_any':
return `Höchstes politisches oder kirchliches Amt mindestens Rang ${amount}`;
case 'office_rank_political':
return `Höchstes politisches Amt mindestens Rang ${amount}`;
case 'lover_count_min':
return `Mindestens ${amount} Liebhaber oder Mätressen`;
case 'lover_count_max':
return `Höchstens ${amount} Liebhaber oder Mätressen`;
default:
return `${type}: ${amount}`;
}
return this.$t('falukant.nobility.requirement.unknown', { type, amount });
},
formatOfficeInfo(info, source) {
if (!info?.name) {
@@ -207,32 +192,25 @@
}
const baseKey = source === 'church' ? 'falukant.church.offices' : 'falukant.politics.positions';
const label = this.$te(`${baseKey}.${info.name}`) ? this.$t(`${baseKey}.${info.name}`) : info.name;
return `${label} (Rang ${info.rank})`;
return this.$t('falukant.nobility.officeWithRank', { label, rank: info.rank });
},
getHousePositionLabel(position) {
const labels = {
1: 'Unter der Brücke',
2: 'eine Strohhütte',
3: 'ein Holzhaus',
4: 'ein Hinterhofzimmer',
5: 'ein kleines Familienhaus',
6: 'ein Stadthaus',
7: 'eine Villa',
8: 'ein Herrenhaus',
9: 'ein Schloss'
};
return labels[position] || `Haus-Stufe ${position}`;
housePositionLabel(position) {
const k = `falukant.nobility.housePosition.${position}`;
if (this.$te(k)) {
return this.$t(k);
}
return this.$t('falukant.nobility.housePosition.fallback', { level: position });
},
formatHouseCondition(value) {
if (Number.isNaN(value)) {
return value;
return String(value);
}
if (value >= 0.95) return 'nahezu makellos';
if (value >= 0.9) return 'sehr gut';
if (value >= 0.8) return 'gut';
if (value >= 0.7) return 'ordentlich';
if (value >= 0.6) return 'brauchbar';
return `${Math.round(value * 100)} %`;
if (value >= 0.95) return this.$t('falukant.nobility.houseConditionQuality.nearPerfect');
if (value >= 0.9) return this.$t('falukant.nobility.houseConditionQuality.veryGood');
if (value >= 0.8) return this.$t('falukant.nobility.houseConditionQuality.good');
if (value >= 0.7) return this.$t('falukant.nobility.houseConditionQuality.decent');
if (value >= 0.6) return this.$t('falukant.nobility.houseConditionQuality.usable');
return this.$t('falukant.nobility.houseConditionPercent', { pct: Math.round(value * 100) });
},
formatDate(isoString) {
const d = new Date(isoString);

View File

@@ -3,9 +3,9 @@
<StatusBar />
<section class="falukant-hero surface-card">
<div>
<span class="falukant-kicker">Falukant</span>
<span class="falukant-kicker">{{ $t('sectionBar.sections.falukant') }}</span>
<h2>{{ $t('falukant.overview.title') }}</h2>
<p>Dein Stand in Wirtschaft, Familie und Besitz in einer verdichteten Übersicht.</p>
<p>{{ $t('falukant.overview.heroIntro') }}</p>
</div>
</section>
@@ -54,22 +54,22 @@
<article class="summary-card surface-card">
<span class="summary-card__label">{{ $t('falukant.overview.metadata.certificate') }}</span>
<strong>{{ falukantUser?.certificate ?? '---' }}</strong>
<p>Bestimmt, welche Produktkategorien du derzeit herstellen darfst.</p>
<p>{{ $t('falukant.overview.summary.certificateHint') }}</p>
</article>
<article class="summary-card surface-card">
<span class="summary-card__label">Niederlassungen</span>
<span class="summary-card__label">{{ $t('falukant.overview.summary.branches') }}</span>
<strong>{{ branchCount }}</strong>
<p>Direkter Zugriff auf deine wichtigsten Geschäftsstandorte.</p>
<p>{{ $t('falukant.overview.summary.branchesHint') }}</p>
</article>
<article class="summary-card surface-card">
<span class="summary-card__label">Produktionen aktiv</span>
<span class="summary-card__label">{{ $t('falukant.overview.summary.productions') }}</span>
<strong>{{ productionCount }}</strong>
<p>Laufende Produktionen, die zeitnah Abschluss oder Kontrolle brauchen.</p>
<p>{{ $t('falukant.overview.summary.productionsHint') }}</p>
</article>
<article class="summary-card surface-card">
<span class="summary-card__label">Lagerpositionen</span>
<span class="summary-card__label">{{ $t('falukant.overview.summary.stock') }}</span>
<strong>{{ stockEntryCount }}</strong>
<p>Verdichteter Blick auf Warenbestand über alle Regionen.</p>
<p>{{ $t('falukant.overview.summary.stockHint') }}</p>
</article>
<article v-if="falukantUser?.debtorsPrison?.active" class="summary-card surface-card">
<span class="summary-card__label">{{ $t('falukant.bank.debtorsPrison.creditworthiness') }}</span>
@@ -85,7 +85,7 @@
<section v-if="falukantUser?.character" class="falukant-routine-grid">
<article
v-for="action in routineActions"
:key="action.title"
:key="action.route"
class="routine-card surface-card"
>
<span class="routine-card__eyebrow">{{ action.kicker }}</span>
@@ -309,7 +309,7 @@
<span>{{ $t(`falukant.overview.branches.level.${branch.branchType.labelTr}`) }}</span>
</div>
</div>
<button type="button" class="button-secondary" @click="openBranch(branch.id)">Öffnen</button>
<button type="button" class="button-secondary" @click="openBranch(branch.id)">{{ $t('falukant.overview.summary.open') }}</button>
</article>
</div>
</section>
@@ -495,33 +495,33 @@ export default {
routineActions() {
return [
{
kicker: 'Routine',
title: 'Niederlassung öffnen',
description: 'Die schnellste Route zu Produktion, Lager, Verkauf und Transport.',
cta: 'Zu den Betrieben',
kicker: this.$t('falukant.overview.routine.branch.kicker'),
title: this.$t('falukant.overview.routine.branch.title'),
description: this.$t('falukant.overview.routine.branch.description'),
cta: this.$t('falukant.overview.routine.branch.cta'),
route: 'BranchView',
},
{
kicker: 'Überblick',
title: 'Finanzen prüfen',
description: 'Kontostand, Verlauf und wirtschaftliche Entwicklung ohne lange Suche.',
cta: 'Geldhistorie',
kicker: this.$t('falukant.overview.routine.finance.kicker'),
title: this.$t('falukant.overview.routine.finance.title'),
description: this.$t('falukant.overview.routine.finance.description'),
cta: this.$t('falukant.overview.routine.finance.cta'),
route: 'MoneyHistoryView',
secondary: true,
},
{
kicker: 'Charakter',
title: 'Familie und Nachfolge',
description: 'Wichtige persönliche Entscheidungen und Haushaltsstatus gesammelt.',
cta: 'Familie öffnen',
kicker: this.$t('falukant.overview.routine.family.kicker'),
title: this.$t('falukant.overview.routine.family.title'),
description: this.$t('falukant.overview.routine.family.description'),
cta: this.$t('falukant.overview.routine.family.cta'),
route: 'FalukantFamily',
secondary: true,
},
{
kicker: 'Besitz',
title: 'Haus und Umfeld',
description: 'Wohnsitz und alltäglicher Status als eigener Arbeitsbereich.',
cta: 'Zum Haus',
kicker: this.$t('falukant.overview.routine.house.kicker'),
title: this.$t('falukant.overview.routine.house.title'),
description: this.$t('falukant.overview.routine.house.description'),
cta: this.$t('falukant.overview.routine.house.cta'),
route: 'HouseView',
secondary: true,
},
@@ -774,7 +774,7 @@ export default {
await this.fetchAllStock();
await this.fetchProductions();
}
showSuccess(this, 'Erbe wurde übernommen.');
showSuccess(this, this.$t('falukant.overview.heirSelection.success'));
} catch (error) {
console.error('Error selecting heir:', error);
showError(this, this.$t('falukant.overview.heirSelection.error'));

View File

@@ -149,7 +149,7 @@
:value="e.id"
:disabled="e.alreadyApplied || e.canApplyByAge === false"
/>
<span>Für diese Kandidatur vormerken</span>
<span>{{ $t('falukant.politics.bookmarkCandidate') }}</span>
</label>
</article>
</div>
@@ -552,10 +552,10 @@ export default {
{ votes: singlePayload }
);
await this.loadElections();
showSuccess(this, 'Stimme erfolgreich abgegeben.');
showSuccess(this, this.$t('falukant.politics.voteSuccess'));
} catch (err) {
console.error(`Error submitting vote for election ${electionId}`, err);
showApiError(this, err, 'Fehler beim Abgeben der Stimme');
showApiError(this, err, this.$t('falukant.politics.voteError'));
}
},
@@ -573,10 +573,10 @@ export default {
{ votes: payload }
);
await this.loadElections();
showSuccess(this, 'Alle Stimmen erfolgreich abgegeben.');
showSuccess(this, this.$t('falukant.politics.voteAllSuccess'));
} catch (err) {
console.error('Error submitting all votes', err);
showApiError(this, err, 'Fehler beim Abgeben der Stimmen');
showApiError(this, err, this.$t('falukant.politics.voteAllError'));
}
},
@@ -632,7 +632,7 @@ export default {
this.selectedApplications = this.openPolitics
.filter(e => e.alreadyApplied || appliedIds.includes(e.id))
.map(e => e.id);
showSuccess(this, 'Kandidatur erfolgreich vorgemerkt.');
showSuccess(this, this.$t('falukant.politics.applyBookmarkSuccess'));
} catch (err) {
console.error('Error submitting applications', err);
if (err?.response?.data?.error === 'too_young') {