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
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
44 lines
1.7 KiB
TypeScript
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;
|
|
}
|