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

2
.gitignore vendored
View File

@@ -17,6 +17,8 @@ frontend/node_modules
frontend/node_modules/*
frontend/dist
frontend/dist/*
frontend/scripts/.i18n-de-fr-cache.json
frontend/scripts/.falukant-fr-smooth-cache.json
frontedtree.txt
backend/dist/
backend/data/model-cache

View File

@@ -0,0 +1,30 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
INSERT INTO type.user_param_value (user_param_type_id, value, order_id)
SELECT upt.id, 'fr', COALESCE(
(SELECT MAX(v.order_id) FROM type.user_param_value v WHERE v.user_param_type_id = upt.id),
0
) + 1
FROM type.user_param upt
WHERE upt.description = 'language'
AND NOT EXISTS (
SELECT 1 FROM type.user_param_value x
WHERE x.user_param_type_id = upt.id AND x.value = 'fr'
);
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DELETE FROM type.user_param_value v
USING type.user_param upt
WHERE v.user_param_type_id = upt.id
AND upt.description = 'language'
AND v.value = 'fr';
`);
},
};

View File

@@ -144,7 +144,7 @@ export const loginUser = async ({ username, password }) => {
const mappedParams = params.map(param => {
return { 'name': param.paramType.description, 'value': param.value };
});
const uiLocaleCodes = ['de', 'en', 'ceb', 'es'];
const uiLocaleCodes = ['de', 'en', 'ceb', 'es', 'fr'];
const langEntry = mappedParams.find((p) => p.name === 'language');
if (langEntry?.value && !uiLocaleCodes.includes(langEntry.value)) {
const idNum = parseInt(langEntry.value, 10);

View File

@@ -67,7 +67,7 @@ const initializeTypes = async () => {
}
const valuesList = {
gender: ['male', 'female', 'transfemale', 'transmale', 'nonbinary'],
language: ['de', 'en', 'ceb', 'es'],
language: ['de', 'en', 'ceb', 'es', 'fr'],
eyecolor: ['blue', 'green', 'brown', 'black', 'grey', 'hazel', 'amber', 'red', 'other'],
haircolor: ['black', 'brown', 'blonde', 'red', 'grey', 'white', 'other'],
hairlength: ['short', 'medium', 'long', 'bald', 'other'],

View File

@@ -0,0 +1,113 @@
# Konzept: Französisch (`fr`) als Bedien-/UI-Sprache
**Stand:** April 2026
**Bezug:** Bestehende Locales `de`, `en`, `es`, `ceb` (Vue I18n, Store, SEO, Header).
---
## 1. Ziel
- Nutzer können die Oberfläche auf **Französisch** stellen (Endonym: **Français**).
- **Vollständige Key-Parität** mit der deutschen Referenz (`de`), analog zu **Spanisch** (`es`): keine stillen Lücken, die nur über Fallback sichtbar werden.
- **Locale-Code:** `fr` (BCP 47). Regionale Varianten (`fr-CA`, `fr-CH`) nur bei Bedarf später; vorerst eine gemeinsame `fr`-UI.
---
## 2. Inhaltliche Leitplanken
| Thema | Empfehlung |
|--------|------------|
| **Anrede** | Einheitlich festlegen: **„vous“** (formell) oder **„tu“** (duzt); für eine Community-Plattform oft **vous** bei Systemtexten, ggf. Ausnahmen in Spieldialogen abstimmen. **Umsetzung:** Die erste französische UI-Version wurde maschinell aus dem Deutschen erzeugt (`google-translate-api-x`); menschliche Nachbearbeitung sollte **vous** durchgängig prüfen (v. a. Social/Vocab/Falukant). |
| **Referenz für Keys** | `de` als maßgebliche Struktur; Übersetzung nach Französisch. |
| **Qualität** | Große Module (Falukant, Admin, Social/Vocab) iterativ oder durch TMS/Übersetzer; CI-Check auf Key-Parität `de``fr`. |
---
## 3. Technische Umsetzung (Frontend)
### 3.1 Neue Übersetzungsdateien
- Ordner: `frontend/src/i18n/locales/fr/`
- Dieselben **19 JSON-Dateien** wie unter `en` / `de` / `es` / `ceb` (z. B. `general.json`, `header.json`, … `seo.json`).
### 3.2 I18n-Bundling
- Datei: `frontend/src/i18n/index.js`
- Alle `fr*` importieren.
- Block `messages.fr = { ...frGeneral, …, ...frSeo }` analog zu `es`.
- **`fallbackLocale`:** gesetzt als **`fr: ['de']`** (Lücken fallen auf Deutsch zurück).
- **`ceb`** nutzt `deepMerge` über `en`+`ceb`; **`fr`** wie **`es`**: nur `fr`-Bundles, kein deepMerge nötig.
### 3.3 Zentrale Liste unterstützter UI-Locales
Aktuell mehrfach dieselbe Liste (`de`, `en`, `ceb`, `es`), u. a. in:
- `frontend/src/store/index.js`
- `frontend/src/main.js` (`?lang=`, localStorage)
- `frontend/src/components/AppHeader.vue`
- `frontend/src/components/SettingsWidget.vue`
- `frontend/src/utils/seo.js`
**Empfehlung:** Eine exportierte Konstante z. B. `frontend/src/i18n/supportedLocales.js` (`SUPPORTED_UI_LOCALES`, ggf. `SEO_UI_LOCALES` daraus ableiten oder gemeinsam pflegen) und alle Stellen darauf umstellen **vor oder beim** Einführen von `fr`, damit keine Stelle vergessen wird.
### 3.4 Sprachauswahl UI
- **`AppHeader.vue`:** Option `{ value: 'fr', nativeLabel: 'Français' }`; `supported` aus Zentralmodul.
- **`SettingsWidget.vue`:** Nach Speichern der Profil-Sprache Store synchron halten; `languagesList` aktuell unvollständig vs. Header **alle** UI-Sprachen (`de`, `en`, `ceb`, `es`, `fr`) konsistent anbieten oder vollständig aus API-`setting.options` speisen.
### 3.5 Register / Einstellungen (JSON)
- In `settings.personal.language` (und falls nötig `register.languages`) in **allen** relevanten Locales den Eintrag **`fr`** / **„Français“** ergänzen.
### 3.6 SEO & Einstieg per URL
- **`frontend/src/utils/seo.js`:** `fr` in die Locale-Liste aufnehmen; `hreflang` für `fr`; `og:locale` / Alternates wie bei bestehenden Sprachen.
- **`main.js`:** Query `?lang=fr` über zentrale erlaubte Locales freischalten.
- **`index.html`:** falls hreflang/noscript manuell gepflegt wird `fr` ergänzen.
### 3.7 Browser-Sprache (optional)
- In `store` / `main.js`: Wenn `navigator.language` mit `fr` beginnt (z. B. `fr`, `fr-FR`, `fr-BE`, `fr-CH`), Standard-UI auf `fr` setzen analog zu bestehender Logik für Deutsch und Bisaya.
---
## 4. Backend
- Falls die **Account-Sprache** serverseitig validiert oder als feste Optionsliste ausgeliefert wird: **`fr`** zur Whitelist / zu den Optionen hinzufügen.
- **Vocab-Sprachen** in der Datenbank (Kurse, Zielsprachen) sind unabhängig von der **UI-Locale**; keine Pflichtänderung für reines UI-Französisch.
---
## 5. Qualitätssicherung & Abnahme
- Skript: flache Key-Parität `de` vs. `fr` pro JSON-Datei (wie bereits für `es` genutzt).
- `npm run build` ohne Fehler.
- Manuell: `?lang=fr`, Wechsel im Header ohne vollständigen Reload, Smoke über Login, Einstellungen, eine Social-/Falukant-Ansicht.
---
## 6. Definition of Done (Checkliste)
- [x] `frontend/src/i18n/locales/fr/*.json` vollständig (Key-Parität zu `de`)
- [x] `i18n/index.js`: `fr` eingebunden, `fallbackLocale` für `fr` gesetzt (`['de']`)
- [x] Zentrale `SUPPORTED_UI_LOCALES` in `frontend/src/i18n/supportedLocales.js`; Store, `main.js`, Header, Settings-Widget, `seo.js` importieren daraus
- [x] `AppHeader`, `main.js`, `store`, `seo.js`, `RegisterDialog` um `fr` / Browser-Erkennung `fr*` erweitert
- [x] `settings` / `register` JSON: Sprache `fr` (und Register um `es` wo gefehlt) ergänzt
- [x] `SettingsWidget`: `languagesList` über alle UI-Sprachen aus `SUPPORTED_UI_LOCALES`
- [x] Backend: `initializeTypes` + `authService` + Migration `type.user_param_value` für `fr`
- [x] SEO/hreflang/`?lang=fr` + `index.html` Alternate + noscript `lang="fr"`
- [x] QA: `npm run i18n:check-parity` (de↔fr), `npm run build`
- [x] Anrede / MT-Hinweis in Abschnitt 2 (Tabelle) und Skript `frontend/scripts/generate-fr-locale-from-de.mjs` für Regenerierung
---
## 7. Nächste Schritte (optional als Tickets)
1. **Infra:** `supportedLocales.js` + `fr` in allen technischen Stellen + leere/kopierte `fr`-JSONs aus `de` als Stub.
2. **SEO:** hreflang, `seo.json` `fr`, `main.js` / `index.html`.
3. **Übersetzung:** modulweise (Core → Social → Falukant → Admin).
4. **QA & CI:** Key-Check im Workflow.
---
*Ende des Konzepts.*

View File

@@ -29,6 +29,7 @@
<link rel="alternate" hreflang="en" href="%VITE_PUBLIC_BASE_URL%/?lang=en" />
<link rel="alternate" hreflang="es" href="%VITE_PUBLIC_BASE_URL%/?lang=es" />
<link rel="alternate" hreflang="ceb" href="%VITE_PUBLIC_BASE_URL%/?lang=ceb" />
<link rel="alternate" hreflang="fr" href="%VITE_PUBLIC_BASE_URL%/?lang=fr" />
<link rel="alternate" hreflang="x-default" href="%VITE_PUBLIC_BASE_URL%/" />
</head>
@@ -56,6 +57,11 @@
<p>Ang YourPart usa ka plataporma alang sa komunidad, chat, forum, blog, trainer sa bokabularyo, ang browser game nga Falukant ug minigames.</p>
<p>Mga bahin: <a href="/blogs?lang=ceb">Blogs</a>, <a href="/vokabeltrainer?lang=ceb">Bokabularyo</a>, <a href="/falukant?lang=ceb">Falukant</a>, <a href="/minigames?lang=ceb">Minigames</a>.</p>
</section>
<section lang="fr" style="max-width:960px;margin:24px auto;padding:0 20px;font-family:Arial,sans-serif;line-height:1.6;">
<h2>YourPart (français)</h2>
<p>YourPart est une plateforme pour la communauté, le chat, les forums, les blogs, lentraînement au vocabulaire, le jeu de construction Falukant dans le navigateur et les mini-jeux.</p>
<p>Zones principales&nbsp;: <a href="/blogs?lang=fr">Blogs</a>, <a href="/vokabeltrainer?lang=fr">Vocabulaire</a>, <a href="/falukant?lang=fr">Falukant</a> et <a href="/minigames?lang=fr">Mini-jeux</a>.</p>
</section>
</noscript>
<script type="module" src="/src/main.js"></script>
</body>

View File

@@ -35,6 +35,7 @@
"@gltf-transform/cli": "^4.3.0",
"@vitejs/plugin-vue": "^5.2.4",
"assert": "^2.1.0",
"google-translate-api-x": "^10.7.2",
"sass": "^1.98.0",
"stream-browserify": "^3.0.0",
"util": "^0.12.5",
@@ -3736,6 +3737,20 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/google-translate-api-x": {
"version": "10.7.2",
"resolved": "https://registry.npmjs.org/google-translate-api-x/-/google-translate-api-x-10.7.2.tgz",
"integrity": "sha512-GSmbvGMcnULaih2NFgD4Y6840DLAMot90mLWgwoB+FG/QpetyZkFrZkxop8ZxXgOAQXGskFOhGJady8nA6ZJ2g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/AidanWelch"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",

View File

@@ -6,7 +6,10 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"optimize-models": "node scripts/optimize-glb.mjs"
"optimize-models": "node scripts/optimize-glb.mjs",
"i18n:check-parity": "node scripts/check-i18n-locale-parity.mjs de fr",
"i18n:translate-fr": "node scripts/generate-fr-locale-from-de.mjs",
"i18n:smooth-falukant-fr": "node scripts/smooth-fr-falukant.mjs"
},
"dependencies": {
"@tiptap/extension-color": "^2.27.2",
@@ -36,6 +39,7 @@
"@gltf-transform/cli": "^4.3.0",
"@vitejs/plugin-vue": "^5.2.4",
"assert": "^2.1.0",
"google-translate-api-x": "^10.7.2",
"sass": "^1.98.0",
"stream-browserify": "^3.0.0",
"util": "^0.12.5",

View File

@@ -0,0 +1,78 @@
/**
* Vergleicht flache Schlüsselpfade zwischen zwei Locale-Ordnern (gleiche JSON-Dateinamen).
*
* Aufruf: node scripts/check-i18n-locale-parity.mjs de fr
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const BASE = path.join(__dirname, '../src/i18n/locales');
function flattenLeaves(obj, prefix = '', out = {}) {
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
out[prefix] = obj;
return out;
}
const keys = Object.keys(obj);
if (keys.length === 0) {
out[prefix] = obj;
return out;
}
for (const k of keys) {
const p = prefix ? `${prefix}.${k}` : k;
const v = obj[k];
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
flattenLeaves(v, p, out);
} else {
out[p] = v;
}
}
return out;
}
const [a, b] = process.argv.slice(2);
if (!a || !b) {
console.error('Usage: node scripts/check-i18n-locale-parity.mjs <localeA> <localeB>');
process.exit(2);
}
const dirA = path.join(BASE, a);
const dirB = path.join(BASE, b);
const files = fs.readdirSync(dirA).filter((f) => f.endsWith('.json'));
let errors = 0;
for (const f of files) {
const pa = path.join(dirA, f);
const pb = path.join(dirB, f);
if (!fs.existsSync(pb)) {
console.error(`Fehlt: ${b}/${f}`);
errors++;
continue;
}
const ja = JSON.parse(fs.readFileSync(pa, 'utf8'));
const jb = JSON.parse(fs.readFileSync(pb, 'utf8'));
const fa = flattenLeaves(ja);
const fb = flattenLeaves(jb);
const ka = new Set(Object.keys(fa));
const kb = new Set(Object.keys(fb));
for (const k of ka) {
if (!kb.has(k)) {
console.error(`[${f}] fehlt in ${b}: ${k}`);
errors++;
}
}
for (const k of kb) {
if (!ka.has(k)) {
console.error(`[${f}] extra in ${b}: ${k}`);
errors++;
}
}
}
if (errors) {
console.error(`\n${errors} Abweichung(en).`);
process.exit(1);
}
console.log(`OK: Key-Parität ${a}${b} für ${files.length} Dateien.`);

View File

@@ -0,0 +1,134 @@
/**
* Übersetzt String-Blätter in locales/fr/*.json von Deutsch nach Französisch
* (google-translate-api-x, inoffiziell), mit Cache unter scripts/.i18n-de-fr-cache.json.
*
* Ausführen aus frontend/: node scripts/generate-fr-locale-from-de.mjs
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { translate } from 'google-translate-api-x';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LOCALES_FR = path.join(__dirname, '../src/i18n/locales/fr');
const CACHE_PATH = path.join(__dirname, '.i18n-de-fr-cache.json');
const CONCURRENCY = Math.max(1, parseInt(process.env.I18N_MT_CONCURRENCY || '4', 10));
const DELAY_MS = Math.max(0, parseInt(process.env.I18N_MT_DELAY_MS || '350', 10));
function loadCache() {
try {
return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));
} catch {
return {};
}
}
function saveCache(cache) {
fs.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 0), 'utf8');
}
function shouldSkipTranslation(str) {
if (typeof str !== 'string' || str.length === 0) return true;
if (/^[\d\s./:\\\-–—:]+$/u.test(str) && str.length < 96) return true;
return false;
}
function collectStrings(node, set) {
if (node === null || node === undefined) return;
if (typeof node === 'string') {
set.add(node);
return;
}
if (Array.isArray(node)) {
for (const x of node) collectStrings(x, set);
return;
}
if (typeof node === 'object') {
for (const k of Object.keys(node)) collectStrings(node[k], set);
}
}
function applyTranslations(node, cache) {
if (node === null || node === undefined) return node;
if (typeof node === 'string') {
if (shouldSkipTranslation(node)) return node;
return cache[node] !== undefined ? cache[node] : node;
}
if (Array.isArray(node)) return node.map((x) => applyTranslations(x, cache));
if (typeof node === 'object') {
const out = {};
for (const k of Object.keys(node)) {
out[k] = applyTranslations(node[k], cache);
}
return out;
}
return node;
}
async function translateOne(text, cache) {
if (shouldSkipTranslation(text)) return text;
if (cache[text]) return cache[text];
for (let attempt = 0; attempt < 4; attempt++) {
try {
const res = await translate(text, { from: 'de', to: 'fr' });
const out = res.text;
cache[text] = out;
return out;
} catch (e) {
await new Promise((r) => setTimeout(r, 900 * (attempt + 1)));
}
}
console.warn('Übersetzung fehlgeschlagen, Original behalten:', text.slice(0, 80));
cache[text] = text;
return text;
}
async function main() {
const files = fs.readdirSync(LOCALES_FR).filter((f) => f.endsWith('.json'));
const all = new Set();
for (const f of files) {
const raw = JSON.parse(fs.readFileSync(path.join(LOCALES_FR, f), 'utf8'));
collectStrings(raw, all);
}
const unique = [...all].filter((s) => !shouldSkipTranslation(s));
const cache = loadCache();
const missing = unique.filter((s) => cache[s] === undefined);
console.log(`Unique (MT): ${unique.length}, ohne Cache-Eintrag: ${missing.length}`);
let next = 0;
let saved = 0;
async function worker() {
for (;;) {
const i = next++;
if (i >= missing.length) return;
const s = missing[i];
await translateOne(s, cache);
saved++;
if (saved % 30 === 0) {
saveCache(cache);
console.log(`${saved}/${missing.length}`);
}
if (DELAY_MS > 0) {
await new Promise((r) => setTimeout(r, DELAY_MS));
}
}
}
const workers = Array.from({ length: Math.min(CONCURRENCY, Math.max(1, missing.length)) }, () => worker());
await Promise.all(workers);
saveCache(cache);
for (const f of files) {
const p = path.join(LOCALES_FR, f);
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
const out = applyTranslations(raw, cache);
fs.writeFileSync(p, `${JSON.stringify(out, null, 4)}\n`, 'utf8');
}
console.log('Fertig: fr/*.json aktualisiert.');
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -0,0 +1,137 @@
/**
* Erzeugt fr/falukant.json neu aus de/falukant.json (google-translate-api-x).
* Stellt sicher, dass benannte Platzhalter {name} exakt wie im Deutschen bleiben.
*
* Aus frontend/: node scripts/smooth-fr-falukant.mjs
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { translate } from 'google-translate-api-x';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DE_PATH = path.join(__dirname, '../src/i18n/locales/de/falukant.json');
const FR_PATH = path.join(__dirname, '../src/i18n/locales/fr/falukant.json');
const CACHE_PATH = path.join(__dirname, '.falukant-fr-smooth-cache.json');
const CONCURRENCY = Math.max(1, parseInt(process.env.I18N_MT_CONCURRENCY || '3', 10));
const DELAY_MS = Math.max(0, parseInt(process.env.I18N_MT_DELAY_MS || '400', 10));
function loadCache() {
try {
return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));
} catch {
return {};
}
}
function saveCache(cache) {
fs.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 0), 'utf8');
}
function shouldSkipTranslation(str) {
if (typeof str !== 'string' || str.length === 0) return true;
if (/^[\d\s./:\\\-–—:]+$/u.test(str) && str.length < 96) return true;
if (/^([dDMYHhms\.\/:,\[\]'\s-]|yyyy|yy)+$/i.test(str.trim()) && str.length < 96) return true;
return false;
}
/** Ersetzt {…}-Tokens in der Übersetzung durch die Namen aus dem Deutschen (gleiche Reihenfolge). */
function alignPlaceholders(deStr, frStr) {
const deNames = [...deStr.matchAll(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g)].map((m) => m[1]);
if (deNames.length === 0) return frStr;
let i = 0;
return frStr.replace(/\{[^}]+\}/g, () => {
const name = deNames[i];
i += 1;
return name !== undefined ? `{${name}}` : '{}';
});
}
async function translateOne(text, cache) {
if (shouldSkipTranslation(text)) return text;
if (cache[text]) return cache[text];
for (let attempt = 0; attempt < 4; attempt++) {
try {
const res = await translate(text, { from: 'de', to: 'fr' });
const aligned = alignPlaceholders(text, res.text);
cache[text] = aligned;
return aligned;
} catch {
await new Promise((r) => setTimeout(r, 800 * (attempt + 1)));
}
}
console.warn('Übersetzung fehlgeschlagen:', text.slice(0, 72));
cache[text] = text;
return text;
}
function collectStrings(node, set) {
if (typeof node === 'string') {
set.add(node);
return;
}
if (Array.isArray(node)) {
for (const x of node) collectStrings(x, set);
return;
}
if (node && typeof node === 'object') {
for (const k of Object.keys(node)) collectStrings(node[k], set);
}
}
function applyMap(node, cache) {
if (typeof node === 'string') {
if (shouldSkipTranslation(node)) return node;
return cache[node] !== undefined ? cache[node] : node;
}
if (Array.isArray(node)) return node.map((x) => applyMap(x, cache));
if (node && typeof node === 'object') {
const out = {};
for (const k of Object.keys(node)) {
out[k] = applyMap(node[k], cache);
}
return out;
}
return node;
}
async function main() {
const deRoot = JSON.parse(fs.readFileSync(DE_PATH, 'utf8'));
const cache = loadCache();
const all = new Set();
collectStrings(deRoot, all);
const unique = [...all].filter((s) => !shouldSkipTranslation(s));
const missing = unique.filter((s) => cache[s] === undefined);
console.log(`Falukant: ${unique.length} übersetzbare Strings, ${missing.length} ohne Cache.`);
let next = 0;
let done = 0;
async function worker() {
for (;;) {
const i = next++;
if (i >= missing.length) return;
const s = missing[i];
await translateOne(s, cache);
done++;
if (done % 40 === 0) {
saveCache(cache);
console.log(`${done}/${missing.length}`);
}
if (DELAY_MS > 0) await new Promise((r) => setTimeout(r, DELAY_MS));
}
}
const workers = Array.from({ length: Math.min(CONCURRENCY, Math.max(1, missing.length)) }, () => worker());
await Promise.all(workers);
saveCache(cache);
const out = applyMap(deRoot, cache);
fs.writeFileSync(FR_PATH, `${JSON.stringify(out, null, 4)}\n`, 'utf8');
console.log('Geschrieben:', FR_PATH);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -43,6 +43,7 @@
<script>
import { mapGetters } from 'vuex';
import apiClient from '@/utils/axios.js';
import { SUPPORTED_UI_LOCALES } from '@/i18n/supportedLocales.js';
export default {
name: 'AppHeader',
@@ -54,6 +55,7 @@ export default {
{ value: 'en', nativeLabel: 'English' },
{ value: 'ceb', nativeLabel: 'Binisaya' },
{ value: 'es', nativeLabel: 'Español' },
{ value: 'fr', nativeLabel: 'Français' },
],
};
},
@@ -78,8 +80,7 @@ export default {
},
methods: {
async onUiLanguageChange(code) {
const supported = ['de', 'en', 'ceb', 'es'];
if (!supported.includes(code)) {
if (!SUPPORTED_UI_LOCALES.includes(code)) {
return;
}
await this.$store.dispatch('setLanguage', code);

View File

@@ -86,6 +86,7 @@ import FloatInputWidget from '@/components/form/FloatInputWidget.vue';
import CheckboxWidget from '@/components/form/CheckboxWidget.vue';
import MultiselectWidget from '@/components/form/MultiselectWidget.vue';
import { showApiError, showError } from '@/utils/feedback.js';
import { SUPPORTED_UI_LOCALES } from '@/i18n/supportedLocales.js';
export default {
name: "SettingsWidget",
@@ -169,8 +170,7 @@ export default {
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)) {
if (code && SUPPORTED_UI_LOCALES.includes(code)) {
this.$store.dispatch('setLanguage', code);
}
}
@@ -181,11 +181,10 @@ export default {
}
},
languagesList() {
return [
{ value: 'en', captionTr: 'settings.personal.language.en' },
{ value: 'de', captionTr: 'settings.personal.language.de' },
{ value: 'ceb', captionTr: 'settings.personal.language.ceb' },
];
return SUPPORTED_UI_LOCALES.map((code) => ({
value: code,
captionTr: `settings.personal.language.${code}`,
}));
},
convertToInt(value) {
const intValue = parseInt(value, 10);

View File

@@ -108,8 +108,19 @@ export default {
short = 'ceb';
} else if (browserLanguage.startsWith('de')) {
short = 'de';
} else if (browserLanguage.startsWith('fr')) {
short = 'fr';
} else if (browserLanguage.startsWith('es')) {
short = 'es';
} else {
short = 'en';
const prefs = navigator.languages || [browserLanguage];
if (prefs.some((l) => l.startsWith('fr'))) {
short = 'fr';
} else if (prefs.some((l) => l.startsWith('es'))) {
short = 'es';
} else {
short = 'en';
}
}
const response = await apiClient.post('/api/settings/getparamvalueid', { paramValue: short });
this.language = response.data.paramValueId;

View File

@@ -80,6 +80,26 @@ import esMessage from './locales/es/message.json';
import esPersonal from './locales/es/personal.json';
import esSeo from './locales/es/seo.json';
import frGeneral from './locales/fr/general.json';
import frHeader from './locales/fr/header.json';
import frNavigation from './locales/fr/navigation.json';
import frHome from './locales/fr/home.json';
import frChat from './locales/fr/chat.json';
import frRegister from './locales/fr/register.json';
import frError from './locales/fr/error.json';
import frActivate from './locales/fr/activate.json';
import frSettings from './locales/fr/settings.json';
import frAdmin from './locales/fr/admin.json';
import frSocialNetwork from './locales/fr/socialnetwork.json';
import frFriends from './locales/fr/friends.json';
import frFalukant from './locales/fr/falukant.json';
import frPasswordReset from './locales/fr/passwordReset.json';
import frBlog from './locales/fr/blog.json';
import frMinigames from './locales/fr/minigames.json';
import frMessage from './locales/fr/message.json';
import frPersonal from './locales/fr/personal.json';
import frSeo from './locales/fr/seo.json';
function isPlainObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
@@ -212,14 +232,36 @@ const messages = {
...esMessage,
...esPersonal,
...esSeo,
}
},
fr: {
...frGeneral,
...frHeader,
...frNavigation,
...frHome,
...frChat,
...frRegister,
...frPasswordReset,
...frError,
...frActivate,
...frSettings,
...frAdmin,
...frSocialNetwork,
...frFriends,
...frFalukant,
...frBlog,
...frMinigames,
...frMessage,
...frPersonal,
...frSeo,
},
};
const i18n = createI18n({
locale: store.state.language,
fallbackLocale: {
ceb: ['en', 'de'],
default: ['de']
fr: ['de'],
default: ['de'],
},
messages
});

View File

@@ -10,6 +10,16 @@
"windy": "Mahangin",
"clear": "Klaro"
},
"conditionBand": {
"excellent": "Maayo kaayo",
"veryGood": "Maayo kaayo",
"good": "Maayo",
"moderate": "Kasarang",
"bad": "Daotan",
"veryBad": "Daotan kaayo",
"catastrophic": "Grabeng daot",
"unknown": "Wala mahibalo-i"
},
"debtorsPrison": {
"actionBlocked": "Kini nga aksyon gi-block samtang naa ka sa debtors' prison.",
"globalWarning": "Ang imong kalangan sa utang nagsugod na og limit sa imong mga aksyon. Posible nga mosunod dayon ang pinugos nga mga lakang.",
@@ -109,7 +119,8 @@
"loading": "Nag-load sa posibleng mga manununod…",
"noHeirs": "Walay available nga mga manununod.",
"select": "Pilia isip play character",
"error": "Sayop sa pagpili sa manununod."
"error": "Sayop sa pagpili sa manununod.",
"success": "Karon nagduwa ka na isip napiling manununod."
},
"metadata": {
"title": "Personal",
@@ -174,6 +185,32 @@
"stock": "Mga posisyon sa stock",
"stockHint": "Mubo nga tan-aw sa mga baligya ug stock sa tanang rehiyon.",
"open": "Ablihi"
},
"routine": {
"branch": {
"kicker": "Rutina",
"title": "Ablihi ang branch",
"description": "Pinakapaspas nga agianan padulong sa produksyon, bodega, baligya ug transporte.",
"cta": "Adto sa operasyon"
},
"finance": {
"kicker": "Overview",
"title": "Tan-awa ang kwarta",
"description": "Balanse, kasaysayan ug ekonomikanhong kalamboan nga dili na kinahanglan pa dugay pangita.",
"cta": "Kasaysayan sa kuwarta"
},
"family": {
"kicker": "Karakter",
"title": "Pamilya ug panununod",
"description": "Mga importante nga personal nga desisyon ug kahimtang sa panimalay sa usa ka dapit.",
"cta": "Ablihi ang pamilya"
},
"house": {
"kicker": "Kabtangan",
"title": "Balay ug palibot",
"description": "Puy-anan ug adlaw-adlaw nga kahimtang isip kaugalingong larangan sa trabaho.",
"cta": "Adto sa balay"
}
}
},
"health": {
@@ -463,21 +500,59 @@
}
},
"nobility": {
"title": "Kadungganan / status",
"tabs": {
"overview": "Overview",
"advance": "Motikas"
},
"highestPoliticalOffice": "Pinakataas nga politikal nga opisina",
"highestOfficeAny": "Pinakataas nga opisina sa kinatibuk-an",
"none": "wala",
"nextTitle": "Sunod nga posibleng titulo",
"advanceNoNext": "Sayop: walay impormasyon bahin sa sunod nga titulo. Palihug i-reload ang panid.",
"cooldown": "Makapaningkamot ka pag-usab sa {date}.",
"requirement": {
"money": "Wealth sa least {amount}",
"cost": "Kantidad: {amount}",
"branches": "At least {amount} sangang opisinaes",
"reputation": "Popularity sa least {amount}",
"house_position": "Balay status sa least level {amount}",
"house_condition": "Balay condition sa least {amount}",
"office_rank_any": "Highest politikal o simbahan opisina sa least rank {amount}",
"office_rank_political": "Highest politikal opisina sa least rank {amount}",
"lover_count_min": "At least {amount} lovers o favorites",
"lover_count_max": "At most {amount} lovers o favorites"
"money": "Bahanding bisan {amount}",
"cost": "Gasto: {amount}",
"branches": "Bisan {amount} ka branch",
"reputation": "Popularidad bisan {amount}",
"house_position": "Kinahanglan ang lebel sa panimalay: {label}",
"house_condition": "Kahimtang sa balay bisan {quality}",
"office_rank_any": "Pinakataas nga politikal o simbahan nga opisina bisan ranggo {amount}",
"office_rank_political": "Pinakataas nga politikal nga opisina bisan ranggo {amount}",
"lover_count_min": "Bisan {amount} ka minyo o pinalabi",
"lover_count_max": "Dili molapas sa {amount} ka minyo o pinalabi",
"unknown": "{type}: {amount}"
},
"housePosition": {
"1": "Nagpuyo ubos sa tulay",
"2": "Usa ka payag nga lawasi",
"3": "Usa ka balay nga kahoy",
"4": "Usa ka lawak sa likod-balay",
"5": "Usa ka gamay nga pamilya nga balay",
"6": "Usa ka townhouse",
"7": "Usa ka villa",
"8": "Usa ka manor",
"9": "Usa ka kastilyo",
"fallback": "Lebel sa balay {level}"
},
"houseConditionQuality": {
"nearPerfect": "hapit perpekto",
"veryGood": "maayo kaayo",
"good": "maayo",
"decent": "maayo ra",
"usable": "gamiton pa"
},
"houseConditionPercent": "{pct}%",
"officeWithRank": "{label} (ranggo {rank})",
"advance": {
"confirm": "Hangyo og pag-uswag",
"processing": "Nagproseso…"
},
"errors": {
"tooSoon": "Dili pa mapa-uswag karon.",
"unmet": "Kulang pa kini nga mga kinahanglanon:",
"generic": "Adunay sayop sa pag-uswag."
}
},
"mood": {
@@ -571,6 +646,9 @@
"elections": "Mga eleksiyon"
},
"bookmarkCandidate": "Timan-i kining kandidatura",
"voteSuccess": "Malampuson nga gihatag ang boto.",
"voteAllSuccess": "Malampuson nga gihatag ang tanang boto.",
"applyBookmarkSuccess": "Malampuson nga natiman-an ang mga kandidatura.",
"voteError": "Sayop sa paghatag sa boto",
"voteAllError": "Sayop sa paghatag sa mga boto",
"applyError": "Dili mapadala ang aplikasyon.",

View File

@@ -9,7 +9,9 @@
"languages": {
"en": "Iningles",
"de": "Aleman",
"ceb": "Bisaya"
"ceb": "Bisaya",
"es": "Espanyol",
"fr": "Pranses"
},
"register": "Pagrehistro",
"close": "Isira",

View File

@@ -58,7 +58,8 @@
"de": "Aleman",
"en": "Iningles",
"ceb": "Bisaya",
"es": "Espanyol"
"es": "Espanyol",
"fr": "Pranses"
},
"eyecolor": {
"blue": "Asul",

View File

@@ -10,6 +10,16 @@
"windy": "Windig",
"clear": "Klar"
},
"conditionBand": {
"excellent": "Ausgezeichnet",
"veryGood": "Sehr gut",
"good": "Gut",
"moderate": "Mäßig",
"bad": "Schlecht",
"veryBad": "Sehr schlecht",
"catastrophic": "Katastrophal",
"unknown": "Unbekannt"
},
"statusbar": {
"age": "Alter",
"wealth": "Vermögen",
@@ -129,7 +139,8 @@
"loading": "Lade mögliche Erben…",
"noHeirs": "Keine Erben verfügbar.",
"select": "Als Spielcharakter wählen",
"error": "Fehler beim Auswählen des Erben."
"error": "Fehler beim Auswählen des Erben.",
"success": "Du spielst nun mit dem gewählten Erben weiter."
},
"metadata": {
"title": "Persönliches",
@@ -195,6 +206,32 @@
"stockHint": "Verdichteter Blick auf Warenbestand über alle Regionen.",
"open": "Öffnen"
},
"routine": {
"branch": {
"kicker": "Routine",
"title": "Niederlassung öffnen",
"description": "Die schnellste Route zu Produktion, Lager, Verkauf und Transport.",
"cta": "Zu den Betrieben"
},
"finance": {
"kicker": "Überblick",
"title": "Finanzen prüfen",
"description": "Kontostand, Verlauf und wirtschaftliche Entwicklung ohne lange Suche.",
"cta": "Geldhistorie"
},
"family": {
"kicker": "Charakter",
"title": "Familie und Nachfolge",
"description": "Wichtige persönliche Entscheidungen und Haushaltsstatus gesammelt.",
"cta": "Familie öffnen"
},
"house": {
"kicker": "Besitz",
"title": "Haus und Umfeld",
"description": "Wohnsitz und alltäglicher Status als eigener Arbeitsbereich.",
"cta": "Zum Haus"
}
},
"productions": {
"title": "Produktionen"
},
@@ -1052,20 +1089,49 @@
"highestOfficeAny": "Höchstes Amt insgesamt",
"none": "keines",
"nextTitle": "Nächster möglicher Titel",
"advanceNoNext": "Fehler: Keine nächste Titel-Information verfügbar. Bitte Seite neu laden.",
"requirement": {
"money": "Vermögen mindestens {amount}",
"cost": "Kosten: {amount}",
"branches": "Mindestens {amount} Niederlassungen",
"reputation": "Beliebtheit mindestens {amount}",
"house_position": "Hausstand mindestens Stufe {amount}",
"house_condition": "Hauszustand mindestens {amount}",
"house_position": "Hausstand mindestens: {label}",
"house_condition": "Hauszustand mindestens {quality}",
"office_rank_any": "Höchstes politisches oder kirchliches Amt mindestens Rang {amount}",
"office_rank_political": "Höchstes politisches Amt mindestens Rang {amount}",
"lover_count_min": "Mindestens {amount} Liebhaber oder Mätressen",
"lover_count_max": "Höchstens {amount} Liebhaber oder Mätressen"
"lover_count_max": "Höchstens {amount} Liebhaber oder Mätressen",
"unknown": "{type}: {amount}"
},
"housePosition": {
"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",
"fallback": "Haus-Stufe {level}"
},
"houseConditionQuality": {
"nearPerfect": "nahezu makellos",
"veryGood": "sehr gut",
"good": "gut",
"decent": "ordentlich",
"usable": "brauchbar"
},
"houseConditionPercent": "{pct} %",
"officeWithRank": "{label} (Rang {rank})",
"advance": {
"confirm": "Aufsteigen beantragen"
"confirm": "Aufsteigen beantragen",
"processing": "Wird bearbeitet…"
},
"errors": {
"tooSoon": "Der Aufstieg ist noch nicht möglich.",
"unmet": "Folgende Voraussetzungen fehlen noch:",
"generic": "Beim Aufstieg ist ein Fehler aufgetreten."
},
"cooldown": "Du kannst frühestens wieder am {date} aufsteigen."
},
@@ -1353,6 +1419,9 @@
"elections": "Wahlen"
},
"bookmarkCandidate": "Für diese Kandidatur vormerken",
"voteSuccess": "Stimme erfolgreich abgegeben.",
"voteAllSuccess": "Alle Stimmen erfolgreich abgegeben.",
"applyBookmarkSuccess": "Kandidatur erfolgreich vorgemerkt.",
"voteError": "Fehler beim Abgeben der Stimme",
"voteAllError": "Fehler beim Abgeben der Stimmen",
"applyError": "Bewerbung konnte nicht eingereicht werden.",

View File

@@ -9,7 +9,9 @@
"languages": {
"en": "Englisch",
"de": "Deutsch",
"ceb": "Bisaya"
"ceb": "Bisaya",
"es": "Spanisch",
"fr": "Französisch"
},
"register": "Registrieren",
"close": "Schließen",

View File

@@ -58,7 +58,8 @@
"de": "Deutsch",
"en": "Englisch",
"ceb": "Bisaya",
"es": "Spanisch"
"es": "Spanisch",
"fr": "Französisch"
},
"eyecolor": {
"blue": "Blau",

View File

@@ -10,6 +10,16 @@
"windy": "Windy",
"clear": "Clear"
},
"conditionBand": {
"excellent": "Excellent",
"veryGood": "Very good",
"good": "Good",
"moderate": "Fair",
"bad": "Poor",
"veryBad": "Very poor",
"catastrophic": "Critical",
"unknown": "Unknown"
},
"debtorsPrison": {
"actionBlocked": "This action is blocked while you are in debtors' prison.",
"globalWarning": "Your credit delinquency is already restricting your actions. Forced measures may follow soon.",
@@ -110,7 +120,8 @@
"loading": "Loading potential heirs…",
"noHeirs": "No heirs available.",
"select": "Select as play character",
"error": "Error selecting heir."
"error": "Error selecting heir.",
"success": "You are now playing as the selected heir."
},
"metadata": {
"title": "Personal",
@@ -175,6 +186,32 @@
"stock": "Storage positions",
"stockHint": "Condensed view of goods across all regions.",
"open": "Open"
},
"routine": {
"branch": {
"kicker": "Routine",
"title": "Open a branch",
"description": "The fastest route to production, storage, sales and transport.",
"cta": "Go to operations"
},
"finance": {
"kicker": "Overview",
"title": "Review finances",
"description": "Account balance, history and economic development without a long search.",
"cta": "Money history"
},
"family": {
"kicker": "Character",
"title": "Family and succession",
"description": "Important personal decisions and household status in one place.",
"cta": "Open family"
},
"house": {
"kicker": "Property",
"title": "House and surroundings",
"description": "Residence and everyday status as its own work area.",
"cta": "Go to house"
}
}
},
"health": {
@@ -464,20 +501,58 @@
}
},
"nobility": {
"title": "Noble status",
"tabs": {
"overview": "Overview",
"advance": "Advance"
},
"highestPoliticalOffice": "Highest political office",
"highestOfficeAny": "Highest office overall",
"none": "none",
"nextTitle": "Next possible title",
"advanceNoNext": "Error: No information on the next title is available. Please reload the page.",
"requirement": {
"money": "Wealth at least {amount}",
"cost": "Cost: {amount}",
"branches": "At least {amount} branches",
"reputation": "Popularity at least {amount}",
"house_position": "House status at least level {amount}",
"house_condition": "House condition at least {amount}",
"house_position": "Household at least: {label}",
"house_condition": "House condition at least {quality}",
"office_rank_any": "Highest political or church office at least rank {amount}",
"office_rank_political": "Highest political office at least rank {amount}",
"lover_count_min": "At least {amount} lovers or favorites",
"lover_count_max": "At most {amount} lovers or favorites"
"lover_count_max": "At most {amount} lovers or favorites",
"unknown": "{type}: {amount}"
},
"housePosition": {
"1": "Living under a bridge",
"2": "A straw hut",
"3": "A wooden house",
"4": "A backyard room",
"5": "A small family house",
"6": "A townhouse",
"7": "A villa",
"8": "A manor house",
"9": "A castle",
"fallback": "House tier {level}"
},
"houseConditionQuality": {
"nearPerfect": "near-flawless",
"veryGood": "very good",
"good": "good",
"decent": "decent",
"usable": "adequate"
},
"houseConditionPercent": "{pct}%",
"officeWithRank": "{label} (rank {rank})",
"advance": {
"confirm": "Request advancement",
"processing": "Processing…"
},
"errors": {
"tooSoon": "You cannot advance yet.",
"unmet": "Requirements not yet met:",
"generic": "Something went wrong."
},
"cooldown": "You can only advance again on {date}."
},
@@ -572,6 +647,9 @@
"elections": "Elections"
},
"bookmarkCandidate": "Bookmark this candidacy",
"voteSuccess": "Vote submitted successfully.",
"voteAllSuccess": "All votes submitted successfully.",
"applyBookmarkSuccess": "Candidacies saved successfully.",
"voteError": "Error while submitting the vote",
"voteAllError": "Error while submitting the votes",
"applyError": "Application could not be submitted.",

View File

@@ -9,7 +9,9 @@
"languages": {
"en": "English",
"de": "German",
"ceb": "Bisaya"
"ceb": "Bisaya",
"es": "Spanish",
"fr": "French"
},
"register": "Register",
"close": "Close",

View File

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

View File

@@ -10,6 +10,16 @@
"windy": "Ventoso",
"clear": "Despejado"
},
"conditionBand": {
"excellent": "Excelente",
"veryGood": "Muy bueno",
"good": "Bueno",
"moderate": "Regular",
"bad": "Malo",
"veryBad": "Muy malo",
"catastrophic": "Crítico",
"unknown": "Desconocido"
},
"statusbar": {
"age": "Edad",
"wealth": "Patrimonio",
@@ -128,7 +138,8 @@
"loading": "Cargando posibles herederos…",
"noHeirs": "No hay herederos disponibles.",
"select": "Elegir como personaje",
"error": "Error al elegir al heredero."
"error": "Error al elegir al heredero.",
"success": "Ahora juegas con el heredero elegido."
},
"metadata": {
"title": "Datos personales",
@@ -194,6 +205,32 @@
"stockHint": "Vista resumida del inventario en todas las regiones.",
"open": "Abrir"
},
"routine": {
"branch": {
"kicker": "Rutina",
"title": "Abrir una sucursal",
"description": "La ruta más rápida a producción, almacén, ventas y transporte.",
"cta": "Ir a operaciones"
},
"finance": {
"kicker": "Resumen",
"title": "Revisar finanzas",
"description": "Saldo, historial y evolución económica sin buscar mucho.",
"cta": "Historial de dinero"
},
"family": {
"kicker": "Personaje",
"title": "Familia y sucesión",
"description": "Decisiones personales y estado del hogar en un solo lugar.",
"cta": "Abrir familia"
},
"house": {
"kicker": "Propiedad",
"title": "Casa y entorno",
"description": "Residencia y día a día como área de trabajo propia.",
"cta": "Ir a la casa"
}
},
"productions": {
"title": "Producciones"
},
@@ -1052,20 +1089,44 @@
"highestOfficeAny": "Cargo más alto en total",
"none": "ninguno",
"nextTitle": "Siguiente título posible",
"advanceNoNext": "Error: no hay información sobre el siguiente título. Recarga la página.",
"requirement": {
"money": "Patrimonio mínimo {amount}",
"cost": "Coste: {amount}",
"branches": "Al menos {amount} sucursales",
"reputation": "Popularidad mínima {amount}",
"house_position": "Casa al menos nivel {amount}",
"house_condition": "Estado de la casa al menos {amount}",
"house_position": "Hogar al menos: {label}",
"house_condition": "Estado de la casa al menos {quality}",
"office_rank_any": "Cargo político o eclesiástico más alto al menos rango {amount}",
"office_rank_political": "Cargo político más alto al menos rango {amount}",
"lover_count_min": "Al menos {amount} amantes o favoritos",
"lover_count_max": "Como máximo {amount} amantes o favoritos"
"lover_count_max": "Como máximo {amount} amantes o favoritos",
"unknown": "{type}: {amount}"
},
"housePosition": {
"1": "Vivir bajo un puente",
"2": "Una choza de paja",
"3": "Una casa de madera",
"4": "Un cuarto en el patio",
"5": "Una casa familiar pequeña",
"6": "Una casa urbana",
"7": "Una villa",
"8": "Una mansión",
"9": "Un castillo",
"fallback": "Nivel de casa {level}"
},
"houseConditionQuality": {
"nearPerfect": "casi impecable",
"veryGood": "muy bueno",
"good": "bueno",
"decent": "decente",
"usable": "aceptable"
},
"houseConditionPercent": "{pct} %",
"officeWithRank": "{label} (rango {rank})",
"advance": {
"confirm": "Solicitar ascenso"
"confirm": "Solicitar ascenso",
"processing": "Procesando…"
},
"cooldown": "Podrás ascender de nuevo como muy pronto el {date}.",
"errors": {
@@ -1358,6 +1419,9 @@
"elections": "Elecciones"
},
"bookmarkCandidate": "Recordar esta candidatura",
"voteSuccess": "Voto enviado correctamente.",
"voteAllSuccess": "Todos los votos se enviaron correctamente.",
"applyBookmarkSuccess": "Candidaturas guardadas correctamente.",
"voteError": "Error al emitir el voto",
"voteAllError": "Error al emitir los votos",
"applyError": "No se pudo enviar la candidatura.",

View File

@@ -10,7 +10,8 @@
"en": "Inglés",
"de": "Alemán",
"ceb": "Bisaya",
"es": "Español"
"es": "Español",
"fr": "Francés"
},
"register": "Registrarse",
"close": "Cerrar",

View File

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

View File

@@ -0,0 +1,9 @@
{
"activate": {
"title": "Activer l'accès",
"message": "Bonjour {nom d'utilisateur}. Veuillez saisir ici le code que nous vous avons envoyé par email.",
"token": "Jetons :",
"submit": "Soumettre",
"failure": "L'activation n'a pas réussi."
}
}

View File

@@ -0,0 +1,510 @@
{
"admin": {
"interests": {
"title": "[Admin] - Gérer les intérêts",
"newinterests": {
"name": "Nom d'intérêt",
"status": "Libéré",
"adultonly": "Réservé aux adultes",
"translations": "Traductions",
"isactive": "Activé",
"isadult": "Réservé aux adultes",
"delete": "Supprimer"
}
},
"contacts": {
"title": "[Admin] - Demandes de contact",
"date": "Date",
"from": "Expéditeur",
"actions": "Actes",
"open": "Modifier",
"finished": "Complet"
},
"editcontactrequest": {
"title": "[Admin] - Modifier la demande de contact"
},
"user": {
"name": "nom d'utilisateur",
"active": "Aktiv",
"blocked": "Gesperrt",
"actions": "Actes",
"search": "Suchen"
},
"vocabLessonReset": {
"title": "Sprachkurs: Lektionsfortschritt",
"intro": "Fortschritt, Übungsergebnisse und gespeicherter Lektionszustand für eine einzelne Lektion löschen (nicht der ganze Kurs). Es werden nur Sprachkurse gelistet, in die dieser Benutzer eingeschrieben ist.",
"loadCourses": "Eingeschriebene Kurse laden",
"selectCourse": "Kurs",
"selectLesson": "Lektion",
"reset": "Lektion für diesen Nutzer zurücksetzen",
"confirmTitle": "Lektionsfortschritt löschen",
"confirm": "Fortschritt der Lektion „{lesson}“ für {username} wirklich löschen?",
"success": "Lektionsfortschritt wurde zurückgesetzt.",
"error": "Zurücksetzen fehlgeschlagen.",
"pickUserFirst": "Zuerst einen Benutzer auswählen.",
"noEnrolledCourses": "Dieser Benutzer ist in keinem Sprachkurs eingeschrieben.",
"loadCoursesError": "Die Kursliste konnte nicht geladen werden.",
"loadingLessons": "Lektionen werden geladen …"
},
"vocabLessonMarkComplete": {
"divider": "Fortschritt reparieren (ohne Übungsergebnisse zu fälschen)",
"throughLabel": "Alle Lektionen bis Lektionsnummer (einschließlich)",
"hint": "Setzt fehlende oder offene Einträge auf „abgeschlossen“, inkl. Ziel-Score und erster Review-Welle. Bereits abgeschlossene Lektionen bleiben unverändert.",
"submit": "Bis hier als abgeschlossen markieren",
"confirmTitle": "Lektionen als abgeschlossen markieren",
"confirm": "Alle Lektionen mit Nummer ≤ {n} für {username} in diesem Kurs als abgeschlossen markieren?",
"success": "{marked} Lektion(en) neu als abgeschlossen gesetzt ({unchanged} waren bereits erledigt).",
"successNone": "Keine Änderung: alle betroffenen Lektionen ({unchanged}) waren bereits abgeschlossen.",
"error": "Markieren fehlgeschlagen."
},
"adultVerification": {
"title": "[Admin] - Erotik-Freigaben",
"intro": "Volljährige Nutzer können den Erotikbereich beantragen. Hier werden Anfragen geprüft und freigegeben oder abgelehnt.",
"username": "Benutzer",
"age": "Alter",
"statusLabel": "Status",
"requestLabel": "Nachweis",
"actions": "Actes",
"approve": "Freigeben",
"reject": "Ablehnen",
"resetPending": "Auf Prüfung setzen",
"openDocument": "Dokument ansehen",
"previewTitle": "Nachweis-Vorschau",
"closePreview": "Vorschau schließen",
"previewUnavailable": "Für diesen Dateityp ist hier keine Vorschau verfügbar.",
"documentMissing": "Le dossier de preuve n'a pas été trouvé sur le serveur.",
"empty": "Aucune demande correspondante trouvée.",
"loadError": "Les partages n'ont pas pu être chargés.",
"updateError": "Le statut n'a pas pu être modifié.",
"documentError": "Le document n'a pas pu être ouvert.",
"filters": {
"pending": "Ouvrir",
"approved": "Libéré",
"rejected": "Rejeté",
"all": "Tous"
},
"status": {
"none": "Non demandé",
"pending": "En cours d'examen",
"approved": "Libéré",
"rejected": "Rejeté"
},
"messages": {
"approved": "Libération accordée.",
"rejected": "Libération refusée.",
"pending": "Anfrage wieder auf Prüfung gesetzt."
}
},
"eroticModeration": {
"title": "[Admin] - Erotik-Moderation",
"intro": "Gemeldete Erotikbilder und -videos können hier geprüft, verborgen, gelöscht oder gegen den Account eskaliert werden.",
"empty": "Keine passenden Meldungen gefunden.",
"loadError": "Die Meldungen konnten nicht geladen werden.",
"actionError": "Die Moderationsaktion konnte nicht ausgeführt werden.",
"actionSuccess": "Die Moderationsaktion wurde gespeichert.",
"target": "Ziel",
"owner": "Besitzer",
"reporter": "Meldender",
"reason": "Grund",
"statusLabel": "Status",
"meta": "Zeit / Maßnahme",
"actions": "Actes",
"image": "Bild",
"video": "Video",
"hidden": "Verborgen",
"preview": "Vorschau",
"previewError": "Die Vorschau konnte nicht geladen werden.",
"dismiss": "Zurückweisen",
"hide": "Verbergen",
"restore": "Wieder freigeben",
"delete": "Supprimer",
"blockUploads": "Uploads sperren",
"revokeAccess": "Erotikzugang entziehen",
"notePrompt": "Notiz zur Moderationsentscheidung",
"actionLabels": {
"dismiss": "Zurückgewiesen",
"hide_content": "Verborgen",
"restore_content": "Libéré",
"delete_content": "Gelöscht",
"block_uploads": "Uploads gesperrt",
"revoke_access": "Zugang entzogen"
},
"filters": {
"open": "Ouvrir",
"actioned": "Bearbeitet",
"dismissed": "Zurückgewiesen",
"all": "Tous"
},
"status": {
"open": "Ouvrir",
"actioned": "Bearbeitet",
"dismissed": "Zurückgewiesen"
}
},
"rights": {
"add": "Recht hinzufügen",
"select": "Bitte wählen",
"current": "Aktuelle Rechte"
},
"forum": {
"title": "[Admin] - Forum",
"currentForums": "Existierende Foren",
"edit": "Ändern",
"delete": "Supprimer",
"createForum": "Anlegen",
"forumName": "Titel",
"create": "Anlegen",
"permissions": {
"label": "Berechtigungen",
"all": "Tout le monde",
"admin": "Administrateurs uniquement",
"teammember": "Membres de l'équipe uniquement",
"user": "Seulement certains utilisateurs",
"age": "Seulement à partir de 14 ans"
},
"selectPermissions": "Veuillez sélectionner",
"confirmDeleteMessage": "Faut-il vraiment supprimer le forum ?",
"confirmDeleteTitle": "Supprimer le forum"
},
"falukant": {
"edituser": {
"title": "Modifier l'utilisateur Falukant",
"username": "nom d'utilisateur",
"characterName": "Nom du personnage",
"user": "Benutzer",
"success": "Les modifications ont été enregistrées.",
"error": "Les modifications n'ont pas pu être enregistrées.",
"errorLoadingBranches": "Fehler beim Laden der Niederlassungen.",
"errorUpdatingStock": "Fehler beim Aktualisieren des Lagers.",
"stockUpdated": "Lager erfolgreich aktualisiert.",
"search": "Suchen",
"tabs": {
"userdata": "Benutzerdaten",
"branches": "Niederlassungen"
},
"branches": {
"title": "Niederlassungen & Lager",
"noStocks": "Kein Lager vorhanden",
"noBranches": "Keine Niederlassungen gefunden",
"addStock": "Lager hinzufügen",
"stockType": "Lagertyp",
"selectStockType": "Lagertyp auswählen",
"quantity": "Menge",
"allStocksAdded": "Alle verfügbaren Lagertypen sind bereits vorhanden"
},
"errorLoadingStockTypes": "Fehler beim Laden der Lagertypen.",
"errorAddingStock": "Fehler beim Hinzufügen des Lagers.",
"stockAdded": "Lager erfolgreich hinzugefügt.",
"invalidStockData": "Veuillez saisir des informations valides sur le type de stockage et la quantité.",
"pregnancy": {
"title": "Grossesse (Administrateur)",
"characterId": "ID du personnage",
"status": "Status",
"statusActive": "Enceinte jusqu'à",
"statusNone": "Pas enceinte",
"fatherId": "ID du personnage du père (facultatif)",
"fatherSelect": "Vater (Ehepartner / Verlobter / Liebhaber)",
"fatherNone": "— aucun père n'a été sauvé —",
"fatherHintList": "Liste des relations de ce caractère (mariage, fiançailles, histoire d'amour active).",
"fatherHintManual": "Kein passender Partner in der Datenbank: Vater-Charakter-ID manuell eintragen.",
"fatherManualPlaceholder": "ID du personnage",
"dueDays": "Tage bis zum Termin",
"dueDaysHint": "0 = Termin heute (Geburt kann je nach Spiel-Logik zeitnah anstehen).",
"force": "Schwangerschaft setzen",
"clear": "Schwangerschaft entfernen",
"successForce": "Schwangerschaft wurde gesetzt.",
"successClear": "Schwangerschaft wurde entfernt.",
"error": "Aktion fehlgeschlagen.",
"relationship": {
"married": "Ehepartner",
"engaged": "Verlobter",
"lover": "Liebhaber"
}
},
"birth": {
"title": "Geburt erzwingen (Admin)",
"motherHint": "Es wird der oben genannte Charakter (Mutter) verwendet.",
"fatherId": "Vater-Charakter-ID",
"fatherSelect": "Vater (Ehepartner / Verlobter / Liebhaber)",
"fatherChoose": "— Vater wählen —",
"fatherHintList": "Liste aus Beziehungen dieses Charakters.",
"fatherHintManual": "Kein Partner in der Liste: Vater-Charakter-ID manuell eintragen.",
"fatherRequired": "Bitte einen Vater auswählen oder die Charakter-ID angeben.",
"context": "Kontext",
"contextMarriage": "Ehe",
"contextLover": "Liebschaft",
"legitimacy": "Legitimität",
"legitimate": "Legitim",
"ackBastard": "Anerkannt unehelich",
"hiddenBastard": "Verborgen unehelich",
"gender": "Kind-Geschlecht",
"genderRandom": "Zufällig",
"male": "Männlich",
"female": "Weiblich",
"force": "Geburt auslösen",
"success": "Kind wurde angelegt (Taufe ausstehend).",
"error": "Geburt konnte nicht ausgelöst werden."
}
},
"map": {
"title": "Falukant Karten-Editor (Regionen)",
"description": "Zeichne Rechtecke auf der Falukant-Karte und weise sie Städten zu.",
"tabs": {
"regions": "Positionen",
"distances": "Entfernungen"
},
"regionList": "Städte",
"noCoords": "Keine Koordinaten gesetzt",
"currentRect": "Aktuelles Rechteck",
"hintDraw": "Wähle eine Stadt und ziehe mit der Maus ein Rechteck auf der Karte, um die Position festzulegen.",
"saveAll": "Enregistrer toutes les villes modifiées",
"connectionsTitle": "Connexions (region_distance)",
"source": "Depuis",
"target": "Après",
"selectSource": "Sélectionnez la ville source",
"selectTarget": "Sélectionnez la ville de destination",
"mode": "Type de transport",
"modeLand": "pays",
"modeWater": "Eau",
"modeAir": "Air",
"distance": "distance",
"saveConnection": "Enregistrer la connexion",
"pickOnMap": "Auf Karte wählen",
"errorSaveConnection": "Die Verbindung konnte nicht gespeichert werden.",
"errorDeleteConnection": "Die Verbindung konnte nicht gelöscht werden.",
"confirmDeleteConnection": "Verbindung wirklich löschen?"
},
"createNPC": {
"title": "NPCs erstellen",
"region": "Stadt",
"allRegions": "Alle Städte",
"ageRange": "Altersbereich",
"to": "bis",
"years": "Jahre",
"titleRange": "Titel-Bereich",
"count": "Anzahl pro Stadt-Titel-Kombination",
"countHelp": "Diese Anzahl wird für jede Kombination aus gewählter Stadt und Titel erstellt.",
"create": "NPCs erstellen",
"creating": "Erstelle...",
"result": "Ergebnis",
"createdCount": "{count} NPCs wurden erstellt.",
"combinationInfo": "{perCombination} PNJ par combinaison × {combinations} combinaisons = {count} PNJ au total",
"age": "Alter",
"errorLoadingRegions": "Erreur lors du chargement des villes.",
"errorLoadingTitles": "Erreur de chargement des titres.",
"errorCreating": "Erreur lors de la création des PNJ.",
"invalidAgeRange": "Tranche d'âge non valide.",
"invalidTitleRange": "Plage de titres non valide.",
"invalidCount": "Numéro invalide (1-500).",
"progress": "Progrès",
"progressDetails": "{actuel} créé par {total} PNJ",
"timeRemainingSeconds": "Temps restant : {secondes} secondes",
"timeRemainingMinutes": "Temps restant : {minutes} minutes {secondes} secondes",
"almostDone": "Presque fini...",
"jobNotFound": "Emploi introuvable ou expiré."
}
},
"chatrooms": {
"title": "[Admin] - Gérer les salons de discussion",
"roomName": "Raumname",
"create": "Chatraum anlegen",
"edit": "Chatraum bearbeiten",
"type": "Typ",
"isPublic": "Öffentlich sichtbar",
"isAdultOnly": "Nur Erotikbereich",
"actions": "Actes",
"genderRestriction": {
"show": "Geschlechtsbeschränkung aktivieren",
"label": "Geschlechtsbeschränkung"
},
"minAge": {
"show": "Mindestalter angeben",
"label": "Mindestalter"
},
"maxAge": {
"show": "Höchstalter angeben",
"label": "Höchstalter"
},
"password": {
"show": "Passwortschutz aktivieren",
"label": "Passwort"
},
"friendsOfOwnerOnly": "Nur Freunde des Besitzers",
"requiredUserRight": {
"show": "Benötigtes Benutzerrecht angeben",
"label": "Benötigtes Benutzerrecht"
},
"roomtype": {
"chat": "Reden",
"dice": "Würfeln",
"poker": "Poker",
"hangman": "Hangman"
},
"rights": {
"talk": "Reden",
"scream": "Schreien",
"whisper": "Flüstern",
"start game": "Spiel starten",
"open room": "Raum öffnen",
"systemmessage": "Systemnachricht"
},
"confirmDelete": "Soll dieser Chatraum wirklich gelöscht werden?"
},
"match3": {
"title": "Match3 Level verwalten",
"newLevel": "Neues Level erstellen",
"editLevel": "Level bearbeiten",
"deleteLevel": "Level löschen",
"confirmDelete": "Möchtest du dieses Level wirklich löschen?",
"levelName": "Name",
"levelDescription": "Beschreibung",
"boardWidth": "Breite",
"boardHeight": "Höhe",
"moveLimit": "Zug-Limit",
"levelOrder": "Reihenfolge",
"boardLayout": "Board-Layout",
"tileTypes": "Types de tuiles disponibles",
"actions": "Actes",
"edit": "Modifier",
"delete": "Supprimer",
"save": "Sauvegarder",
"cancel": "Annuler",
"update": "Mise à jour",
"create": "Créer",
"boardControls": {
"fillAll": "Activer tout",
"clearAll": "Désactiver tout",
"invert": "Inverser"
},
"loading": "Niveau de chargement...",
"retry": "Essayer à nouveau",
"availableLevels": "Niveaux disponibles : {count}",
"levelFormat": "Niveau {number} : {name}",
"levelObjectives": "Level-Objekte",
"objectivesTitle": "Siegvoraussetzungen",
"addObjective": "Objektiv hinzufügen",
"removeObjective": "Entfernen",
"objectiveType": "Typ",
"objectiveTypeScore": "Punkte sammeln",
"objectiveTypeMatches": "Matches machen",
"objectiveTypeMoves": "Züge verwenden",
"objectiveTypeTime": "Zeit einhalten",
"objectiveTypeSpecial": "Spezialziel",
"objectiveOperator": "Operator",
"operatorGreaterEqual": "Größer oder gleich (≥)",
"operatorLessEqual": "Kleiner oder gleich (≤)",
"operatorEqual": "Gleich (=)",
"operatorGreater": "Größer als (>)",
"operatorLess": "Kleiner als (<)",
"objectiveTarget": "Zielwert",
"objectiveTargetPlaceholder": "z.B. 100",
"objectiveOrder": "Reihenfolge",
"objectiveOrderPlaceholder": "1, 2, 3...",
"objectiveDescription": "Beschreibung",
"objectiveDescriptionPlaceholder": "par ex. Collectez 100 points",
"objectiveRequired": "Requis pour terminer le niveau",
"noObjectives": "Aucune condition de victoire définie. Cliquez sur « Ajouter un objectif » pour en créer un."
},
"userStatistics": {
"title": "[Admin] - Statistiques utilisateur",
"totalUsers": "Nombre total d'utilisateurs",
"genderDistribution": "Répartition par sexe",
"ageDistribution": "Répartition par âge"
},
"taxiTools": {
"title": "Outils de taxi",
"description": "Gérer les plans, niveaux et configurations des taxis",
"mapEditor": {
"title": "Modifier la carte",
"availableMaps": "Cartes disponibles : {count}",
"newMap": "Neue Map erstellen",
"mapFormat": "{name} (Position: {x},{y})",
"mapName": "Map-Name",
"mapDescription": "Beschreibung",
"mapWidth": "Breite",
"mapHeight": "Höhe",
"tileSize": "Tile-Größe",
"positionX": "X-Position",
"positionY": "Y-Position",
"mapType": "Map-Typ",
"mapLayout": "Map-Layout",
"tilePalette": "Tile-Palette",
"streetNames": "Straßennamen",
"extraElements": "Zusätzliche Elemente",
"streetNameHorizontal": "Straßenname (horizontal)",
"streetNameVertical": "Straßenname (vertikal)",
"continueHorizontal": "In anderer Richtung fortführen (→)",
"continueVertical": "In anderer Richtung fortführen (↓)",
"continueOther": "In anderer Richtung fortführen",
"position": "position",
"fillAllRoads": "Toutes les rues",
"clearAll": "Supprimer tout",
"generateRandom": "Générer aléatoirement",
"delete": "Supprimer",
"update": "Mise à jour",
"cancel": "Annuler",
"create": "Créer",
"createSuccess": "La carte a été créée avec succès !",
"updateSuccess": "La carte a été mise à jour avec succès !",
"deleteSuccess": "La carte a été supprimée avec succès !"
}
},
"servicesStatus": {
"title": "Statut des services",
"description": "Surveiller l'état du backend, du chat et du démon",
"status": {
"connected": "Attachés ensemble",
"connecting": "Connecter...",
"disconnected": "Non connecté",
"error": "Erreur",
"unknown": "Inconnu"
},
"backend": {
"title": "Backend",
"connected": "Backend-Service ist erreichbar und verbunden"
},
"chat": {
"title": "Chat",
"connected": "Chat-Service ist erreichbar und verbunden"
},
"daemon": {
"title": "Daemon",
"connected": "Daemon-Service ist erreichbar und verbunden",
"connections": {
"title": "Aktive Verbindungen",
"none": "Keine aktiven Verbindungen",
"userId": "Benutzer-ID",
"username": "nom d'utilisateur",
"connections": "Verbindungen",
"duration": "Verbindungsdauer",
"lastPong": "Zeit seit letztem Pong",
"pingTimeouts": "Ping-Timeouts",
"pongReceived": "Pong empfangen",
"yes": "Ja",
"no": "Nein",
"notConnected": "Démon non connecté",
"sendError": "Erreur lors de l'envoi de la demande",
"error": "Erreur lors de l'obtention des connexions"
},
"websocketLog": {
"title": "Journal WebSocket",
"showLog": "Afficher le journal WebSocket",
"refresh": "Mise à jour",
"loading": "Chargement...",
"close": "Fermer",
"entryCount": "{count} entrées",
"noEntries": "Keine Log-Einträge vorhanden",
"notConnected": "Démon non connecté",
"sendError": "Erreur lors de l'envoi de la demande",
"parseError": "Fehler beim Verarbeiten der Antwort",
"timestamp": "Zeitstempel",
"direction": "Richtung",
"peer": "Peer",
"connUser": "Verbindungs-User",
"targetUser": "Ziel-User",
"event": "Event"
}
}
}
}
}

View File

@@ -0,0 +1,61 @@
{
"blog": {
"posts": "Beiträge",
"noPosts": "Keine Beiträge.",
"newPost": "Neuen Beitrag verfassen",
"title": "Titel",
"publish": "Veröffentlichen",
"pickImage": "Bild auswählen",
"uploadImage": "Bild hochladen",
"list": {
"eyebrow": "Community-Blogs",
"title": "Blogs",
"intro": "Articles, statuts de projets et idées personnelles de la communauté YourPart.",
"create": "Créer un nouveau blog",
"loading": "Charger…",
"empty": "Aucun blog trouvé.",
"by": "depuis",
"unknownAuthor": "Inconnu",
"open": "Vers le blog",
"fallbackExcerpt": "Entrées publiques, réflexions et statuts de projets de la communauté."
},
"view": {
"loading": "Charger…",
"edit": "Modifier",
"entriesCount": "{count} entrées",
"empty": "Aucune entrée disponible.",
"fallbackDescription": "Blog communautaire public sur YourPart.",
"notFoundTitle": "Blog introuvable | VotrePart",
"notFoundDescription": "Le blog demandé n'a pas pu être chargé."
},
"editor": {
"createTitle": "Blog erstellen",
"editTitle": "Blog bearbeiten",
"description": "Beschreibung",
"visibility": "Sichtbarkeit",
"visibilityPublic": "Öffentlich",
"visibilityLoggedIn": "Nur eingeloggte Nutzer",
"ageRange": "Altersbereich",
"gender": "Geschlecht",
"genderMale": "Männlich",
"genderFemale": "Weiblich",
"save": "Sauvegarder",
"newPostTitle": "Neuer Beitrag",
"addPost": "Beitrag hinzufügen",
"shareTitle": "Blog teilen",
"url": "URL",
"copyLink": "Link kopieren",
"shareToFriends": "An Freunde senden",
"emailAddresses": "E-Mail-Adressen (Kommagetrennt)",
"send": "Senden",
"restrictedHint": "Hinweis: Dieser Blog ist nicht öffentlich. Empfänger benötigen ggf. ein Login und passende Alters/Geschlechts-Berechtigung.",
"invalidAgeRange": "Tranche d'âge non valide",
"copySuccess": "Lien copié",
"copyError": "Échec de la copie",
"shareError": "Échec du partage",
"emailError": "L'envoi du mail a échoué",
"friendsSent": "Envoyé à {count} ami(s).",
"emailsSent": "{count} e-mail(s) envoyé(s)."
}
}
}

View File

@@ -0,0 +1,158 @@
{
"chat": {
"multichat": {
"title": "Chat multiple",
"eroticTitle": "Chat érotique",
"autoscroll": "Défilement automatique",
"options": "Possibilités",
"send": "Senden",
"shout": "Schreien",
"action": "action",
"roll": "Würfeln",
"colorpicker": "Choisissez la couleur",
"colorpicker_preview": "Aperçu : ce message utilise la couleur sélectionnée.",
"hex": "HEX",
"invalid_hex": "Ungültiger Hex-Wert",
"hue": "Farbton",
"saturation": "Sättigung",
"lightness": "Helligkeit",
"ok": "Ok",
"cancel": "Annuler",
"placeholder": "Nachricht eingeben...",
"action_select_user": "Bitte Benutzer auswählen",
"action_to": "Aktion an {to}",
"action_phrases": {
"left_room": "wechselt zu Raum",
"leaves_room": "verlässt Raum",
"left_chat": "hat den Chat verlassen."
},
"system": {
"room_entered": "Du hast den Raum \"{room}\" betreten.",
"user_entered_room": "{user} hat den Raum betreten.",
"user_left_room": "{user} hat den Raum verlassen.",
"color_changed_self": "Du hast deine Farbe zu {color} geändert.",
"color_changed_user": "{user} hat seine/ihre Farbe zu {color} geändert."
},
"status": {
"connecting": "Verbinden…",
"connected": "Attachés ensemble",
"disconnected": "Getrennt",
"error": "Fehler bei der Verbindung"
},
"reloadRooms": "Räume neu laden",
"createRoom": {
"toggleShowChat": "Chat anzeigen",
"toggleCreateRoom": "Raum anlegen",
"title": "Neuen Raum erstellen",
"commandPrefix": "Kommando",
"labels": {
"roomName": "Raumname",
"visibility": "Sichtbarkeit",
"gender": "Geschlecht",
"minAge": "Mindestalter",
"maxAge": "Höchstalter",
"password": "Passwort",
"rightId": "Benötigtes Recht",
"typeId": "Raumtyp",
"friendsOnly": "friends_only=true"
},
"placeholders": {
"roomName": "z. B. Lounge",
"password": "ohne Leerzeichen"
},
"options": {
"none": "(keine)",
"visibilityPublic": "Öffentlich",
"visibilityPrivate": "Privat",
"genderMale": "Männlich",
"genderFemale": "Weiblich",
"genderAny": "Alle / Keine Einschränkung"
},
"actions": {
"create": "Raum erstellen",
"reset": "Zurücksetzen"
},
"validation": {
"roomNameRequired": "Raumname ist erforderlich.",
"minAgeInvalid": "min_age muss >= 0 sein.",
"maxAgeInvalid": "max_age muss >= 0 sein.",
"ageRangeInvalid": "min_age ne peut pas être supérieur à max_age.",
"passwordSpaces": "Le mot de passe ne peut pas contenir d'espaces.",
"rightIdInvalid": "right_id doit être > 0.",
"typeIdInvalid": "type_id doit être > 0."
},
"messages": {
"noConnection": "Aucune connexion au serveur de chat.",
"invalidForm": "Veuillez corriger vos entrées dans le formulaire de chambre.",
"roomNameMissing": "Veuillez fournir un nom de chambre.",
"sent": "Création de salle envoyée : {commande}",
"created": "La salle \"{room}\" a été créée avec succès.",
"createNotConfirmed": "La chambre \"{room}\" n'a pas encore été confirmée. Veuillez consulter la liste des chambres."
},
"ownedRooms": {
"title": "Mes pièces créées",
"hint": "Löschen per Daemon-Befehl: /dr <raumname> (Alias: /delete_room <raumname>)",
"empty": "Du hast noch keine eigenen Räume.",
"public": "public",
"private": "private",
"confirmDelete": "Soll der Raum \"{room}\" wirklich gelöscht werden?",
"deleteSent": "Löschbefehl gesendet: /dr {room}",
"deleteError": "Raum konnte nicht gelöscht werden."
},
"rights": {
"mainadmin": "Hauptadministrator",
"contactrequests": "Kontaktanfragen",
"users": "Benutzer",
"userrights": "Benutzerrechte",
"forum": "Forum",
"interests": "Interessen",
"falukant": "Falukant",
"minigames": "Minispiele",
"match3": "Match3",
"taxiTools": "Outils de taxi",
"chatrooms": "Chaträume",
"servicesStatus": "Statut des services"
},
"types": {}
},
"password": {
"title": "Passwort erforderlich",
"inputLabel": "Entrez le mot de passe",
"submit": "Rejoindre",
"cancel": "Annuler",
"requiredPrompt": "La salle \"{room}\" est protégée par mot de passe. Veuillez entrer le mot de passe :",
"invalidPrompt": "Mot de passe incorrect pour \"{room}\". Veuillez saisir à nouveau :",
"cancelled": "Rejoindre \"{room}\" annulé.",
"empty": "Le mot de passe ne peut pas être vide."
}
},
"randomchat": {
"title": "Discussion aléatoire",
"age": "Alter",
"gender": {
"title": "Votre sexe",
"male": "Männlich",
"female": "Weiblich"
},
"start": "Commencer",
"agerange": "Alter",
"gendersearch": "Genres",
"camonly": "Uniquement avec caméra",
"showcam": "Montrez votre propre caméra",
"addfriend": "Zu Freunden hinzufügen",
"close": "Chat beenden",
"autosearch": "Automatisch suchen",
"input": "Ihr Text",
"waitingForMatch": "Warten auf einen Teilnehmer...",
"chatpartner": "Du chattest jetzt mit einer <gender> Person im Alter von <age> Jahren.",
"partnergenderm": "männlichen",
"partnergenderf": "weiblichen",
"self": "Du",
"partner": "Partner",
"jumptonext": "Diesen Chat beenden",
"userleftchat": "Der Gesprächstpartner hat den Chat verlassen.",
"startsearch": "Suche nächstes Gespräch",
"selfstopped": "Du hast das Gespräch verlassen."
}
}
}

View File

@@ -0,0 +1,7 @@
{
"error": {
"title": "Fehler aufgetreten",
"close": "Fermer",
"credentialsinvalid": "Die Zugangsdaten sind nicht korrekt."
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
{
"friends": {
"kicker": "Communauté",
"intro": "Amitiés, demandes ouvertes et contacts continus en un seul endroit.",
"title": "Amis",
"stats": {
"existing": "Existant",
"open": "Ouvrir"
},
"tabs": {
"existing": "Existant",
"rejected": "Rejeté",
"pending": "En attente",
"requested": "Demandé"
},
"actions": {
"end": "Beenden",
"accept": "Annehmen",
"reject": "Ablehnen",
"withdraw": "Retirer"
},
"headers": {
"name": "Name",
"age": "Alter",
"gender": "Geschlecht",
"actions": "Actes"
}
}
}

View File

@@ -0,0 +1,191 @@
{
"welcome": "Bienvenue sur YourPart",
"imprint": {
"title": "Mentions légales",
"button": "Mentions légales"
},
"dataPrivacy": {
"title": "Politique de confidentialité",
"button": "Politique de confidentialité"
},
"contact": {
"title": "Contact",
"button": "Contact"
},
"error-title": "Erreur",
"warning-title": "Avertissement",
"info-title": "Information",
"dialog": {
"contact": {
"email": "Adresse e-mail",
"name": "Nom",
"message": "Votre message pour nous",
"accept": "Votre adresse e-mail sera temporairement stockée dans notre système. Une fois votre demande traitée, l'adresse e-mail sera supprimée du système.",
"acceptdatasave": "J'accepte le stockage temporaire de mon adresse e-mail.",
"accept2": "Malheureusement, nous ne pouvons pas vous répondre sans ce consentement."
}
},
"general": {
"datetimelong": "dd.MM.yyyy HH:mm:ss",
"loading": "Chargement...",
"back": "Retour",
"cancel": "Annuler",
"ok": "OK",
"yes": "Oui",
"no": "Non"
},
"OK": "OK",
"Cancel": "Annuler",
"yes": "Oui",
"no": "Non",
"message": {
"close": "Fermer"
},
"appShell": {
"header": {
"tagline": "Plateforme communautaire",
"beta": "Bêta",
"backend": "Backend",
"daemon": "Daemon",
"language": "Langue"
},
"footer": {
"systemLabel": "Système",
"noOpenDialogs": "Pas de dialogues ouverts",
"activeWindows": "{count} fenêtre(s) active(s)",
"systemReady": "Système prêt",
"systemStatusUnavailable": "L'état du système n'est actuellement pas directement disponible dans cette vue."
}
},
"widgets": {
"dashboard": {
"dragHandle": "Déplacer",
"loading": "Chargement..."
},
"birthdays": {
"today": "Aujourd'hui!",
"tomorrow": "Demain",
"turningAge": "(aura {age})",
"inDays": "{count} jours",
"empty": "Aucun anniversaire d'ami visible"
},
"upcoming": {
"today": "Aujourd'hui",
"tomorrow": "Demain",
"timeAt": "{time} h",
"allDay": "Toute la journée",
"empty": "Aucun rendez-vous à venir"
},
"appointments": {
"title": "📅 Rendez-vous",
"loading": "Chargement des rendez-vous...",
"empty": "Aucune date à venir",
"loadError": "Les rendez-vous n'ont pas pu être chargés"
},
"list": {
"noEntries": "Aucune entrée",
"entriesCount": "({count} entrées)",
"fieldsCount": "({count} champs)"
},
"news": {
"emptyValue": "—"
},
"falukant": {
"emptyValue": "—"
},
"vocabCourses": {
"empty": "Vous n'êtes pas actuellement inscrit à un cours de vocabulaire.",
"browseCourses": "Découvrez les cours",
"unnamedCourse": "Cours sans titre",
"lessonLine": "Leçon n° {number} : {title}",
"noLessons": "Pas encore de cours dans ce cours.",
"allDone": "Terminé toutes les leçons",
"openLesson": "À la leçon",
"openCourse": "Au cours"
}
},
"gender": {
"male": "Männlich",
"female": "Weiblich",
"transmale": "Homme trans",
"transfemale": "Femme trans",
"nonbinary": "Non binaire"
},
"common": {
"edit": "Modifier",
"delete": "Supprimer",
"create": "Créer",
"update": "Mise à jour",
"save": "Sauvegarder",
"add": "Ajouter",
"cancel": "Annuler",
"yes": "Ja",
"no": "Nein"
},
"sectionBar": {
"sections": {
"default": "Zone",
"falukant": "Falukant",
"vocab": "Entraîneur de vocabulaire",
"forum": "Forum",
"community": "Communauté",
"settings": "Paramètres",
"administration": "administration",
"minigames": "Minispiele",
"personal": "Personnel",
"blog": "Blogue"
},
"titles": {
"friends": "Amis",
"guestbook": "Livre d'or",
"search": "Recherche",
"gallery": "galerie",
"forum": "Forum",
"topic": "Thème",
"diary": "agenda",
"languages": "Langues",
"newLanguage": "Nouvelle langue",
"subscribeLanguage": "Abonnez-vous à la langue",
"language": "Langue",
"chapter": "Chapitre",
"courses": "Cours",
"course": "Kurs",
"lesson": "Lektion",
"createCharacter": "Créer un personnage",
"overview": "Übersicht",
"branch": "bifurquer",
"moneyHistory": "Histoire de l'argent",
"family": "Familie",
"house": "Maison",
"nobility": "Noblesse",
"reputation": "Ansehen",
"church": "Kirche",
"education": "Éducation",
"bank": "banque",
"directors": "Directeurs",
"health": "Santé",
"politics": "politique",
"underground": "souterrain",
"personalSettings": "Données personnelles",
"viewSettings": "Avis",
"sexualitySettings": "sexualité",
"flirtSettings": "flirter",
"accountSettings": "compte",
"languageAssistantSettings": "Assistant vocal",
"interests": "Interessen",
"adminInterests": "Gestion des intérêts",
"adminUsers": "Benutzer",
"adminUserStatistics": "Statistiques des utilisateurs",
"adminContacts": "Kontaktanfragen",
"adminUserRights": "droite",
"adminForums": "Gestion des forums",
"adminChatRooms": "Chaträume",
"adminFalukantUsers": "Utilisateur Falukant",
"adminFalukantMap": "Carte de Falukant",
"adminCreateNpc": "Créer un PNJ",
"adminMinigames": "Gestion des Match3",
"adminTaxiTools": "Outils de taxi",
"adminServicesStatus": "Statut des services"
}
}
}

View File

@@ -0,0 +1,5 @@
{
"logo": "logo",
"title": "VotrePart",
"advertisement": "Publicité"
}

View File

@@ -0,0 +1,96 @@
{
"home": {
"dashboard": {
"kicker": "Votre région",
"title": "Content de te revoir!",
"subtitle": "Votre entrée personnelle dans la communauté, vos rendez-vous, Falukant et vos activités en cours.",
"edit": "Modifier le tableau de bord",
"addWidget": "+ Ajouter un widget...",
"addAgain": "Ajouter à nouveau",
"done": "Prêt",
"sectionTitle": "Votre aperçu",
"sectionIntro": "Les widgets peuvent être déplacés et ajustés en mode édition.",
"widgetTitlePlaceholder": "Titel",
"removeWidget": "Supprimer le widget",
"remove": "Entfernen",
"empty": "Aucun widget pour l'instant. Cliquez sur « Modifier le tableau de bord », puis sur « +Ajouter un widget ».",
"defaultAppointmentsWidget": "Rendez-vous",
"loadError": "Le tableau de bord n'a pas pu être chargé.",
"saveError": "Le tableau de bord n'a pas pu être enregistré.",
"widgetLabels": {
"appointments": "Rendez-vous",
"falukant": "Falukant",
"news": "Nouvelles",
"birthdays": "Anniversaires",
"upcoming": "Prochaines dates",
"calendar": "calendrier",
"vocabCourses": "Cours de langue"
},
"overview": {
"activeWidgetsLabel": "Widgets actifs",
"activeWidgetsText": "Votre tableau de bord est modulable et peut être réorganisé à tout moment.",
"availableModulesLabel": "Modules disponibles",
"availableModulesText": "Vous pouvez combiner les modules communauté, calendrier, actualités et Falukant.",
"editModeLabel": "Mode édition",
"editModeActive": "Aktiv",
"editModeInactive": "De",
"editModeActiveText": "Les widgets peuvent actuellement être ajoutés et personnalisés.",
"editModeInactiveText": "Le contenu reste ciblé et facile à lire."
}
},
"vocabLanding": {
"eyebrow": "Apprendre des langues en ligne",
"title": "L'entraîneur de vocabulaire de YourPart combine apprentissage, cours et exercices sur une seule plateforme.",
"lead": "Travaillez avec des leçons interactives, élargissez votre vocabulaire et utilisez du contenu structuré pour un flux d'apprentissage motivant directement dans le navigateur.",
"cta": "Commencez gratuitement",
"feature1Title": "Cours interactifs",
"feature1Text": "Les cours, leçons et exercices vous aident à développer systématiquement de nouvelles compétences linguistiques.",
"feature2Title": "Orienté vers la pratique",
"feature2Text": "Le vocabulaire, la grammaire et la répétition sont alignés sur une routine d'apprentissage adaptée à un usage quotidien.",
"feature3Title": "Fait partie d'une communauté",
"feature3Text": "L'espace linguistique est intégré dans une plateforme communautaire plus large avec des blogs, un forum et un chat."
},
"betaNoticeLabel": "Remarque bêta :",
"betaNoticeText": "YourPart est en cours de développement actif. Les fonctions peuvent être incomplètes, le contenu manque encore et des modifications peuvent survenir.",
"nologin": {
"welcome": "Bienvenue dans votrePartie",
"description": "yourPart est un réseau social où vous pouvez vous faire des amis et rencontrer de nouvelles personnes. Ici, vous pouvez afficher vos images à d'autres personnes et contrôler qui peut voir quelles images. Bien entendu, vous pouvez également échanger des messages et même discuter. A grande échelle, avec beaucoup d'autres en même temps, et dans de petites discussions aléatoires. Et n'oubliez pas que vous pouvez également filmer ici.",
"introHtml": "YourPart est une plateforme en ligne en pleine croissance qui combine des fonctions communautaires, un chat en temps réel, des forums, un réseau social avec une galerie d'images et le jeu de construction <em>Falukant</em>. Le site est actuellement en phase bêta - nous développons continuellement les fonctions, le contenu et la stabilité.",
"expected": {
"title": "Ce qui t'attend",
"items": {
"chat": "<strong>Chat</strong> : espaces publics, rencontres aléatoires (chat aléatoire) et ajustements de couleurs.",
"social": "<strong>Réseau social</strong> : Profil, amitiés, galerie de photos avec visibilité.",
"forum": "<strong>Forum</strong> : créer des sujets, rédiger des messages, droits de modération (basés sur les rôles).",
"falukant": "<strong>Falukant</strong> : Affaires et vie quotidienne gérez les succursales, produisez, stockez, vendez.",
"minigames": "<strong>Mini-jeux</strong> : par ex. B. Niveau Match 3 court divertissement entre les deux.",
"multilingual": "<strong>Multilingue</strong> : allemand/anglais le contenu est continuellement mis à jour."
}
},
"falukantShort": {
"title": "Falukant brièvement expliqué",
"text": "Chez Falukant, vous dirigez des entreprises, développez vos connaissances, optimisez la production et les ventes, surveillez les prix et réagissez aux événements. Les notifications vous informent des changements de statut en temps réel."
},
"privacyBeta": {
"title": "Protection des données et statut bêta",
"text": "YourPart est en version bêta. Des modifications, des échecs et des traductions manquantes peuvent survenir. Nous valorisons la protection des données et la transparence ; plus d'informations suivront pendant la version bêta."
},
"getStarted": {
"title": "Se joindre à",
"text": "Vous pouvez déjà utiliser la plateforme, la tester et donner votre avis. Inscrivez-vous via « {register} » ou démarrez le chat aléatoire sans engagement."
},
"randomchat": "Discussion aléatoire",
"startrandomchat": "Démarrer une discussion aléatoire",
"login": {
"name": "Nom de connexion",
"namedescription": "Entrez votre nom d'utilisateur ici",
"password": "Passwort",
"passworddescription": "Entrez votre mot de passe ici",
"lostpassword": "Mot de passe oublié",
"register": "Inscrivez-vous avec votrePart",
"stayLoggedIn": "Restez connecté",
"submit": "Se connecter"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"message": {
"title": "avis",
"close": "Fermer",
"test": "Travaux d'essai",
"success": "L'action a été couronnée de succès."
}
}

View File

@@ -0,0 +1,107 @@
{
"minigames": {
"title": "Minispiele",
"description": "Découvrez une collection de mini-jeux amusants !",
"play": "Jouer",
"backToGames": "Retour aux jeux",
"comingSoon": {
"title": "À venir",
"description": "Des jeux plus passionnants sont en développement !"
},
"match3": {
"title": "Match 3 - Campagne Joyaux",
"description": "Connectez trois joyaux identiques ou plus pour collecter des points !",
"campaignDescription": "Jouez à tous les niveaux et collectez des étoiles !",
"gameStats": "Statistiques du jeu",
"score": "Points",
"moves": "Trains",
"currentLevel": "Niveau actuel",
"level": "niveau",
"stars": "Étoiles",
"movesLeft": "Trains restants",
"restartLevel": "Niveau de redémarrage",
"pause": "pause",
"resume": "Continue à jouer",
"paused": "Jeu en pause",
"levelComplete": "Niveau terminé !",
"levelScore": "Note de niveau",
"movesUsed": "Trains utilisés",
"starsEarned": "Étoiles reçues",
"nextLevel": "Niveau suivant",
"campaignComplete": "Campagne terminée !",
"totalScore": "Total de points",
"totalStars": "Étoiles globales",
"levelsCompleted": "Niveaux terminés",
"restartCampaign": "Relancer la campagne",
"nextStep": "Étape suivante",
"objectivesCollapse": "Réduire les objectifs",
"objectivesShow": "Afficher les objectifs",
"objectives": "Objectifs",
"loadingBoard": "Le plateau de jeu est en préparation...",
"loadingHint": "Les données de niveau, les objectifs et la disposition du terrain sont actuellement en cours de synchronisation."
},
"taxi": {
"title": "Simulateur de taxi",
"description": "Conduisez des passagers dans la ville et gagnez de l'argent !",
"gameStats": "Statistiques du jeu",
"score": "Points",
"money": "Argent",
"passengers": "passagers",
"currentLevel": "Niveau actuel",
"level": "niveau",
"fuel": "carburant",
"fuelLeft": "Carburant restant",
"restartLevel": "Niveau de redémarrage",
"pause": "pause",
"resume": "Continue à jouer",
"paused": "Jeu en pause",
"levelComplete": "Niveau terminé !",
"levelScore": "Note de niveau",
"moneyEarned": "Argent gagné",
"passengersDelivered": "Passagers transportés",
"nextLevel": "Niveau suivant",
"campaignComplete": "Campagne terminée !",
"totalScore": "Total de points",
"totalMoney": "Argent total",
"levelsCompleted": "Niveaux terminés",
"restartCampaign": "Relancer la campagne",
"pickupPassenger": "Récupérer le passager",
"deliverPassenger": "Livrer un passager",
"refuel": "Ravitailler",
"startEngine": "Démarrer le moteur",
"stopEngine": "Arrêter le moteur",
"controls": "pilotage",
"accelerate": "Appuyer sur le champignon",
"brake": "Freins",
"steerRight": "Dirigez-vous vers la droite",
"steerLeft": "Dirigez-vous vers la gauche",
"goals": "Objectifs",
"avoidCollisions": "Évitez les collisions avec d'autres véhicules",
"streetNames": "Straßennamen",
"remainingVehicles": "Véhicules restants",
"fuelTitle": "carburant",
"pointsTitle": "Points",
"speedViolations": "excès de vitesse",
"redLightsPassed": "Faire passer des feux rouges",
"highscore": "Note élevée",
"topPlayers": "Les 20 meilleurs joueurs",
"loadingHighscore": "Chargement des meilleurs scores...",
"noHighscore": "Pas encore de scores élevés",
"pointsShort": "Pt",
"backToGame": "Retour au jeu",
"minimap": "Mini-carte",
"loadedPassengers": "Passagers invités",
"waitingPassengers": "Passagers en attente",
"noPassengersInTaxi": "Aucun passager dans le taxi",
"noWaitingPassengers": "Pas de passagers en attente",
"name": "Name",
"destination": "Ziel",
"bonus": "prime",
"time": "Temps",
"crash": {
"title": "Accident!",
"message": "Vous avez eu un accident ! Crashes : {crashs}"
}
}
}
}

View File

@@ -0,0 +1,119 @@
{
"navigation": {
"home": "Page d'accueil",
"logout": "Se déconnecter",
"friends": "Amis",
"socialnetwork": "Point de rencontre",
"chats": "Discussions",
"falukant": "Falukant",
"minigames": "Minispiele",
"personal": "Persönliches",
"settings": "Paramètres",
"administration": "administration",
"m-chats": {
"multiChat": "Chat multi-utilisateur",
"randomChat": "Chat pour célibataires aléatoires",
"eroticChat": "Chat érotique"
},
"m-socialnetwork": {
"guestbook": "Livre d'or",
"blog": "Blogue",
"usersearch": "Recherche d'utilisateurs",
"forum": "Forum",
"gallery": "galerie",
"sprachenlernen": "Apprendre des langues",
"blockedUsers": "Utilisateurs bloqués",
"oneTimeInvitation": "Invitations uniques",
"diary": "agenda",
"erotic": "érotisme",
"m-erotic": {
"pictures": "Photos",
"videos": "vidéos"
},
"m-sprachenlernen": {
"vocabtrainer": "Entraîneur de vocabulaire",
"sprachkurse": "Cours de langue",
"m-vocabtrainer": {
"newLanguage": "Nouvelle langue"
}
}
},
"m-minigames": {
"match3": "Match 3 - Bijoux",
"taxi": "Simulateur de taxi"
},
"m-personal": {
"sprachenlernen": "Apprendre des langues",
"calendar": "calendrier",
"m-sprachenlernen": {
"vocabtrainer": "Entraîneur de vocabulaire",
"sprachkurse": "Cours de langue",
"m-vocabtrainer": {
"newLanguage": "Nouvelle langue"
}
}
},
"m-settings": {
"homepage": "Page d'accueil",
"account": "compte",
"personal": "Persönliches",
"view": "Regarder",
"flirt": "flirter",
"interests": "Interessen",
"notifications": "Notifications",
"sexuality": "sexualité",
"languageAssistant": "Assistant vocal"
},
"m-administration": {
"contactrequests": "Kontaktanfragen",
"users": "Benutzer",
"userrights": "Benutzerrechte",
"m-users": {
"userlist": "Liste des utilisateurs",
"adultverification": "Sorties érotiques",
"eroticmoderation": "Modération érotique",
"userstatistics": "Statistiques des utilisateurs",
"userrights": "Benutzerrechte"
},
"forum": "Forum",
"interests": "Interessen",
"falukant": "Falukant",
"m-falukant": {
"logentries": "Entrées de journal",
"edituser": "Modifier l'utilisateur",
"database": "base de données",
"mapEditor": "Editeur de cartes",
"createNPC": "NPCs erstellen"
},
"minigames": "Minispiele",
"m-minigames": {
"match3": "Niveaux Match3",
"taxiTools": "Outils de taxi"
},
"chatrooms": "Chaträume",
"servicesStatus": "Statut des services"
},
"m-friends": {
"manageFriends": "Gérer les amis",
"chat": "Chat",
"profile": "profil"
},
"m-falukant": {
"create": "Créer",
"overview": "Übersicht",
"towns": "Niederlassungen",
"factory": "production",
"family": "Familie",
"house": "Maison",
"darknet": "souterrain",
"reputation": "Reputation",
"moneyhistory": "Des flux de trésorerie",
"nobility": "Sozialstatus",
"politics": "politique",
"education": "Éducation",
"health": "Santé",
"bank": "banque",
"church": "Kirche"
}
}
}

View File

@@ -0,0 +1,13 @@
{
"passwordReset": {
"title": "Réinitialiser le mot de passe",
"email": "e-mail",
"reset": "Zurücksetzen",
"success": "Si l'e-mail existe, des instructions de réinitialisation ont été envoyées.",
"failure": "La réinitialisation du mot de passe a échoué. Veuillez réessayer plus tard.",
"emailHint": "Nous enverrons le lien à l'adresse e-mail fournie.",
"validation": {
"invalidEmail": "S'il vous plaît, mettez une adresse email valide."
}
}
}

View File

@@ -0,0 +1,81 @@
{
"personal": {
"calendar": {
"title": "calendrier",
"kicker": "planification",
"intro": "Rendez-vous, anniversaires et vos propres entrées dans un aperçu structuré.",
"today": "Aujourd'hui",
"newEntry": "Nouvelle entrée",
"editEntry": "Modifier l'entrée",
"selectedDays": "{count} jours sélectionnés",
"createEventForSelection": "Créer un rendez-vous",
"clearSelection": "Désélectionner",
"allDay": "Toute la journée",
"views": {
"month": "Mois",
"week": "Semaine",
"workweek": "semaine de travail",
"day": "jour"
},
"weekdays": {
"mon": "Mo",
"tue": "Mar",
"wed": "Épouser",
"thu": "faire",
"fri": "MS",
"sat": "Assis",
"sun": "Donc"
},
"weekdaysFull": {
"mon": "Lundi",
"tue": "Mardi",
"wed": "Mercredi",
"thu": "Jeudi",
"fri": "Vendredi",
"sat": "Samedi",
"sun": "Dimanche"
},
"months": {
"jan": "Janvier",
"feb": "Février",
"mar": "Mars",
"apr": "Avril",
"may": "Peut",
"jun": "Juin",
"jul": "Juillet",
"aug": "Août",
"sep": "Septembre",
"oct": "Octobre",
"nov": "Novembre",
"dec": "Décembre"
},
"categories": {
"personal": "Personnel",
"work": "Travail",
"family": "Familie",
"health": "Santé",
"birthday": "Anniversaire",
"holiday": "Vacances",
"reminder": "Mémoire",
"other": "Divers"
},
"form": {
"title": "Titel",
"titlePlaceholder": "Entrez le titre...",
"category": "catégorie",
"startDate": "Date de début",
"startTime": "Heure de début",
"endDate": "Date de fin",
"endTime": "Fin des temps",
"allDay": "Toute la journée",
"description": "Beschreibung",
"descriptionPlaceholder": "Description facultative...",
"save": "Sauvegarder",
"cancel": "Annuler",
"delete": "Supprimer",
"saveError": "Erreur lors de l'enregistrement du rendez-vous",
"deleteError": "Erreur lors de la suppression du rendez-vous"
}
}
}
}

View File

@@ -0,0 +1,30 @@
{
"register": {
"title": "Inscrivez-vous avec votrePart",
"email": "Adresse email",
"username": "nom d'utilisateur",
"password": "Passwort",
"repeatPassword": "Répéter le mot de passe",
"language": "Langue",
"languages": {
"en": "Anglais",
"de": "Allemand",
"ceb": "Bisaya",
"es": "Espagnol",
"fr": "Français"
},
"register": "Registre",
"close": "Fermer",
"failure": "Une erreur s'est produite.",
"success": "Vous avez été inscrit avec succès. Veuillez vérifier votre boîte de réception dès maintenant pour activer votre accès.",
"passwordMismatch": "Les mots de passe ne correspondent pas.",
"emailinuse": "L'adresse e-mail est déjà utilisée.",
"usernameinuse": "Le nom d'utilisateur n'est pas disponible.",
"validation": {
"invalidEmail": "S'il vous plaît, mettez une adresse email valide.",
"usernameTooShort": "Le nom d'utilisateur doit comporter au moins 3 caractères.",
"passwordHint": "Au moins 8 caractères.",
"passwordTooShort": "Le mot de passe est encore trop court."
}
}
}

View File

@@ -0,0 +1,51 @@
{
"seo": {
"default": {
"title": "YourPart - communauté, chat, forum, entraîneur de vocabulaire, Falukant et mini-jeux",
"description": "YourPart combine communauté, chat, forum, blogs, entraîneurs de vocabulaire, le jeu de construction Falukant et des mini-jeux par navigateur sur une seule plateforme.",
"keywords": "YourPart, communauté, chat, forum, blog, entraîneur de vocabulaire, Falukant, mini-jeux, gratuit, privé, protection des données"
},
"home": {
"title": "YourPart - communauté, chat, forum, blogs, entraîneur de vocabulaire et jeux",
"description": "YourPart est une plateforme communautaire avec chat, forum, blogs, entraîneur de vocabulaire, le jeu de construction par navigateur Falukant et des mini-jeux.",
"keywords": "YourPart, communauté, chat, forum, blogs, entraîneur de vocabulaire, jeu par navigateur, Falukant, mini-jeux, gratuit, privé",
"jsonLdDescription": "Plateforme communautaire avec chat, forum, blogs, entraîneur de vocabulaire, Falukant et mini-jeux par navigateur."
},
"falukant": {
"title": "Falukant - Jeu de construction de navigateur médiéval sur YourPart",
"description": "Falukant est le jeu de construction par navigateur médiéval sur YourPart avec le commerce, la politique, la famille, l'éducation et le développement du caractère.",
"keywords": "Falukant, jeu par navigateur, jeu de construction, jeu médiéval, jeu économique, jeu politique, YourPart, gratuit",
"jsonLdDescription": "Jeu de construction par navigateur médiéval avec développement du commerce, de la politique, de la famille et du personnage.",
"jsonLdName": "Falukant"
},
"minigames": {
"title": "Mini-jeux sur YourPart - Match 3 et Taxi dans le navigateur",
"description": "Découvrez les mini-jeux par navigateur sur YourPart : Match 3 et Taxi proposent des parties rapides directement sur la plateforme.",
"keywords": "Mini-jeux, jeux par navigateur, match 3, jeu de taxi, jeux occasionnels, YourPart, gratuit",
"jsonLdDescription": "Mini-jeux par navigateur sur YourPart avec Match 3 et Taxi.",
"jsonLdCollectionName": "Mini-jeux YourPart"
},
"vocab": {
"title": "Entraîneur de vocabulaire sur YourPart - apprenez les langues en ligne",
"description": "L'entraîneur de vocabulaire de YourPart vous accompagne dans l'apprentissage des langues avec des leçons, des cours et des exercices interactifs.",
"keywords": "Entraîneur de vocabulaire, apprendre des langues, apprendre en ligne, cours de langue, exercices, YourPart, gratuit",
"jsonLdDescription": "Entraîneur de vocabulaire interactif avec cours, leçons et exercices pour l'apprentissage des langues.",
"jsonLdName": "Entraîneur de vocabulaire YourPart"
},
"blogList": {
"title": "Blogs sur YourPart  Articles et sujets de la communauté",
"description": "Découvrez des blogs publics sur YourPart avec des contributions, des réflexions, des expériences et des sujets de la communauté dans divers domaines.",
"keywords": "Blogs, blog communautaire, articles, publications, YourPart",
"jsonLdDescription": "Blogs publics et publications de la communauté sur YourPart.",
"jsonLdName": "Blogs de votre part"
},
"blogPage": {
"title": "Blogs de votre part",
"description": "Blogs publics, articles et contenu communautaire sur YourPart.",
"keywords": "Blog, VotrePart, Communauté"
},
"blogPost": {
"pageTitle": "{titre} | Blog de votre part"
}
}
}

View File

@@ -0,0 +1,249 @@
{
"settings": {
"personal": {
"title": "Données personnelles",
"label": {
"language": "Langue",
"birthdate": "date de naissance",
"gender": "Geschlecht",
"town": "Stadt",
"zip": "Code postal",
"eyecolor": "Couleur des yeux",
"haircolor": "couleur de cheveux",
"hairlength": "longueur des cheveux",
"skincolor": "Couleur de peau",
"freckles": "Taches de rousseur",
"weight": "Poids",
"bodyheight": "Größe",
"piercings": "piercings",
"tattoos": "Tatouages",
"sexualpreference": "Alignement",
"pubichair": "poils pubiens",
"penislength": "Longueur du pénis",
"brasize": "Taille du soutien-gorge",
"willChildren": "je veux des enfants",
"smokes": "Fumée",
"drinks": "je bois de l'alcool",
"hasChildren": "j'ai des enfants",
"interestedInGender": "Intéressé par"
},
"tooltip": {
"language": "Langue",
"birthdate": "date de naissance",
"gender": "Geschlecht",
"town": "Stadt",
"zip": "Code postal",
"eyecolor": "Couleur des yeux",
"haircolor": "couleur de cheveux",
"hairlength": "longueur des cheveux",
"skincolor": "Couleur de peau",
"freckles": "Taches de rousseur",
"weight": "Poids",
"bodyheight": "Größe",
"piercings": "piercings",
"tattoos": "Tatouages",
"sexualpreference": "Alignement",
"pubichair": "poils pubiens",
"penislength": "Longueur du pénis",
"brasize": "Taille du soutien-gorge"
},
"gender": {
"male": "Männlich",
"female": "Weiblich",
"transmale": "Femme trans",
"transfemale": "Homme trans",
"nonbinary": "Non binaire"
},
"language": {
"de": "Allemand",
"en": "Anglais",
"ceb": "Bisaya",
"es": "Espagnol",
"fr": "Français"
},
"eyecolor": {
"blue": "Bleu",
"green": "Vert",
"brown": "Brun",
"black": "Noir",
"grey": "Gris",
"hazel": "noisette",
"amber": "ambre",
"red": "Rouge",
"other": "Autre"
},
"haircolor": {
"black": "Noir",
"brown": "Brun",
"blonde": "Blond",
"red": "Rouge",
"grey": "Gris",
"white": "Blanc",
"other": "Autre"
},
"hairlength": {
"short": "Court",
"medium": "Moyen",
"long": "Long",
"bald": "Chauve",
"other": "Autre"
},
"skincolor": {
"light": "Brillant",
"medium": "Moyen",
"dark": "Sombre",
"other": "Autre"
},
"freckles": {
"much": "Beaucoup",
"medium": "Moyen",
"less": "Peu",
"none": "Non"
},
"sexualpreference": {
"straight": "Hétérosexuel",
"gay": "Gays",
"bi": "Bisexuel",
"asexual": "Asexué",
"pan": "Pansexuel"
},
"pubichair": {
"none": "Non",
"short": "Court",
"medium": "Moyen",
"long": "Long",
"hairy": "Naturellement",
"waxed": "Enlèvement de cire chaude",
"landingstrip": "piste",
"bikinizone": "Zone bikini uniquement",
"other": "Autre"
},
"interestedInGender": {
"male": "Hommes",
"female": "Femmes"
},
"smokes": {
"often": "Souvent",
"socially": "En compagnie",
"daily": "Tous les jours",
"never": "Jamais"
},
"drinks": {
"often": "Souvent",
"socially": "En compagnie",
"daily": "Tous les jours",
"never": "Jamais"
}
},
"view": {
"title": "Regarder"
},
"sexuality": {
"title": "sexualité"
},
"account": {
"title": "compte",
"heroEyebrow": "Paramètres",
"heroIntro": "Conservez le nom dutilisateur, le-mail, le mot de passe et la visibilité en un seul endroit.",
"username": "nom d'utilisateur",
"email": "Adresse email",
"newpassword": "Passwort",
"newpasswordretype": "Répéter le mot de passe",
"deleteAccount": "Supprimer le compte",
"language": "Langue",
"showinsearch": "Afficher dans les recherches des utilisateurs",
"changeaction": "Modifier les données utilisateur",
"oldpassword": "Ancien mot de passe (obligatoire)",
"validation": {
"newPasswordTooShort": "Le nouveau mot de passe doit comporter au moins 8 caractères.",
"passwordMismatch": "Les mots de passe ne correspondent pas.",
"oldPasswordRequired": "Le mot de passe actuel est requis pour modifier le mot de passe."
},
"feedback": {
"saved": "Paramètres du compte enregistrés avec succès.",
"saveError": "Une erreur s'est produite lors de l'enregistrement des paramètres du compte."
},
"adultAccessTitle": "Espace érotique",
"adultAccessIntro": "La zone érotique est uniquement destinée aux utilisateurs majeurs et est également activée par les modérateurs.",
"requestAdultVerification": "Demander l'activation",
"requestAdultVerificationSuccess": "L'activation a été demandée.",
"requestAdultVerificationError": "L'activation n'a pas pu être demandée.",
"adultStatus": {
"ineligible": {
"title": "Pas disponible",
"body": "La zone érotique nest visible que par les utilisateurs majeurs."
},
"none": {
"title": "Pas encore activé",
"body": "La zone est visible mais verrouillée jusqu'à ce qu'elle soit examinée par un modérateur."
},
"pending": {
"title": "L'examen est en cours",
"body": "Votre demande est en cours d'examen pour modération. La zone restera fermée jusqu'à sa libération."
},
"approved": {
"title": "Débloqué",
"body": "L'espace érotique est activé pour votre compte."
},
"rejected": {
"title": "Activation rejetée",
"body": "La dernière demande n'a pas été publiée. Vous pouvez faire une nouvelle demande."
}
}
},
"languageAssistant": {
"eyebrow": "Paramètres",
"title": "Assistant vocal et IA",
"intro": "Ici, vous pouvez stocker votre propre accès API (par exemple OpenAI), que la plateforme peut utiliser pour les fonctions de cours de langue. La clé est stockée cryptée côté serveur ; vous avez besoin d'un compte auprès du fournisseur concerné.",
"linkSignup": "Créer un compte avec OpenAI (nouvelle fenêtre)",
"linkApiKeys": "Gérer les clés API chez OpenAI (nouvelle fenêtre)",
"enabled": "Autoriser l'utilisation des fonctions vocales",
"baseUrl": "URL de base de l'API (facultatif)",
"baseUrlPlaceholder": "Vide = Par défaut (OpenAI). Pour Ollama par ex. Par ex. http://127.0.0.1:11434/v1",
"model": "Nom du modèle",
"apiKey": "Clé API",
"apiKeyHint": "Laissez vide pour conserver la clé enregistrée.",
"apiKeyPlaceholderNew": "Insérer une nouvelle clé",
"apiKeyPlaceholderHasKey": "L'enregistrement se termine dans  {last4}  en laissant ce champ vide, la clé est conservée.",
"apiKeyPlaceholderClear": "La mémoire sera effacée lorsque vous enregistrerez « Supprimer la clé » ci-dessous",
"clearKey": "Supprimer la clé API enregistrée",
"save": "Sauvegarder",
"saved": "Paramètres enregistrés.",
"saveError": "L'enregistrement a échoué.",
"confirmClear": "Vraiment supprimer les clés API ?",
"keyStatusStored": "Clé API enregistrée.",
"keyStatusInvalid": "Une clé API stockée existe mais ne peut pas être lue. Veuillez réenregistrer.",
"keyStatusMissing": "Aucune clé API n'est actuellement stockée."
},
"interests": {
"title": "Interessen",
"new": "Nouvel intérêt",
"add": "Ajouter",
"added": "Le nouvel intérêt a été ajouté et est en cours de modification. Tant qu'il n'est pas terminé, il ne sera pas visible dans la liste d'intérêts.",
"adderror": "Une erreur s'est produite lors de l'ajout des intérêts.",
"errorsetinterest": "Les intérêts n'ont pas pu être réservés pour vous."
},
"visibility": {
"Invisible": "Ne pas montrer",
"OnlyFriends": "Afficher uniquement aux amis",
"FriendsAndAdults": "Montrer aux amis et aux adultes",
"AdultsOnly": "Afficher uniquement pour les adultes",
"All": "Montrer à tout le monde"
},
"feedback": {
"updateError": "La modification n'a pas pu être enregistrée.",
"visibilityUpdateError": "La visibilité n'a pas pu être mise à jour."
},
"flirt": {
"title": "flirter"
},
"immutable": {
"tooltip": "Ce champ ne peut pas être modifié. Pour les modifications, veuillez contacter le support.",
"supportContact": "Contacter l'assistance",
"supportMessage": {
"general": "Bonjour,\n\nJe souhaite demander une modification de mes données de profil non modifiables. \n\nVeuillez me contacter pour plus de détails. \n\nCordialement",
"specific": "Bonjour,\n\nJe souhaite demander une modification des données de profil immuables suivantes : {fields}\n\nVeuillez me contacter pour plus de détails. \n\nCordialement"
}
}
}
}

View File

@@ -0,0 +1,787 @@
{
"socialnetwork": {
"usersearch": {
"kicker": "Recherche communautaire",
"intro": "Trouvez des contacts appropriés dans la communauté en utilisant leur nom, leur âge et leur sexe.",
"ageSeparator": "bis",
"resultsCount": "{count} clics",
"openProfile": "Ouvrir le profil",
"title": "Recherche d'utilisateurs",
"username": "nom d'utilisateur",
"age_from": "Âge de",
"age_to": "bis",
"gender": "Geschlecht",
"search_button": "Suchen",
"no_results": "Aucun résultat trouvé",
"results_title": "Résultats de recherche :",
"result": {
"nick": "Surnom",
"gender": "Geschlecht",
"age": "Alter"
}
},
"profile": {
"pretitle": "Charger des données. S'il vous plaît, attendez...",
"error_title": "Utilisateur introuvable",
"title": "Profil de <nom d'utilisateur>",
"tab": {
"general": "Général",
"sexuality": "sexualité",
"images": "galerie",
"guestbook": "Livre d'or"
},
"values": {
"bool": {
"true": "Ja",
"false": "Nein"
},
"smokes": {
"never": "Jamais",
"socially": "En compagnie",
"often": "Souvent",
"daily": "Tous les jours"
},
"drinks": {
"never": "Jamais",
"socially": "En compagnie",
"often": "Souvent",
"daily": "Tous les jours"
},
"interestedInGender": {
"male": "hommes",
"female": "Femmes"
},
"sexualpreference": {
"straight": "Hétérosexuel",
"gay": "Gays",
"bi": "Bisexuel",
"pan": "Pansexuel",
"asexual": "Asexué"
},
"pubichair": {
"none": "Non",
"short": "Court",
"medium": "Moyen",
"long": "Long",
"hairy": "Non rasé",
"waxed": "Ciré",
"landingstrip": "piste",
"other": "Autre",
"bikinizone": "ligne de bikini"
},
"gender": {
"male": "Männlich",
"female": "Weiblich",
"transmale": "Femme trans",
"transfemale": "Homme trans",
"nonbinary": "Non binaire"
},
"language": {
"de": "Allemand",
"en": "Anglais"
},
"eyecolor": {
"blue": "Bleu",
"green": "Vert",
"brown": "Brun",
"black": "Noir",
"grey": "Gris",
"hazel": "noisette",
"amber": "ambre",
"red": "Rouge",
"other": "Autre"
},
"haircolor": {
"black": "Noir",
"brown": "Brun",
"blonde": "Blond",
"red": "Rouge",
"grey": "Gris",
"white": "Blanc",
"other": "Autre"
},
"hairlength": {
"short": "Court",
"medium": "Moyen",
"long": "Long",
"bald": "Chauve",
"other": "Autre"
},
"skincolor": {
"light": "Brillant",
"medium": "Moyen",
"dark": "Sombre",
"other": "Autre"
},
"freckles": {
"much": "Beaucoup",
"medium": "Moyen",
"less": "Peu",
"none": "Non"
}
},
"guestbook": {
"showInput": "Afficher la nouvelle entrée",
"hideInput": "Masquer la nouvelle entrée",
"imageUpload": "Bild",
"submit": "Soumettre l'entrée",
"noEntries": "Aucune entrée trouvée",
"entryImageAlt": "Image pour l'entrée du livre d'or"
},
"interestedInGender": "Intéressé par",
"hasChildren": "A des enfants",
"smokes": "Fumée",
"drinks": "alcool",
"willChildren": "Veut des enfants",
"sexualpreference": "Orientation sexuelle",
"pubichair": "poils pubiens",
"penislength": "Longueur du pénis",
"brasize": "Taille du soutien-gorge",
"piercings": "piercings",
"tattoos": "Tatouages",
"language": "Langue",
"gender": "Geschlecht",
"eyecolor": "Couleur des yeux",
"haircolor": "couleur de cheveux",
"hairlength": "longueur des cheveux",
"freckles": "Taches de rousseur",
"skincolor": "Couleur de peau",
"birthdate": "date de naissance",
"age": "Alter",
"town": "Stadt",
"bodyheight": "Größe",
"weight": "Poids"
},
"gallery": {
"kicker": "Images et dossiers",
"intro": "Organisez votre propre contenu, rendez-le visible et structurez-le dans des dossiers.",
"title": "galerie",
"folders": "Dossier",
"create_folder": "Créer un dossier",
"upload": {
"title": "Bild hochladen",
"image_title": "Titel",
"image_file": "déposer",
"visibility": "Visible à",
"upload_button": "Télécharger",
"selectvisibility": "Veuillez sélectionner"
},
"images": "Photos",
"visibility": {
"everyone": "Chaque",
"friends": "Amis",
"adults": "Adulte",
"friends-and-adults": "amis et adultes",
"selected-users": "Utilisateurs sélectionnés",
"none": "Personne"
},
"create_folder_dialog": {
"title": "Créer un dossier",
"parent_folder": "Sera créé en",
"folder_title": "Nom du dossier",
"visibility": "Visible à",
"select_visibility": "Veuillez sélectionner"
},
"noimages": "Il n'y a actuellement aucune image dans ce dossier",
"imagedialog": {
"image_title": "Titel",
"edit_visibility": "Visible à",
"save_changes": "enregistrer les modifications",
"close": "Fermer",
"edit_visibility_placeholder": "Veuillez sélectionner"
},
"delete_folder_confirmation_title": "Supprimer le dossier",
"delete_folder_confirmation_message": "Voulez-vous vraiment supprimer le dossier « %%folderName%% » ?",
"edit_image_dialog": {
"title": "Modifier les données d'image"
},
"show_image_dialog": {
"title": "Bild"
},
"imagePreviewAlt": "Aperçu de l'image",
"imageLoadingAlt": "L'image est en cours de chargement"
},
"guestbook": {
"kicker": "Livre d'or",
"intro": "Actualités, retours et petits insights de votre réseau.",
"title": "Livre d'or",
"prevPage": "Dos",
"nextPage": "Plus loin",
"page": "Page"
},
"diary": {
"kicker": "Entrées personnelles",
"intro": "Réflexions, notes et mises à jour rapides dans une vue calme et personnelle.",
"placeholder": "Écrivez votre entrée de journal...",
"title": "agenda",
"noEntries": "Vous n'avez encore rien écrit dans votre journal.",
"newEntry": "Nouvelle entrée de journal",
"editEntry": "Modifier une entrée de journal",
"save": "Sauvegarder",
"update": "Ändern",
"cancel": "Annuler",
"edit": "Ändern",
"delete": "Supprimer",
"confirmDelete": "Voulez-vous vraiment supprimer l'entrée ?",
"prevPage": "Dos",
"nextPage": "Plus loin",
"page": "Page"
},
"forum": {
"kicker": "Forum communautaire",
"intro": "Sujets, discussions et nouvelles contributions dans un seul endroit structuré.",
"createTitle": "Écrire un nouveau sujet",
"createIntro": "Définissez dabord le titre, puis rédigez le message et publiez-le immédiatement.",
"cancelCreation": "Annuler",
"creationHint": "Le titre et le contenu doivent tous deux être renseignés.",
"communityFallback": "Communauté",
"topicIntro": "Discussions, réponses et nouveaux posts dans un espace de lecture ciblé.",
"topicCreated": "Sujet créé avec succès.",
"topicCreateError": "Erreur lors de la création du sujet",
"title": "Forum",
"showNewTopic": "Créer un nouveau sujet",
"hideNewTopic": "Suspendre la création",
"noTitles": "Aucun sujet disponible",
"topic": "Thème",
"createNewTopic": "Créer un sujet",
"createdBy": "Créé par",
"createdAt": "Créé le",
"reactions": "réaction",
"lastReaction": "Dernière réaction de",
"pagination": {
"first": "Première page",
"previous": "Page précédente",
"next": "Page suivante",
"last": "Dernière page",
"page": "Page <<page>> de <<de>>"
},
"createNewMesssage": "Envoyer la réponse"
},
"friendship": {
"error": {
"alreadyexists": "La demande d'ami existe déjà"
},
"state": {
"none": "Pas amis",
"waiting": "Demande d'ami envoyée mais sans réponse",
"open": "L'amitié a été demandée",
"denied": "Demande d'ami rejetée",
"withdrawn": "Demande d'ami retirée",
"accepted": "Amis"
},
"added": "Vous avez fait une demande d'amitié.",
"withdrawn": "Vous avez retiré votre demande d'ami.",
"denied": "Vous avez refusé la demande d'ami.",
"accepted": "L'amitié était nouée."
},
"erotic": {
"eyebrow": "érotisme",
"accessTitle": "Activation pour la zone érotique",
"accessIntro": "Les images, vidéos et zones de discussion ultérieures sont affichées à partir de 18 ans, mais ne peuvent être utilisées qu'après approbation de la modération.",
"lockedShort": "Cette zone ne sera utilisable qu'après accord du modérateur.",
"requestVerification": "Demander l'activation",
"requestSent": "L'activation a été demandée.",
"requestError": "L'activation n'a pas pu être demandée.",
"requestInfoTitle": "Preuve soumise",
"documentLabel": "Dossier de preuve",
"noteLabel": "Note rapide pour la modération",
"settingsLink": "Ouvrir les paramètres du compte",
"verificationHintTitle": "Note sur la preuve",
"verificationHintBody": "Vous pouvez envoyer une photo. Si votre âge ny figure pas clairement, la demande sera rejetée et vous devrez présenter une pièce didentité à la place.",
"notifications": {
"approved": "Votre espace érotique a été activé par le modérateur.",
"rejected": "Votre candidature pour le domaine érotique a été rejetée. Si votre âge n'est pas clairement visible sur les photos, veuillez envoyer une pièce d'identité."
},
"picturesTitle": "Photos érotiques",
"picturesIntro": "Votre propre contenu reste strictement séparé de la zone normale de la galerie. Ici, vous gérez uniquement les images de la zone érotique activée.",
"uploadTitle": "Télécharger une photo érotique",
"noimages": "Il n'y a actuellement aucune image dans ce dossier érotique.",
"videosTitle": "Vidéos érotiques",
"videosIntro": "Vos propres vidéos sont gérées séparément de l'espace social normal. Ici, vous organisez les téléchargements, la visibilité et le statut de modération en un seul endroit.",
"videoUploadTitle": "Télécharger une vidéo érotique",
"videoUploadHint": "Téléchargez ici des vidéos pour votre zone érotique activée et conservez le titre et la description directement lors du téléchargement.",
"videoDescription": "Beschreibung",
"videoFile": "Fichier vidéo",
"videoFormats": "MP4, WEBM, OGG, MOV",
"myVideos": "Mes vidéos",
"sharedVideos": "Vidéos partagées",
"foreignVideosIntro": "Vidéos pour adultes approuvées.",
"foreignVideosOnlyHint": "Ici, vous ne verrez que les vidéos approuvées pour un usage adulte.",
"sharedVideosIntro": "Vidéos visibles dans les zones réservées aux adultes approuvées.",
"noSharedVideos": "Il n'y a actuellement aucune vidéo partagée pour vous.",
"libraryTitle": "bibliothèque",
"libraryIntro": "Possédez vos téléchargements, partages et rapports en un seul endroit.",
"libraryEmptyHint": "Créez votre première vidéo sur la gauche puis gérez-la ici dans la bibliothèque.",
"latestUpload": "Dernier téléchargement",
"visibleVideos": "Vidéos visibles",
"moderationCases": "Cas de modération",
"notesTitle": "Remarques",
"friendsVisibilityHint": "Les amis ne verront le contenu que sils ont lâge légal et ont accès à la zone réservée aux adultes.",
"selectedUsersVisibilityHint": "Les personnes spécifiquement agréées doivent également être majeures et agréées.",
"selectedUsersPlaceholder": "Anna, Bert, Clara",
"imagePreviewAlt": "Aperçu de l'image",
"imageLoadingAlt": "L'image est en cours de chargement",
"untitled": "Sans titre",
"noUploadYet": "Pas encore de téléchargement",
"closeEditing": "Fermer l'édition",
"editVisibility": "Modifier les versions",
"noVideos": "Vous n'avez pas encore mis en ligne de vidéos érotiques.",
"reportAction": "Rapport",
"reportHint": "Utilisez {action} directement sur l'entrée respective si le contenu doit être vérifié.",
"reportNote": "Note rapide pour la modération",
"submitReport": "Envoyer un message",
"reportSubmitted": "Le rapport a été enregistré.",
"reportError": "Le message n'a pas pu être enregistré.",
"moderationHidden": "Masqué par la modération",
"hiddenByModeration": "Ce contenu a été temporairement masqué par modération.",
"reportReasons": {
"suspected_minor": "Soupçon d'être mineur",
"non_consensual": "Contenu non consensuel",
"violence": "Violence ou abus",
"harassment": "Harcèlement ou pression",
"spam": "Spam ou arnaque",
"other": "Divers"
},
"intro": "La zone est débloquée. Les modules image et vidéo proprement dits suivent à létape suivante.",
"enabledTitle": "Accès débloqué",
"enabledBody": "Votre compte est approuvé pour la zone érotique. La vue séparée de l'image et de la vidéo est maintenant créée ici.",
"roadmapTitle": "Suivant",
"roadmapModeration": "canaux de modération et de signalement séparés",
"roadmapUpload": "propres vues de téléchargement et de gestion",
"roadmapSeparation": "séparation nette entre la galerie normale et la zone érotique",
"status": {
"none": {
"title": "Pas encore activé",
"body": "La zone est visible mais reste verrouillée jusqu'à approbation du modérateur."
},
"pending": {
"title": "L'examen est en cours",
"body": "Votre demande est en cours d'examen pour modération."
},
"approved": {
"title": "Débloqué",
"body": "L'espace érotique est déjà activé pour votre compte."
},
"rejected": {
"title": "Activation rejetée",
"body": "La dernière demande a été rejetée. Vous pouvez faire une nouvelle demande."
}
}
},
"vocab": {
"title": "Entraîneur de vocabulaire",
"description": "Créez (ou abonnez-vous) à des langues et partagez-les avec vos amis.",
"heroEyebrow": "Apprentissage des langues",
"summaryTotalLabel": "Langues totales",
"summaryTotalIntro": "Toutes les zones linguistiques actives dans lesquelles vous utilisez ou gérez du contenu.",
"summaryOwnedLabel": "Zones propres",
"summaryOwnedIntro": "Ici, vous créez activement vous-même du contenu, des chapitres et du matériel d'apprentissage.",
"summarySubscribedLabel": "Abonné",
"summarySubscribedIntro": "Ces domaines sont destinés à lapprentissage et au progrès plutôt quà ladministration.",
"taskCreateEyebrow": "Démarrage rapide",
"taskCreateTitle": "Créer une nouvelle langue",
"taskCreateIntro": "Le meilleur point de départ si vous souhaitez structurer et maintenir vous-même du contenu.",
"taskContinueEyebrow": "Continuez à apprendre",
"taskContinueTitle": "Cours et chapitres ouverts",
"taskContinueIntro": "Accédez directement aux parcours d'apprentissage existants et continuez à travailler avec les cours existants.",
"ownedSectionTitle": "Langues propres",
"ownedSectionIntro": "Accès direct à lédition, aux chapitres et à la gestion des cours.",
"ownedHint": "Gérer et maintenir le contenu",
"ownedEmpty": "Il nexiste pas encore de zones linguistiques distinctes.",
"subscribedSectionTitle": "Langues souscrites",
"subscribedSectionIntro": "Idéal pour un retour rapide aux études sans aucun effort administratif.",
"subscribedHint": "Apprendre, pratiquer et visualiser les progrès",
"subscribedEmpty": "Aucune langue abonnée disponible.",
"languageHeroEyebrow": "Langue",
"languageHeroIntro": "Chapitres, fonctions de recherche et partages pour cette langue en un seul endroit.",
"newLanguageHeroEyebrow": "Entraîneur de vocabulaire",
"newLanguageHeroIntro": "Créez une nouvelle langue, générez un code de version et passez directement à l'édition.",
"newLanguageNameHint": "Un nom court et clair suffit pour commencer.",
"newLanguageNameValidation": "Le nom doit comporter au moins 2 caractères.",
"subscribeHeroEyebrow": "Entraîneur de vocabulaire",
"chapterHeroEyebrow": "Entraîneur de vocabulaire",
"chapterHeroIntro": "Parcourez le contenu des chapitres, maintenez le vocabulaire et passez directement à l'exercice.",
"newLanguage": "Nouvelle langue",
"newLanguageTitle": "Créer une nouvelle langue",
"languageName": "nom de la langue",
"create": "Anlegen",
"saving": "Sauvegarder...",
"created": "La langue a été créée.",
"createdTitle": "Entraîneur de vocabulaire",
"createdMessage": "La langue a été créée. Le menu est mis à jour.",
"createError": "Impossible de créer la langue.",
"openLanguage": "Öffnen",
"none": "Vous n'avez encore créé ou souscrit à aucune langue.",
"owner": "propre",
"subscribed": "Abonné",
"languageTitle": "Entraîneur de vocabulaire : {nom}",
"notFound": "Langue introuvable ou accès impossible.",
"shareCode": "Partager le code",
"shareHint": "Vous pouvez transmettre ce code à vos amis afin qu'ils puissent s'abonner à la langue.",
"subscribeByCode": "Abonnez-vous par code",
"subscribeTitle": "Abonnez-vous à la langue",
"subscribeHint": "Saisissez le code de partage que vous avez reçu d'un ami.",
"subscribe": "S'abonner",
"subscribeSuccess": "Abonnement réussi. Le menu est mis à jour.",
"subscribeError": "L'abonnement a échoué. Code invalide ou pas d'accès.",
"trainerPlaceholder": "Les fonctions du formateur (vocabulaire/requêtes) constituent l'étape suivante.",
"chapters": "Chapitre",
"newChapter": "Nouveau chapitre",
"createChapter": "Créer un chapitre",
"createChapterError": "Impossible de créer des chapitres.",
"noChapters": "Aucun chapitre pour l'instant.",
"chapterTitle": "Chapitre : {titre}",
"addVocab": "Ajouter du vocabulaire",
"learningWord": "Apprendre la langue",
"referenceWord": "référence",
"add": "Ajouter",
"addVocabError": "Impossible d'ajouter du vocabulaire.",
"noVocabs": "Il n'y a pas de mots de vocabulaire dans ce chapitre.",
"practice": {
"open": "Pratique",
"title": "Pratiquer le vocabulaire",
"allVocabs": "Tout le vocabulaire",
"simple": "Pratique facile",
"noPool": "Aucun vocabulaire disponible pour pratiquer.",
"dirLearningToRef": "Apprentissage d'une langue → Référence",
"dirRefToLearning": "Référence → Apprendre une langue",
"check": "Vérifier",
"next": "Plus loin",
"skip": "Sauter",
"correct": "Correct!",
"wrong": "Incorrect.",
"acceptable": "Traductions correctes possibles :",
"stats": "statistiques",
"success": "Succès",
"fail": "échec"
},
"search": {
"open": "Recherche",
"title": "Rechercher du vocabulaire",
"term": "Terme de recherche",
"motherTongue": "langue maternelle",
"learningLanguage": "Apprendre la langue",
"lesson": "Lektion",
"search": "Suchen",
"noResults": "Aucun coup sûr.",
"error": "La recherche a échoué."
},
"courses": {
"title": "Cours d'apprentissage des langues",
"create": "Créer un cours",
"myCourses": "Mes cours",
"allCourses": "Tous les cours",
"none": "Aucun cours trouvé.",
"owner": "Besitzer",
"enrolled": "Inscrit",
"public": "Öffentlich",
"difficulty": "difficulté",
"lessons": "Leçons",
"enroll": "Courrier recommandé",
"continue": "Continuer",
"edit": "Modifier",
"addLesson": "Ajouter une leçon",
"completed": "Complété",
"score": "Score",
"review": "Répéter",
"start": "Commencer",
"resetLessonProgress": "Réinitialiser la leçon",
"resetLessonProgressConfirm": "Réinitialiser la progression de cette leçon ? Le statut enregistré, les résultats des exercices et le statut d'entraîneur sont supprimés. Les autres enseignements restent inchangés.",
"resetLessonProgressSuccess": "La leçon a été réinitialisée.",
"resetLessonProgressError": "La leçon n'a pas pu être réinitialisée.",
"noLessons": "Ce cours n'a pas encore de leçons.",
"lessonNumber": "Numéro de leçon",
"chapter": "Chapitre",
"selectChapter": "Sélectionner un chapitre",
"selectLanguage": "Sélectionnez la langue",
"confirmDelete": "Vraiment supprimer la leçon ?",
"titleLabel": "Titel",
"descriptionLabel": "Beschreibung",
"languageLabel": "Langue",
"findByCode": "Rechercher un cours par code",
"shareCode": "Partager le code",
"searchPlaceholder": "Rechercher un cours...",
"allLanguages": "Toutes les langues",
"targetLanguage": "Langue cible",
"nativeLanguage": "langue maternelle",
"allNativeLanguages": "Toutes les langues maternelles",
"myNativeLanguage": "Ma langue maternelle",
"forAllLanguages": "Pour toutes les langues",
"optional": "Facultatif",
"invalidCode": "Code invalide",
"courseNotFound": "Cours introuvable",
"grammarExercises": "Vérification de la grammaire",
"exerciseFlowIntro": "Effectuez les tâches dans lordre. Chaque tâche correctement résolue vous rapproche de la fin de la leçon.",
"exerciseProgressLabel": "Progrès",
"exerciseTargetLabel": "Nécessaire",
"exerciseCardLabel": "Tâche {number}",
"exerciseSequentialProgress": "Question {actuelle} de {total}",
"exerciseSequentialBack": "Dos",
"exerciseSequentialNext": "Plus loin",
"exerciseWrongTitle": "Pas encore bien",
"exerciseReinforcementGoPractice": "Passer à la pratique",
"exerciseReinforcementStay": "Restez avec l'examen",
"exerciseReinforcementGoPracticeAck": "Lisez, passez à la pratique",
"exerciseReinforcementStayAck": "Lisez, restez avec l'examen",
"exerciseStatusOpen": "Ouvrir",
"exerciseStatusCorrect": "Complété",
"exerciseStatusRetry": "Revérifier",
"exerciseAnswerAllHint": "Répondez d'abord aux {total} tâches. En cours : {answered}. Pour réussir, il faut au moins {target} %.",
"exerciseNeedMoreCorrectHint": "Vous avez actuellement {score} %. Pour terminer cette leçon, vous avez besoin d'au moins {target} %.",
"exercisePassedHint": "Objectif atteint : {score} % du {target} % requis. Dès que toutes les tâches sont terminées, l'examen est considéré comme réussi.",
"exerciseReinforcementHint": "Après une erreur, vous revenez brièvement en mode apprentissage. Entraînez-vous à {count} questions du formateur et l'examen du chapitre sera à nouveau activé.",
"exercisePrepReinforcementHint": "Si vous faites une erreur, relisez les conditions préparées. Lexamen de chapitre sera alors à nouveau activé.",
"exerciseGrammarLead": "Grammaire importante pour cet examen",
"noExercises": "Aucun examen disponible",
"enterAnswer": "Entrez la réponse",
"checkAnswer": "Vérifier la réponse",
"correct": "Correct!",
"wrong": "Incorrect",
"explanation": "Explication",
"learn": "Apprendre",
"exercises": "Examen de chapitre",
"learnVocabulary": "Apprendre le vocabulaire",
"lessonOverviewText": "Cette leçon combine du vocabulaire, des modèles, de courtes impulsions grammaticales et une pratique active de la langue.",
"lessonDescription": "Description de la leçon",
"culturalNotes": "Notes culturelles",
"grammarExplanations": "Explications de grammaire",
"grammarImpulse": "Impulsion grammaticale",
"learningGoals": "Objectifs d'apprentissage",
"corePatterns": "modèle de base",
"corePatternsHint": "Lisez d'abord la langue cible, y compris la signification allemande - de cette façon, vous apprendrez consciemment chaque modèle dans les deux sens.",
"learningGrammarTitle": "Classer brièvement la grammaire",
"learningGrammarIntro": "Lisez brièvement ces 1 à 2 notes après les termes. Ensuite, vous allez chez le formateur avec plus d'orientation.",
"vocabPrepTitle": "Préparation avant l'entraîneur de vocabulaire",
"vocabPrepStep1": "Lisez attentivement le modèle de base et la liste de mots (allemand ↔ langue cible).",
"vocabPrepProgress": "passage {pass} : terme {actuel} de {total}",
"vocabPrepTargetLabel": "Langue cible",
"vocabPrepGlossLabel": "Allemand",
"vocabPrepNextItem": "Prochain mandat",
"vocabPrepConfirm1": "Première révision effectuée",
"vocabPrepStep2": "Reprenez les mêmes termes (répétition active, sans pratique).",
"vocabPrepConfirm2": "Deuxième révision effectuée",
"vocabPrepReady": "Vous pouvez maintenant commencer avec l'entraîneur de vocabulaire.",
"learningPathLabel": "Chemin principal",
"learningPathTitle": "Votre parcours d'apprentissage pour cette leçon",
"learningPathIntro": "Suivez ces étapes une par une : préparez-vous, révisez, formez-vous, puis passez à l'examen du chapitre.",
"lessonDetailsToggle": "Afficher plus de détails sur la leçon",
"deepenSectionTitle": "Approfondissez et lisez",
"assistantSectionTitle": "Approfondissez avec l'assistant vocal",
"vocabOverviewToggle": "Afficher un aperçu complet des termes",
"vocabTrainerLockedHint": "Veuillez d'abord confirmer deux séances d'apprentissage dans « Préparation avant l'entraîneur de vocabulaire ».",
"exerciseUnlockHintAfterPrep": "Commencez par parcourir les termes préparés. Lexamen du chapitre sera alors débloqué.",
"speakingTasks": "Travaux oraux",
"speakingPrompt": "Devoir de prise de parole",
"practicalTasks": "Tâches pratiques",
"importantVocab": "Termes importants",
"vocabInfoText": "Ces termes seront utilisés lors de l'examen. Apprenez-les passivement ici avant de passer à l'examen du chapitre.",
"noVocabInfo": "Lisez la description ci-dessus et les explications de l'examen pour apprendre les termes les plus importants.",
"vocabTrainer": "Entraîneur de vocabulaire",
"vocabTrainerDescription": "Pratiquez les concepts les plus importants de cette leçon de manière interactive.",
"startVocabTrainer": "Démarrer l'entraîneur de vocabulaire",
"stopTrainer": "Terminer l'entraîneur",
"translateTo": "Traduire en Bisaya",
"translateFrom": "Traduire en allemand",
"next": "Plus loin",
"totalAttempts": "Essayer",
"successRate": "Taux de réussite",
"modeMultipleChoice": "choix multiple",
"modeTyping": "Saisie de texte",
"currentLesson": "Leçon actuelle",
"mixedReview": "Répétition",
"lessonCompleted": "Leçon terminée !",
"goToNextLesson": "Passer à la leçon suivante ?",
"allLessonsCompleted": "Toutes les leçons terminées !",
"startExercises": "Pour l'examen du chapitre",
"lessonTypeLabel": "Type de cours",
"recommendedDuration": "Durée recommandée",
"exerciseLoad": "Montant d'exercice",
"exercisesShort": "Exercices",
"durationFlexible": "Flexible",
"durationMinutes": "{minutes} minutes",
"lessonTypeVocab": "Vocabulaire",
"lessonTypeGrammar": "grammaire",
"lessonTypeConversation": "Conversation",
"lessonTypeCulture": "culture",
"lessonTypeReview": "Répétition",
"correctAnswer": "Bonne réponse",
"alternatives": "Réponses alternatives",
"notStarted": "Pas commencé",
"continueCurrentLesson": "Vers la leçon en cours",
"previousLessonRequired": "Veuillez d'abord terminer la leçon précédente",
"lessonNumberShort": "#",
"buildSentencePlaceholder": "Construisez votre phrase ici",
"completeDialogPlaceholder": "Remplissez la ligne de dialogue manquante",
"situationalResponsePlaceholder": "Formulez votre réponse à la situation",
"patternDrillPlaceholder": "Formuler une phrase appropriée en utilisant l'exemple",
"modelSentence": "Ensemble de modèles",
"modelDialogLine": "Ligne de dialogue possible",
"modelResponse": "Réponse possible",
"modelPattern": "Ensemble d'échantillons possible",
"patternPrompt": "Modèle",
"readingAloudInstruction": "Lisez le texte à voix haute. Cliquez sur « Démarrer l'enregistrement » et commencez à parler.",
"speakingFromMemoryInstruction": "Parlez librement depuis votre tête. Utilisez les mots-clés affichés.",
"startRecording": "Commencer l'enregistrement",
"stopRecording": "Arrêter l'enregistrement",
"startSpeaking": "Commencez à parler",
"recording": "L'enregistrement est en cours",
"listening": "Écouter...",
"recordingStopped": "Enregistrement terminé",
"recordingError": "Erreur d'enregistrement",
"recognizedText": "Texte reconnu",
"speechRecognitionNotSupported": "La reconnaissance vocale n'est pas prise en charge par ce navigateur. Veuillez utiliser Chrome ou Edge.",
"speakingFallbackInstruction": "Votre navigateur ne prend pas en charge la saisie vocale ici. Au lieu de cela, écrivez votre réponse orale sous forme de texte, puis vérifiez-la normalement.",
"speakingFallbackPlaceholder": "Écrivez ici ce que vous diriez...",
"keywords": "Mots-clés",
"switchBackToMultipleChoice": "Retour aux choix multiples",
"languageAssistantEyebrow": "Assistant vocal",
"languageAssistantCourseTitle": "Prise en charge de l'IA pour ce cours",
"languageAssistantCourseReady": "L'assistant linguistique est mis en place et est disponible dans les cours pour des explications, des corrections et de courts exercices de dialogue.",
"languageAssistantCourseSetup": "Configurez l'assistant vocal pour pouvoir poser des questions spécifiques et pratiquer de petits dialogues pendant les leçons.",
"languageAssistantOpenLesson": "Ouvrir dans la leçon en cours",
"languageAssistantTitle": "Entraînez-vous avec l'assistant vocal",
"languageAssistantIntro": "Utilisez l'IA directement pour la leçon en cours : faites expliquer la grammaire, pratiquez de courts dialogues ou corrigez vos propres phrases.",
"languageAssistantSettings": "Définir l'assistant vocal",
"languageAssistantSetupHint": "L'assistant vocal n'est pas encore configuré ou est actuellement désactivé. Enregistrez dabord le modèle et laccès à lAPI dans les paramètres.",
"languageAssistantModePractice": "Pratique",
"languageAssistantModeExplain": "Expliquer",
"languageAssistantModeCorrect": "Correct",
"languageAssistantPromptExplain": "Expliquer la grammaire",
"languageAssistantPromptPractice": "Pratiquez le mini-dialogue",
"languageAssistantPromptCorrect": "Améliorer ma phrase",
"languageAssistantSpeakerAi": "Assistant vocal",
"languageAssistantSpeakerYou": "Du",
"languageAssistantInputLabel": "Votre message",
"languageAssistantInputPlaceholder": "Posez une question sur la leçon ou écrivez votre propre phrase à corriger.",
"languageAssistantSend": "Envoyer à l'assistant vocal",
"languageAssistantSending": "La réponse sera récupérée...",
"languageAssistantError": "L'assistant vocal ne pouvait pas répondre pour le moment.",
"languageAssistantPresetExplainStart": "Veuillez m'expliquer les modèles et la grammaire les plus importants de la leçon.",
"languageAssistantPatternHint": "Utilisez ce modèle en particulier",
"languageAssistantPresetPracticeStart": "Pratiquons un court dialogue quotidien pour la leçon \"{lesson}\". Posez-moi des questions et attendez mes réponses.",
"languageAssistantPresetCorrectStart": "J'aimerais écrire mes propres phrases pour la leçon \"{lesson}\". Veuillez corriger mes réponses pour qu'elles soient concises et compréhensibles.",
"thisLesson": "cette leçon",
"courseKicker": "Cours d'apprentissage",
"courseListKicker": "Cours",
"courseListIntro": "Filtrez, trouvez et continuez à apprendre des cours publics et propres.",
"courseShareCodePlaceholder": "par ex. Par ex. abc123def456",
"courseFlowEyebrow": "Flux diurne",
"courseFlowTitle": "Continuez raisonnablement aujourd'hui",
"courseFlowIntro": "Tout dabord, la recommandation quotidienne avec une commande précise. Cela comprend les quatre domaines : répétition courte due, bloc actuel, phase intensive, approfondissement libre.",
"quickReviewTitle": "Courte répétition",
"quickReviewIntro": "Courte session avec {count} termes. Une fois terminée, la répétition programmée est marquée comme terminée.",
"quickReviewDoneTitle": "Fait",
"quickReviewDoneScore": "Correct : {correct} / {total}",
"quickReviewBackToCourse": "Retour au cours",
"quickReviewProgress": "Durée {actuelle} de {total}",
"quickReviewPromptMeaning": "Que signifie « {term} » ?",
"quickReviewPromptTarget": "Appuyez sur la langue cible : \"{term}\"",
"quickReviewAcknowledge": "Lire, continuer",
"courseTodayPlanTitle": "Recommandation pour aujourd'hui",
"courseTodayPlanIntro": "Voici comment procéder : tout d'abord sous « Maintenant, répétez brièvement », puis les leçons ouvertes dans votre bloc, puis, si nécessaire, la phase intensive. Les répétitions courtes sont les trois petites dates après la fin de la leçon (généralement après environ 1, 3 et 7 jours) afin que le vocabulaire reste fidèle.",
"courseTodayPlanIntroNoDueReview": "Aucune courte répétition nest prévue aujourdhui. Commencez par les cours ouverts du bloc en cours puis, si nécessaire, passez à la phase intensive. De courtes répétitions réapparaissent automatiquement sur un cycle de 1/3/7 jours.",
"courseTodayPlanStepReviewDue": "Maintenant, répétez brièvement",
"courseTodayPlanStepBlock": "Continuer dans le bloc actuel",
"courseTodayPlanStepIntensive": "Répétition intense",
"courseTodayPlanStepContinue": "La prochaine étape de votre parcours d'apprentissage",
"courseTodayPlanStepPractice": "Pratiquez librement (facultatif)",
"courseTodayPlanOpen": "Leçon ouverte",
"courseTodayPlanTrainer": "Pratique dans le formateur",
"courseTodayPlanEmpty": "Il ny a actuellement aucune répétition échelonnée et il ny a pas détape de bloc suivante claire. Choisissez une leçon ci-dessous ou utilisez l'étude approfondie gratuite avec le formateur.",
"courseFlowReviewStat": "Répétition due : {count}",
"courseFlowBlockStat": "Bloc actif : {block}",
"courseFlowReviewTitle": "Répétition due",
"courseFlowReviewDescription": "Des enseignements déjà réalisés et qui devraient revenir aujourd'hui.",
"courseFlowReviewEmpty": "Aujourdhui, aucune leçon plus ancienne nest marquée comme devant être révisée.",
"courseFlowBlockTitle": "Bloc actuel",
"courseFlowBlockDescription": "C'est là que se situe la prochaine progression régulière du cours.",
"courseFlowBlockEmpty": "Le bloc en cours est déjà terminé ou il n'y a actuellement aucune leçon en bloc ouverte.",
"courseFlowIntensiveTitle": "Phase intensive due",
"courseFlowIntensiveDescription": "Répétition condensée dès que le bloc devant lui est largement en place.",
"courseFlowIntensiveEmpty": "Il ny a actuellement aucune nouvelle phase intensive activée.",
"courseFlowPracticeTitle": "Approfondissement gratuit",
"courseFlowPracticeDescription": "Cours terminés pour une formation de suivi occasionnelle en dehors du parcours obligatoire.",
"courseFlowPracticeEmpty": "Dès que vous aurez terminé les premières leçons, elles apparaîtront ici pour une formation complémentaire gratuite.",
"practiceInTrainer": "Pratique dans le formateur",
"lessonsCount": "{count} leçons",
"lessonBlockLabel": "Bloc {number}",
"lessonIntensiveBadge": "Répétition intense",
"addLessonValidation": "Veuillez indiquer le numéro, le titre et le chapitre au complet.",
"addLessonSuccess": "Leçon créée avec succès.",
"addLessonError": "Erreur lors de l'ajout de la leçon.",
"createCourseError": "Erreur lors de la création du cours.",
"deleteLessonTitle": "Supprimer la leçon",
"deleteLessonSuccess": "Leçon supprimée avec succès.",
"deleteLessonError": "Erreur lors de la suppression de la leçon.",
"enrollCourseError": "Erreur lors de l'inscription.",
"editLessonPending": "Une édition des leçons individuelles suivra.",
"timeToday": "aujourd'hui",
"timeSinceOneDay": "depuis 1 jour",
"timeSinceDays": "pendant {count} jours",
"reviewDueNow": "dû maintenant",
"reviewDueTomorrow": "à rendre demain",
"reviewDueInDays": "dû dans {count} jours",
"reviewDueToday": "dû aujourd'hui",
"reviewDueSinceOneDay": "dû il y a 1 jour",
"reviewDueSinceDays": "dû pour {count} jours",
"reviewBadgeScheduleTomorrow": "prochaine vague demain",
"reviewBadgeScheduleInDays": "prochaine vague dans {count} jours",
"reviewBadgeScheduleToday": "Vague prévue aujourd'hui",
"reviewBadgeScheduleOverdue": "Vague en retard (depuis {count} jours)",
"reviewBadgeLineAllDone": "Toutes les répétitions courtes terminées (3×)",
"reviewBadgeLineDue": "Bref rappel : étape {step} sur 3 · à vous maintenant",
"reviewBadgeLineScheduled": "Bref rappel : étape {step} sur 3 · prochaine session {when}",
"reviewWhenFriendlyTomorrow": "matin",
"reviewWhenFriendlyInDays": "dans {count} jours",
"reviewWhenFriendlyToday": "aujourd'hui",
"reviewWhenFriendlyOverdue": "en retard (il y a {count} jour(s))",
"reviewWhenFriendlySoon": "bientôt",
"reviewBadgeTooltipDone": "Vous avez effectué les trois courtes répétitions recommandées à la fin de cette leçon. La pratique dans l'entraîneur continue.",
"reviewBadgeTooltipActive": "Une fois le cours terminé, le cours recommande trois courtes répétitions espacées d'environ 1, 3 et 7 jours afin que le vocabulaire soit conservé en mémoire. Le badge indique quelle étape (1-3) est actuellement en cours ou quand aura lieu le prochain rendez-vous.",
"reviewStageDay1": "Étape 1 sur 3 (~1 jour)",
"reviewStageDay3": "Étape 2 sur 3 (~ 3 jours)",
"reviewStageDay7": "Étape 3 sur 3 (~ 7 jours)",
"reviewStageCompleted": "Toutes les courtes répétitions sont terminées",
"phaseQuickstart": "Démarrage rapide",
"phaseDailyLife": "Tous les jours",
"phaseStabilization": "stabilisation",
"phaseDefault": "Phase d'apprentissage",
"didacticModeCoreInput": "Nouveau matériel",
"didacticModeGuidedDialogue": "Dialogue guidé",
"didacticModeContrastTraining": "Formation de contraste",
"didacticModePatternDrill": "Formation de modèle",
"didacticModeRealLifeScenario": "Scénario quotidien",
"didacticModeIntensiveReview": "Phase de répétition",
"didacticModeCheckpoint": "Point de contrôle",
"didacticModeDefault": "Unité d'apprentissage",
"didacticModeFocusDefault": "Objectif d'apprentissage",
"lessonMetaFocus": "se concentrer",
"lessonMetaPhase": "phase",
"lessonMetaNewUnits": "Nouvelles unités",
"lessonMetaReview": "Répétition",
"intensiveReviewTitle": "Phase de répétition intensive",
"intensiveReviewIntro": "Cette leçon donne la priorité à la répétition et au renforcement. Les nouveaux matériaux sont délibérément réduits afin que les modèles existants deviennent stables.",
"reviewPriorityTitle": "La répétition progresse étape par étape",
"reviewPriorityIntro": "Tout dabord, laccent est mis sur les nouveaux concepts de cette leçon. Au fur et à mesure que vous progressez, le vocabulaire plus ancien sera de plus en plus incorporé.",
"exerciseLockTitle": "L'examen du chapitre est toujours verrouillé",
"exerciseUnlockHintTrainerCore": "L'examen de chapitre est débloqué lorsque les trois conditions sont remplies : au moins {newTarget} questions sur le nouveau contenu (ligne \"Nouveau contenu\"), un total d'environ {attempts} questions du formateur et au moins {rate} % de taux de réussite.",
"exerciseUnlockHintTrainerMixSuffix": "Le vocabulaire plus ancien est progressivement incorporé.",
"trainerStartWithReview": "Commencez par le nouveau vocabulaire de cette leçon. Au fur et à mesure que la pratique progresse, l'entraîneur mélange automatiquement les répétitions appropriées.",
"startLesson": "Commencer la leçon",
"trainerProgressNewContent": "Nouveau contenu : {current}/{target}",
"trainerProgressReview": "Répéter : {count}",
"trainerProgressMixShare": "Proportion de mélange : {pourcentage} %",
"unknownExerciseTypeNotice": "Ce type d'exercice n'est pas encore affiché de manière interactive dans la vue actuelle.",
"unknownExerciseTypeLabel": "Tapez : {type}",
"lessonReviewHeadlineDone": "Cette leçon a atteint le niveau d'immersion libre.",
"lessonReviewHeadlineDue": "Cette vague de critiques est attendue maintenant.",
"lessonReviewHeadlineScheduled": "Cette leçon est réservée à la prochaine vague de critiques.",
"lessonReviewHintDone": "La répétition 1/3/7 jour est terminée. Vous pouvez désormais continuer à entraîner la leçon de manière flexible.",
"lessonReviewHintNextDue": "Prochaine date d'échéance : {due}.",
"reviewTimeNow": "maintenant",
"reviewTimeTomorrow": "matin",
"reviewTimeInDays": "dans {count} jours"
}
}
}
}

View File

@@ -0,0 +1,23 @@
/**
* Zentrale UI-Locales (BCP 47). Reihenfolge für Dropdowns (Deutsch zuerst, dann üblich).
*/
export const SUPPORTED_UI_LOCALES = ['de', 'en', 'ceb', 'es', 'fr'];
/** hreflang / SEO: ohne funktionale Abweichung zu {@link SUPPORTED_UI_LOCALES}, nur Reihenfolge wie bisher in seo.js. */
export const SEO_UI_LOCALES = ['de', 'en', 'es', 'ceb', 'fr'];
export const HREFLANG_FOR_UI = {
de: 'de',
en: 'en',
es: 'es',
ceb: 'ceb',
fr: 'fr',
};
export const OG_LOCALE_FOR_UI = {
de: 'de_DE',
en: 'en_GB',
es: 'es_ES',
ceb: 'ceb_PH',
fr: 'fr_FR',
};

View File

@@ -5,6 +5,7 @@ import router from './router';
import './assets/styles.scss';
import i18n from './i18n';
import { setSeoI18nAccessor, applyRouteSeo } from './utils/seo';
import { SUPPORTED_UI_LOCALES } from './i18n/supportedLocales.js';
import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
@@ -25,7 +26,15 @@ function getBrowserLanguage() {
if (browserLanguage.startsWith('de')) {
return 'de';
}
if (browserLanguage.startsWith('fr')) {
return 'fr';
}
if (browserLanguage.startsWith('es')) {
return 'es';
}
// Prüfe alle verfügbaren Sprachen für deutschsprachige Länder
const allLanguages = navigator.languages || [navigator.language];
for (const lang of allLanguages) {
@@ -47,13 +56,23 @@ function getBrowserLanguage() {
}
}
}
for (const lang of allLanguages) {
if (lang.startsWith('fr')) {
return 'fr';
}
}
for (const lang of allLanguages) {
if (lang.startsWith('es')) {
return 'es';
}
}
// Fallback: Englisch für alle anderen Sprachen
return 'en';
}
const SUPPORTED_UI_LOCALES = ['de', 'en', 'ceb', 'es'];
function readLangFromUrl() {
try {
const q = new URLSearchParams(window.location.search).get('lang');

View File

@@ -5,6 +5,7 @@ import router from '../router';
import apiClient from '../utils/axios.js';
import { io } from 'socket.io-client';
import { getDaemonSocketUrl, getSocketIoUrl } from '../utils/appConfig.js';
import { SUPPORTED_UI_LOCALES } from '../i18n/supportedLocales.js';
const AUTH_KEYS = ['isLoggedIn', 'user', 'userid'];
@@ -30,8 +31,6 @@ function clearAuthStorage() {
});
}
const SUPPORTED_UI_LOCALES = ['de', 'en', 'ceb', 'es'];
function persistAuthStorage(user, rememberMe) {
const targetStorage = rememberMe ? localStorage : sessionStorage;
clearAuthStorage();
@@ -78,7 +77,15 @@ const store = createStore({
if (browserLanguage.startsWith('de')) {
return 'de';
}
if (browserLanguage.startsWith('fr')) {
return 'fr';
}
if (browserLanguage.startsWith('es')) {
return 'es';
}
const allLanguages = navigator.languages || [navigator.language];
for (const lang of allLanguages) {
if (lang.startsWith('ceb') || lang.startsWith('bis')) {
@@ -97,7 +104,19 @@ const store = createStore({
}
}
}
for (const lang of allLanguages) {
if (lang.startsWith('fr')) {
return 'fr';
}
}
for (const lang of allLanguages) {
if (lang.startsWith('es')) {
return 'es';
}
}
return 'en';
})(),
menu: JSON.parse(localStorage.getItem('menu')) || [],
@@ -147,9 +166,13 @@ const store = createStore({
}
const germanSpeakingCountries = ['de', 'at', 'ch', 'li'];
if (browserLanguage.startsWith('de')) {
state.language = 'de';
} else if (browserLanguage.startsWith('fr')) {
state.language = 'fr';
} else if (browserLanguage.startsWith('es')) {
state.language = 'es';
} else {
const allLanguages = navigator.languages || [navigator.language];
let isGerman = false;
@@ -173,7 +196,23 @@ const store = createStore({
}
}
}
state.language = isGerman ? 'de' : 'en';
if (isGerman) {
state.language = 'de';
return;
}
for (const lang of allLanguages) {
if (lang.startsWith('fr')) {
state.language = 'fr';
return;
}
}
for (const lang of allLanguages) {
if (lang.startsWith('es')) {
state.language = 'es';
return;
}
}
state.language = 'en';
}
},
setLanguage(state, language) {

View File

@@ -1,25 +1,17 @@
import { getPublicBaseUrl } from './appConfig.js';
import {
SEO_UI_LOCALES,
HREFLANG_FOR_UI,
OG_LOCALE_FOR_UI,
} from '../i18n/supportedLocales.js';
const DEFAULT_BASE_URL = 'https://www.your-part.de';
const DEFAULT_SITE_NAME = 'YourPart';
const DEFAULT_TITLE = 'YourPart - Community, Chat, Forum, Vokabeltrainer, Falukant und Minispiele';
const DEFAULT_DESCRIPTION = 'YourPart verbindet Community, Chat, Forum, Blogs, Vokabeltrainer, das Aufbauspiel Falukant und Browser-Minispiele auf einer Plattform.';
const DEFAULT_IMAGE = `${DEFAULT_BASE_URL}/images/logos/logo.png`;
const SEO_UI_LOCALES = ['de', 'en', 'es', 'ceb'];
const HREFLANG_FOR_UI = {
de: 'de',
en: 'en',
es: 'es',
ceb: 'ceb',
};
const OG_LOCALE_FOR_UI = {
de: 'de_DE',
en: 'en_GB',
es: 'es_ES',
ceb: 'ceb_PH',
};
export { SEO_UI_LOCALES, HREFLANG_FOR_UI, OG_LOCALE_FOR_UI };
const MANAGED_META_KEYS = [
['name', 'description'],
@@ -138,7 +130,7 @@ function uiLocaleToOgLocale(ui) {
return OG_LOCALE_FOR_UI[ui] || OG_LOCALE_FOR_UI.de;
}
/** OpenGraph locale (z. B. de_DE) aus UI-Sprache (de|en|es|ceb). */
/** OpenGraph locale (z. B. de_DE) aus UI-Sprache (de|en|es|ceb|fr). */
export function seoOgLocale(ui) {
return uiLocaleToOgLocale(ui);
}

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') {