mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
The privacy dashboard re-implemented the audit-retention resolver with its own default and its own floor. Two implementations of one disclosure, quoted to the same person on two pages and by the purge job; it uses the shared resolver now. The record-content branch in the settings gate matched a classification rather than a page, so a second manage-writable destination would have rendered the allergy and family-history managers under its own heading without anybody choosing that. It resolves through a slug map, and the contract test fails if the map and the classification disagree in either direction — a destination cannot join the kind without somebody deciding what it renders. The managed-profile nav filter assumed a guardian grant is always manage. It is today, which is why the check was easy to leave out; an entry below that level would have listed destinations the gate refuses. Interpolation used a replacement string, and String.replace reads $&, $`, $', $1 and $$ out of one. A display name containing $' rewrote the sentence around it — and display names land in exactly the sentences that say who did what in somebody's health record. A replacer function has no $ grammar at all, which removes the class rather than escaping the members of it somebody thought of.
43 lines
1.7 KiB
TypeScript
43 lines
1.7 KiB
TypeScript
import { defaultLocale, type Locale } from "./config";
|
|
import { allMessages, resolveKey } from "./shared-resolve";
|
|
|
|
export interface ServerTranslator {
|
|
locale: Locale;
|
|
t(key: string, params?: Record<string, string | number>): string;
|
|
}
|
|
|
|
/**
|
|
* Server-side translator that reads the same messages/{en,de}.json bundles
|
|
* the client uses. Falls back to English, then to the raw key.
|
|
*
|
|
* Use this from API routes / background jobs where useTranslations() is not
|
|
* available. Mirrors the behaviour of the client I18nProvider.
|
|
*/
|
|
export function getServerTranslator(locale: Locale): ServerTranslator {
|
|
return {
|
|
locale,
|
|
t(key, params) {
|
|
let value = resolveKey(allMessages[locale], key);
|
|
if (value === undefined && locale !== defaultLocale) {
|
|
value = resolveKey(allMessages[defaultLocale], key);
|
|
}
|
|
if (value === undefined) return key;
|
|
if (params) {
|
|
for (const [k, v] of Object.entries(params)) {
|
|
// A REPLACER FUNCTION, not a replacement string. `String.replace`
|
|
// reads `$&`, `$\``, `$'`, `$1` and `$$` out of a replacement
|
|
// STRING, so a value carrying one of them rewrites the sentence
|
|
// around it: a display name of `$\'` splices everything after the
|
|
// placeholder back in, and `$\`` splices everything before it. Every
|
|
// interpolated value here is user-controlled — a person's display
|
|
// name is the common one, and it lands in sentences on the sharing
|
|
// panel that describe who did what. A function replacement is
|
|
// substituted verbatim and has no `$` grammar at all.
|
|
value = value.replace(new RegExp(`\\{${k}\\}`, "g"), () => String(v));
|
|
}
|
|
}
|
|
return value;
|
|
},
|
|
};
|
|
}
|