fix(api): send correct staff emails and allow suppressing them (#3048)

This commit is contained in:
Hampus
2026-09-29 23:55:54 +02:00
committed by GitHub
parent f0b3c82cfd
commit 39f9beda5a
113 changed files with 2496 additions and 206 deletions
@@ -3,7 +3,7 @@
import type {I18nResult, TemplateCompiler} from '@fluxer/i18n/src/runtime/I18nTypes';
import {renderTemplate} from '@fluxer/i18n/src/runtime/RenderTemplate';
import {getEffectiveStaticLocale, hasStaticLocale} from '@fluxer/i18n/src/runtime/StaticLocale';
import type MessageFormat from '@messageformat/core';
import type {default as MessageFormat, MessageFormatOptions} from '@messageformat/core';
export type StaticLocaleMessages<TKey extends string, TValue> = Partial<Record<TKey, TValue>>;
@@ -14,6 +14,7 @@ export interface StaticI18nConfig<TKey extends string, TValue, TVariables> {
normalizeLocale?: (locale: string) => string;
onWarning?: (message: string) => void;
validateVariables?: (key: TKey, template: TValue, variables: TVariables) => string | null;
messageFormatOptions?: MessageFormatOptions<'string'>;
}
interface StaticI18nModule<TKey extends string, TValue, TVariables> {
@@ -70,6 +71,7 @@ export function createStaticI18n<TKey extends string, TValue, TVariables>(
variables,
compile,
messageFormatCache,
messageFormatOptions: config.messageFormatOptions,
});
},
hasLocale(locale: string): boolean {
@@ -109,6 +109,54 @@ export function extractMessageTemplateVariables(template: string): Set<string> {
return variables;
}
function collectMessageTemplatePlaceholders(tokens: ReadonlyArray<Token>, placeholders: Set<string>): void {
for (const token of tokens) {
switch (token.type) {
case 'argument': {
placeholders.add(token.arg);
break;
}
case 'function': {
const param = (token.param ?? [])
.map((paramToken) => (paramToken.type === 'content' ? paramToken.value : ''))
.join('')
.trim();
placeholders.add(`${token.arg},${token.key},${param}`);
if (token.param) {
collectMessageTemplatePlaceholders(token.param, placeholders);
}
break;
}
case 'select': {
const keys = token.cases.map((selectCase) => selectCase.key).sort();
placeholders.add(`${token.arg},select[${keys.join(',')}]`);
for (const selectCase of token.cases) {
collectMessageTemplatePlaceholders(selectCase.tokens, placeholders);
}
break;
}
case 'plural':
case 'selectordinal': {
placeholders.add(`${token.arg},plural`);
for (const selectCase of token.cases) {
collectMessageTemplatePlaceholders(selectCase.tokens, placeholders);
}
break;
}
case 'content':
case 'octothorpe': {
break;
}
}
}
}
export function extractMessageTemplatePlaceholders(template: string): Array<string> {
const placeholders = new Set<string>();
collectMessageTemplatePlaceholders(parse(template), placeholders);
return [...placeholders].sort();
}
export function validateMessageTemplateVariables(
template: string,
variables: Record<string, unknown> | undefined,
+4 -2
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {I18nResult, TemplateCompiler} from '@fluxer/i18n/src/runtime/I18nTypes';
import MessageFormat from '@messageformat/core';
import MessageFormat, {type MessageFormatOptions} from '@messageformat/core';
interface TemplateRenderOptions<TKey extends string, TValue, TVariables> {
key: TKey;
@@ -10,6 +10,7 @@ interface TemplateRenderOptions<TKey extends string, TValue, TVariables> {
variables: TVariables;
compile: TemplateCompiler<TValue, TVariables>;
messageFormatCache: Map<string, MessageFormat>;
messageFormatOptions?: MessageFormatOptions<'string'>;
}
export function renderTemplate<TKey extends string, TValue, TVariables>({
@@ -19,11 +20,12 @@ export function renderTemplate<TKey extends string, TValue, TVariables>({
variables,
compile,
messageFormatCache,
messageFormatOptions,
}: TemplateRenderOptions<TKey, TValue, TVariables>): I18nResult<TKey, TValue> {
try {
let messageFormat = messageFormatCache.get(locale);
if (!messageFormat) {
messageFormat = new MessageFormat(locale);
messageFormat = new MessageFormat(locale, messageFormatOptions);
messageFormatCache.set(locale, messageFormat);
}
return {
@@ -3,6 +3,7 @@
import type {MessageArgsForTemplate, MessageVariablesForTemplate} from '@fluxer/i18n/src/runtime/MessageCatalogTypes';
import {
defineStaticLocaleMessages,
extractMessageTemplatePlaceholders,
extractMessageTemplateVariables,
validateMessageTemplateVariables,
} from '@fluxer/i18n/src/runtime/MessageCatalogTypes';
@@ -19,6 +20,21 @@ describe('message catalog type helpers', () => {
expect([...extractMessageTemplateVariables(template)].sort()).toEqual(['count', 'userName']);
expect(validateMessageTemplateVariables(template, {count: 2})).toBe('Missing required i18n variable: userName');
});
it('extracts placeholder signatures including formats and select cases', () => {
const template =
'{when, date, full} {when, time, short }{reason, select, null {} other {{reason}}} {count, plural, one {# {name}} other {#}}';
expect(extractMessageTemplatePlaceholders(template)).toEqual([
'count,plural',
'name',
'reason',
'reason,select[null,other]',
'when,date,full',
'when,time,short',
]);
expect(extractMessageTemplatePlaceholders('{count, plural, one {#} few {#} other {#}}')).toEqual(
extractMessageTemplatePlaceholders('{count, plural, one {#} other {#}}'),
);
});
it('types placeholders from literal templates', () => {
type Variables =
MessageVariablesForTemplate<'Hello {userName}, you have {count, plural, one {one invite} other {# invites}}.'>;