feat(enforcement): record decisions and send notices (#3492)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-15 07:12:46 +02:00
1 parent da1842ecab
commit f5c30fdf8f
383 files changed
+29692 -4139

No files matched your search

@@ -25,17 +25,16 @@ type PlaceholderName<T extends string> = ValidPlaceholderName<PlaceholderHead<T>
type PlaceholderNameAfterOpen<T extends string> = T extends `${infer Placeholder}}${string}`
? PlaceholderName<Placeholder>
: never;
type RestAfterPlaceholder<T extends string> = T extends `${string}}${infer Rest}` ? Rest : '';
type NumericPlaceholderNameAfterOpen<T extends string> = T extends `${infer Name}, plural,${string}`
? PlaceholderName<Name>
? ValidPlaceholderName<Trim<Name>>
: T extends `${infer Name}, selectordinal,${string}`
? PlaceholderName<Name>
? ValidPlaceholderName<Trim<Name>>
: never;
type TemplatePlaceholderNames<T extends string> = T extends `${string}{${infer AfterOpen}`
? PlaceholderNameAfterOpen<AfterOpen> | TemplatePlaceholderNames<RestAfterPlaceholder<AfterOpen>>
? PlaceholderNameAfterOpen<AfterOpen> | TemplatePlaceholderNames<AfterOpen>
: never;
type NumericTemplatePlaceholderNames<T extends string> = T extends `${string}{${infer AfterOpen}`
? NumericPlaceholderNameAfterOpen<AfterOpen> | NumericTemplatePlaceholderNames<RestAfterPlaceholder<AfterOpen>>
? NumericPlaceholderNameAfterOpen<AfterOpen> | NumericTemplatePlaceholderNames<AfterOpen>
: never;
export type MessageVariablesForTemplate<T extends string> = [TemplatePlaceholderNames<T>] extends [never]
? never
@@ -43,4 +43,13 @@ describe('message catalog type helpers', () => {
expectTypeOf<Args>().toEqualTypeOf<[variables: {userName: string | number | boolean | Date}]>();
expectTypeOf<NoArgs>().toEqualTypeOf<[variables?: undefined]>();
});
it('types placeholders nested inside select and plural cases', () => {
type Variables =
MessageVariablesForTemplate<'{all, select, yes {All gone.} other {{count, plural, =1 {# item} other {# items}} until {ends_at}.}}'>;
expectTypeOf<Variables>().toEqualTypeOf<{
all: string | number | boolean | Date;
count: number;
ends_at: string | number | boolean | Date;
}>();
});
});
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import * as fs from 'node:fs';
import * as path from 'node:path';
import {fileURLToPath} from 'node:url';
import {extractMessageTemplateVariables} from '@fluxer/i18n/src/runtime/MessageCatalogTypes';
import MessageFormat from '@messageformat/core';
import {describe, expect, it} from 'vitest';
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../..');
const CATALOG_DIRS = {
content: 'fluxer_api/src/api/content_i18n/weblate',
email: 'fluxer_api/pkgs/email/src/email_i18n/weblate',
errors: 'packages/errors/src/i18n/weblate',
} as const;
type CatalogEntry = string | {subject: string; body: string};
function readCatalog(file: string): Record<string, CatalogEntry> {
return JSON.parse(fs.readFileSync(file, 'utf8')) as Record<string, CatalogEntry>;
}
function templates(entry: CatalogEntry): Array<[string, string]> {
return typeof entry === 'string'
? [['', entry]]
: [
['.subject', entry.subject],
['.body', entry.body],
];
}
function sortedVariables(template: string): Array<string> {
return [...extractMessageTemplateVariables(template)].sort();
}
const CASES = Object.entries(CATALOG_DIRS).flatMap(([catalog, dir]) => {
const localeDir = path.join(REPO_ROOT, dir, 'locales');
return fs
.readdirSync(localeDir)
.filter((file) => file.endsWith('.json'))
.map((file) => ({
catalog,
locale: file.slice(0, -'.json'.length),
source: readCatalog(path.join(REPO_ROOT, dir, 'messages.json')),
messages: readCatalog(path.join(localeDir, file)),
}));
});
describe('static i18n catalogs', () => {
it('finds every catalog', () => {
expect(new Set(CASES.map((entry) => entry.catalog))).toEqual(new Set(Object.keys(CATALOG_DIRS)));
});
it.each(CASES.map((entry) => [`${entry.catalog} ${entry.locale}`, entry] as const))(
'%s compiles under the plural rules of its locale',
(_label, {locale, messages}) => {
const messageFormat = new MessageFormat(locale);
const failures = Object.entries(messages).flatMap(([key, entry]) =>
templates(entry).flatMap(([part, template]) => {
try {
messageFormat.compile(template);
return [];
} catch (error) {
return [`${key}${part}: ${String(error).split('\n')[0]}`];
}
}),
);
expect(failures).toEqual([]);
},
);
it.each(CASES.map((entry) => [`${entry.catalog} ${entry.locale}`, entry] as const))(
'%s uses the variables of the English source',
(_label, {source, messages}) => {
const mismatches = Object.entries(messages).flatMap(([key, entry]) => {
const sourceEntry = source[key];
if (sourceEntry === undefined) return [`${key}: not in the source catalog`];
const sourceTemplates = new Map(templates(sourceEntry));
return templates(entry).flatMap(([part, template]) => {
const expected = sortedVariables(sourceTemplates.get(part) ?? '');
const actual = sortedVariables(template);
return expected.join(',') === actual.join(',') ? [] : [`${key}${part}: ${actual} != ${expected}`];
});
});
expect(mismatches).toEqual([]);
},
);
});