dorfteich/apps/api/src/i18n/api-i18n.ts
Claude Fable 5 e164370691 #154: Aufgabenübersicht als Kern-Block (Seite + Unterseiten)
Neuer Block-Atom task_overview (Markdown-Fence dorfteich-tasks,
HTML-Placeholder). Shared extractTaskRows liest Task-Zeilen mit Text,
Mentions (#150) und Start-/Zieldaten (#152); TasksService sammelt zur
Lesezeit den Teilbaum (rekursiv via collectSubtreeIds, canAccessPage-
Filter je Quellseite) aus Basis-State + page_updates-Log — KEINE
abgeleitete Tabelle nötig (Teilbäume sind klein, kein Drift). Neuer
auth-Endpoint GET /read/:pond/:slug/tasks; die öffentliche Ansicht
expandiert den Placeholder serverseitig zur statischen Tabelle
(Instanz-Sprache). NodeView mit Live-Tabelle und Rückschreib-Checkboxen
(optimistisch, Override bis der debounced Collab-Persist nachzieht);
Einfügen über die Block-Auswahl (eingebauter Eintrag). Unit- + DB-Tests,
neuer CI-Pack tasks.spec (voller Loop inkl. Rückschreiben end-to-end),
User-Guide-Doku en+de.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-20 01:20:30 +02:00

46 lines
1.8 KiB
TypeScript

import deErrors from '@dorfteich/shared/i18n/de/errors.json';
import deLegal from '@dorfteich/shared/i18n/de/legal.json';
import deMails from '@dorfteich/shared/i18n/de/mails.json';
import deTasks from '@dorfteich/shared/i18n/de/tasks.json';
import enErrors from '@dorfteich/shared/i18n/en/errors.json';
import enLegal from '@dorfteich/shared/i18n/en/legal.json';
import enMails from '@dorfteich/shared/i18n/en/mails.json';
import enTasks from '@dorfteich/shared/i18n/en/tasks.json';
import { createInstance, type i18n as I18n } from 'i18next';
/**
* Minimal i18next instance for user-facing api texts (error messages,
* later e-mails). Kept separate from any request context — callers pass
* the language explicitly.
*/
export const apiI18n: I18n = createInstance();
void apiI18n.init({
resources: {
en: { errors: enErrors, mails: enMails, legal: enLegal, tasks: enTasks },
de: { errors: deErrors, mails: deMails, legal: deLegal, tasks: deTasks },
},
fallbackLng: 'en',
supportedLngs: ['de', 'en'],
interpolation: { escapeValue: false },
});
/** Best supported language for an Accept-Language header value. */
export function negotiateLanguage(acceptLanguage: string | undefined): 'de' | 'en' {
if (!acceptLanguage) return 'en';
// First matching language tag wins; quality factors are ignored on
// purpose — with two languages the added complexity buys nothing.
for (const part of acceptLanguage.split(',')) {
const tag = part.trim().toLowerCase();
if (tag.startsWith('de')) return 'de';
if (tag.startsWith('en')) return 'en';
}
return 'en';
}
/** Localized message for an error code, or undefined when uncatalogued. */
export function translateErrorCode(code: string, language: 'de' | 'en'): string | undefined {
const key = `errors:${code}`;
return apiI18n.exists(key) ? apiI18n.t(key, { lng: language }) : undefined;
}