mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
feat(enforcement): record decisions and send notices (#3492)
This commit is contained in:
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([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user