dorfteich/apps/api/src/i18n/api-i18n.ts
Claude Fable 5 fd2bdb3fb8
All checks were successful
CI / Lint, typecheck, test (push) Successful in 3m7s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m14s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 9s
CI / Auth e2e pack (push) Successful in 5m9s
CI / Import/export fidelity gate (push) Successful in 45s
Add instance legal pages with public rendering and footer links (#82)
Imprint and privacy policy are two new Markdown instance settings
(legal.imprint, legal.privacyPolicy), edited by Site Admins in a new
"Legal pages" admin section with a toggleable rendered preview. The
preview uses the same shared pipeline the server renders with
(markdown → schema doc → escaped HTML), so stored markup can never
smuggle script to visitors.

The pages render publicly at /legal/imprint and /legal/privacy — as an
SPA route plus, like #56, a self-contained server-rendered HTML
document under /api/v1/legal/:kind. The endpoints are setup-exempt:
legal information stays reachable even while the first-run wizard is
pending. Unconfigured pages show a localized notice instead of 404ing,
and Site Admins additionally get a warning banner linking to the
settings. A new footer with both links appears on every SPA view
(editor, auth screens, public pages) and in the server-rendered
documents, whose shared shell moved to public/html-shell.ts and now
renders its chrome in the instance default locale (ADR 0012).

docs/self-hosting/legal-template.md ships imprint and privacy-policy
templates in English and German whose sections mirror Dorfteich's
actual processing activities (accounts, sessions, rate-limit IPs,
proxy logs, transactional mail, content, export, deletion, no
third-party requests), with a review checklist tied to security.md
§Privacy.

New `legal` i18n namespace (de+en); api and web e2e coverage including
a new CI legal pack (footer navigation, notice vs. admin banner, and
the admin form publishing a text end to end).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-11 16:40:04 +02:00

44 lines
1.7 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 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 { 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 },
de: { errors: deErrors, mails: deMails, legal: deLegal },
},
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;
}