All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 7m28s
CI / Build container images (pull_request) Successful in 2m7s
CI / Auth e2e pack (pull_request) Successful in 9m37s
CI / Import/export fidelity gate (pull_request) Successful in 1m7s
CD / Build and push images (push) Successful in 23s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m47s
CD / Promote to Int (push) Successful in 16s
CI / Lint, typecheck, test (push) Successful in 7m25s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 9m41s
CI / Import/export fidelity gate (push) Successful in 1m12s
An instance had no way to look like itself: the top bar said "Dorfteich" whatever the operator called their instance, `instance.name` was never rendered in the running app at all, and there was no favicon anywhere — `index.html` had no `<link rel="icon">` and `public/` held only fonts and theme-init.js. Where the line is drawn, and why: - **The api never decodes an image.** Cropping, scaling and the conversion to PNG happen on a canvas in the browser; the api checks the PNG signature, reads the IHDR dimensions at their fixed offsets and enforces the caps. An image library would put a decoder in front of attacker-supplied bytes AND would have to be carried through the `--network none` offline build. Reading two big-endian integers is not decoding. - **SVG is refused**, with its own error message rather than a generic "not a PNG": it can carry script, and serving it from our own origin would be a cross-site-scripting vector. An operator who tried one should learn that it is deliberate. - **The crop is driven by number inputs, not by dragging.** A drag-only cropper excludes keyboard and switch users outright; a number input is arrow-key operable and screen-reader readable without any custom aria. The resulting pixel size is stated in text, not only drawn as a frame. - **The variant is chosen by CSS, not JavaScript.** `theme-init.js` has already resolved `data-theme` before first paint, so the correct logo is the one painted rather than the one that appears after a flash. Without a dark variant the LIGHT logo carries both themes — the operator's own asset shown unchanged beats one they did not choose (the rule #307 extends to ponds). The settings screen warns; it never blocks. - **The favicon link is static, its resource dynamic.** index.html stays a static file and the api answers with the uploaded icon or a shipped default — that route must never 404, or the browser keeps its generic icon for good. The default is generated by a script from Node's own zlib (`gen-default-favicon.mjs`), for the same offline-build reason. - Both favicon sizes are uploaded together: one source, one crop, so the tab icon and the home-screen icon can never disagree. - Branding is served WITHOUT a session, because the login screen carries it and the browser fetches the favicon before anyone signs in. The admin screen says so — an operator may not expect their logo to be public. - The metadata is not writable through the settings endpoint: it describes bytes on disk, and hand-writing it would claim an asset that is not there. `./data/branding` follows the three-step rule #303 paid for: env default + `data-dirs.ts` entry, compose volume (repo AND the stages on ONE), and the `mkdir`/`chown` line in the api Dockerfile. `data-dirs.test.ts` is new and closes the hole that made #303's variant invisible: the nightly archive skips a missing directory WORDLESSLY, so the fence now demands that every `*_DIR` the backup env declares actually travels in the archive. Verified against the real defect — removing the line fails it by name. Audit catalogue v1.7 (`branding.changed`), carrying `scope` from the start so #307 is the same event with a different scope, not a second id. Verified: api suite 103 files green (a lone `public-api` ECONNRESET under local parallel load, green in isolation — the documented local flake); branding suite 12 tests against a real directory; crop arithmetic unit tests; a11y pack 11/11 in both schemes; /admin measured at 320px with the new section (overflow 0); and the whole flow walked in the browser: upload → crop 780×180 → stored as 512×118 → logo in the sidebar linking home with the instance name as its accessible name → topbar wordmark following `instance.name` → light logo still shown under `data-theme="dark"`.
154 lines
6.3 KiB
TypeScript
154 lines
6.3 KiB
TypeScript
import deAccess from '@dorfteich/shared/i18n/de/access.json';
|
|
import deAuth from '@dorfteich/shared/i18n/de/auth.json';
|
|
import deBranding from '@dorfteich/shared/i18n/de/branding.json';
|
|
import deComments from '@dorfteich/shared/i18n/de/comments.json';
|
|
import deCommon from '@dorfteich/shared/i18n/de/common.json';
|
|
import deEditor from '@dorfteich/shared/i18n/de/editor.json';
|
|
import deErrors from '@dorfteich/shared/i18n/de/errors.json';
|
|
import deExport from '@dorfteich/shared/i18n/de/export.json';
|
|
import deFiles from '@dorfteich/shared/i18n/de/files.json';
|
|
import deFont from '@dorfteich/shared/i18n/de/font.json';
|
|
import deGraph from '@dorfteich/shared/i18n/de/graph.json';
|
|
import deImport from '@dorfteich/shared/i18n/de/import.json';
|
|
import deLabels from '@dorfteich/shared/i18n/de/labels.json';
|
|
import deLegal from '@dorfteich/shared/i18n/de/legal.json';
|
|
import deLinks from '@dorfteich/shared/i18n/de/links.json';
|
|
import deMembers from '@dorfteich/shared/i18n/de/members.json';
|
|
import deNotifications from '@dorfteich/shared/i18n/de/notifications.json';
|
|
import dePonds from '@dorfteich/shared/i18n/de/ponds.json';
|
|
import dePlugins from '@dorfteich/shared/i18n/de/plugins.json';
|
|
import dePublic from '@dorfteich/shared/i18n/de/public.json';
|
|
import deQuotas from '@dorfteich/shared/i18n/de/quotas.json';
|
|
import deSearch from '@dorfteich/shared/i18n/de/search.json';
|
|
import deSetup from '@dorfteich/shared/i18n/de/setup.json';
|
|
import deApiTokens from '@dorfteich/shared/i18n/de/apiTokens.json';
|
|
import deSystem from '@dorfteich/shared/i18n/de/system.json';
|
|
import deTasks from '@dorfteich/shared/i18n/de/tasks.json';
|
|
import deUsers from '@dorfteich/shared/i18n/de/users.json';
|
|
import deWatches from '@dorfteich/shared/i18n/de/watches.json';
|
|
import deSettings from '@dorfteich/shared/i18n/de/settings.json';
|
|
import enAccess from '@dorfteich/shared/i18n/en/access.json';
|
|
import enAuth from '@dorfteich/shared/i18n/en/auth.json';
|
|
import enBranding from '@dorfteich/shared/i18n/en/branding.json';
|
|
import enComments from '@dorfteich/shared/i18n/en/comments.json';
|
|
import enCommon from '@dorfteich/shared/i18n/en/common.json';
|
|
import enEditor from '@dorfteich/shared/i18n/en/editor.json';
|
|
import enErrors from '@dorfteich/shared/i18n/en/errors.json';
|
|
import enExport from '@dorfteich/shared/i18n/en/export.json';
|
|
import enFiles from '@dorfteich/shared/i18n/en/files.json';
|
|
import enFont from '@dorfteich/shared/i18n/en/font.json';
|
|
import enGraph from '@dorfteich/shared/i18n/en/graph.json';
|
|
import enImport from '@dorfteich/shared/i18n/en/import.json';
|
|
import enLabels from '@dorfteich/shared/i18n/en/labels.json';
|
|
import enLegal from '@dorfteich/shared/i18n/en/legal.json';
|
|
import enLinks from '@dorfteich/shared/i18n/en/links.json';
|
|
import enMembers from '@dorfteich/shared/i18n/en/members.json';
|
|
import enNotifications from '@dorfteich/shared/i18n/en/notifications.json';
|
|
import enPonds from '@dorfteich/shared/i18n/en/ponds.json';
|
|
import enPlugins from '@dorfteich/shared/i18n/en/plugins.json';
|
|
import enPublic from '@dorfteich/shared/i18n/en/public.json';
|
|
import enQuotas from '@dorfteich/shared/i18n/en/quotas.json';
|
|
import enSearch from '@dorfteich/shared/i18n/en/search.json';
|
|
import enSetup from '@dorfteich/shared/i18n/en/setup.json';
|
|
import enApiTokens from '@dorfteich/shared/i18n/en/apiTokens.json';
|
|
import enSystem from '@dorfteich/shared/i18n/en/system.json';
|
|
import enTasks from '@dorfteich/shared/i18n/en/tasks.json';
|
|
import enUsers from '@dorfteich/shared/i18n/en/users.json';
|
|
import enWatches from '@dorfteich/shared/i18n/en/watches.json';
|
|
import enSettings from '@dorfteich/shared/i18n/en/settings.json';
|
|
import i18n from 'i18next';
|
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
|
import { initReactI18next } from 'react-i18next';
|
|
|
|
/**
|
|
* Translation resources are bundled (no async loading): the whole catalog
|
|
* is small, and the offline-capable editor (ADR 0003) must not depend on
|
|
* fetching language files. Detection: ?lng=… wins, then the browser
|
|
* language; the user-profile setting (issue #17) will be layered on top.
|
|
*/
|
|
void i18n
|
|
.use(LanguageDetector)
|
|
.use(initReactI18next)
|
|
.init({
|
|
resources: {
|
|
en: {
|
|
common: enCommon,
|
|
comments: enComments,
|
|
errors: enErrors,
|
|
access: enAccess,
|
|
auth: enAuth,
|
|
settings: enSettings,
|
|
editor: enEditor,
|
|
export: enExport,
|
|
files: enFiles,
|
|
branding: enBranding,
|
|
font: enFont,
|
|
graph: enGraph,
|
|
import: enImport,
|
|
labels: enLabels,
|
|
legal: enLegal,
|
|
links: enLinks,
|
|
members: enMembers,
|
|
notifications: enNotifications,
|
|
ponds: enPonds,
|
|
plugins: enPlugins,
|
|
public: enPublic,
|
|
quotas: enQuotas,
|
|
search: enSearch,
|
|
setup: enSetup,
|
|
apiTokens: enApiTokens,
|
|
system: enSystem,
|
|
tasks: enTasks,
|
|
users: enUsers,
|
|
watches: enWatches,
|
|
},
|
|
de: {
|
|
common: deCommon,
|
|
comments: deComments,
|
|
errors: deErrors,
|
|
access: deAccess,
|
|
auth: deAuth,
|
|
settings: deSettings,
|
|
editor: deEditor,
|
|
export: deExport,
|
|
files: deFiles,
|
|
branding: deBranding,
|
|
font: deFont,
|
|
graph: deGraph,
|
|
import: deImport,
|
|
labels: deLabels,
|
|
legal: deLegal,
|
|
links: deLinks,
|
|
members: deMembers,
|
|
notifications: deNotifications,
|
|
ponds: dePonds,
|
|
plugins: dePlugins,
|
|
public: dePublic,
|
|
quotas: deQuotas,
|
|
search: deSearch,
|
|
setup: deSetup,
|
|
apiTokens: deApiTokens,
|
|
system: deSystem,
|
|
tasks: deTasks,
|
|
users: deUsers,
|
|
watches: deWatches,
|
|
},
|
|
},
|
|
defaultNS: 'common',
|
|
fallbackLng: 'en',
|
|
supportedLngs: ['de', 'en'],
|
|
interpolation: { escapeValue: false }, // React already escapes.
|
|
detection: { order: ['querystring', 'navigator'], lookupQuerystring: 'lng', caches: [] },
|
|
});
|
|
|
|
// Mirror the active language onto <html lang> (issue #163, WCAG 3.1.1):
|
|
// index.html ships a static fallback, and screen readers pick their voice
|
|
// from this attribute. Fires on init and on every later switch (e.g. the
|
|
// user-profile locale applied in auth-context).
|
|
i18n.on('languageChanged', (lng) => {
|
|
document.documentElement.lang = lng;
|
|
});
|
|
if (i18n.resolvedLanguage) document.documentElement.lang = i18n.resolvedLanguage;
|
|
|
|
export default i18n;
|