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
46 lines
1.8 KiB
TypeScript
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;
|
|
}
|