PronounsPage/test/locales/data.test.ts
2025-07-27 23:43:57 +02:00

227 lines
9.6 KiB
TypeScript

import type { SyncExpectationResult } from '@vitest/expect';
import { describe, expect, test } from 'vitest';
import type { Config } from '~/locale/config.ts';
import type { NounTemplatesData, PronounGroupData, PronounExamplesData, PronounData } from '~/locale/data.ts';
import allLocales from '~/locale/locales.ts';
import { loadSuml, loadTsv } from '~/server/loader.ts';
import { normaliseKey } from '~/src/buildPronoun.ts';
import { Example } from '~/src/language/examples.ts';
import type { VariantsFromBaseConverter } from '~/src/language/grammarTables.ts';
import { availableGenders, gendersWithNumerus } from '~/src/nouns.ts';
import type { NounsData } from '~/src/nouns.ts';
function toHaveValidMorphemes(actual: string, morphemes: string[]): SyncExpectationResult {
const containedMorphemes = Example.parse(actual).parts
.filter((part) => typeof part !== 'string' && part.type === 'morpheme')
.map((part) => part.morpheme);
const unknownMorphemes = containedMorphemes.filter((morpheme) => !morphemes.includes(morpheme));
if (unknownMorphemes.length > 0) {
return {
message: () => `expected example '${actual}' to have valid morphemes,` +
` but these are unknown:\n${unknownMorphemes.join(', ')}`,
pass: false,
};
} else {
return {
message: () => 'expected example to have invalid morphemes',
pass: true,
};
}
}
interface CustomMatchers<R> {
toHaveValidMorphemes(morphemes: string[]): R;
}
declare module 'vitest' {
interface Assertion<T> extends CustomMatchers<T> {}
}
expect.extend({ toHaveValidMorphemes });
describe.each(allLocales)('data files of $code', async ({ code }) => {
const config = await loadSuml<Config>(`locale/${code}/config.suml`);
const pronouns = await loadTsv<PronounData<string>>(`locale/${code}/pronouns/pronouns.tsv`);
const pronounGroups = await loadTsv<PronounGroupData>(`locale/${code}/pronouns/pronounGroups.tsv`);
const examples = await loadTsv<PronounExamplesData>(`locale/${code}/pronouns/examples.tsv`);
test('pronouns/pronouns.tsv match schema', async () => {
if (pronouns.length === 0) {
return;
}
const required = [
'key',
'description',
'normative',
'plural',
'pluralHonorific',
'pronounceable',
...config.pronouns.morphemes ?? [],
];
const optional = ['history', 'thirdForm', 'smallForm', 'sourcesInfo', 'hidden'];
const actual = Object.keys(pronouns[0]);
expect(actual).toEqual(expect.arrayContaining(required));
expect([...required, ...optional]).toEqual(expect.arrayContaining(actual));
});
test('pronouns/pronouns.tsv have unique keys', () => {
const keys = new Set();
for (const pronoun of pronouns) {
const pronounKeys = pronoun.key
.replace(/،/g, ',')
.split(',')
.map((key) => normaliseKey(key));
for (const key of pronounKeys) {
expect(keys).not.toContain(key);
keys.add(key);
}
}
});
test('pronouns/pronounGroups.tsv match schema', () => {
if (pronounGroups.length === 0) {
return;
}
const required = ['name', 'pronouns'];
const optional = ['key', 'description', 'hidden'];
const actual = Object.keys(pronounGroups[0]);
expect(actual).toEqual(expect.arrayContaining(required));
expect([...required, ...optional]).toEqual(expect.arrayContaining(actual));
});
test('pronouns/pronounGroups.tsv reference pronouns by canonical name', () => {
const knownPronounNames = pronouns.map((pronoun) => {
return pronoun.key.replace(/،/g, ',').split(',')[0];
});
for (const pronounGroup of pronounGroups) {
const pronounNames = pronounGroup.pronouns?.replace(/،/g, ',').split(',') ?? [];
for (const pronounName of pronounNames) {
expect(knownPronounNames).toContain(pronounName);
}
}
});
test('pronouns/examples.tsv match schema', () => {
if (examples.length === 0) {
return;
}
const required = [
'singular',
];
if (config.pronouns.plurals) {
required.push('plural');
}
if (config.pronouns.honorifics) {
required.push('isHonorific');
}
const actual = Object.keys(examples[0]);
expect(actual).toEqual(required);
});
test('pronouns/examples.tsv contain valid morphemes', async () => {
for (const example of examples) {
expect(example.singular).toHaveValidMorphemes(config.pronouns.morphemes ?? []);
if (example.plural) {
expect(example.plural).toHaveValidMorphemes(config.pronouns.morphemes ?? []);
}
}
});
test('pronouns/examples.tsv contains plural examples when language has plurals', () => {
const hasExamplesWithPlurals = examples.some((example) => {
return example.plural && example.plural !== example.singular;
});
expect(hasExamplesWithPlurals).toBe(!!config.pronouns.plurals);
});
test('pronouns/examples.tsv contains honorific examples when language has honorifics', () => {
const hasExamplesWithHonorifics = examples.some((example) => example.isHonorific);
expect(hasExamplesWithHonorifics).toBe(!!config.pronouns.honorifics);
});
if (config.nouns.templates?.enabled) {
const nounTemplates = await loadTsv<NounTemplatesData>(`locale/${code}/nouns/nounTemplates.tsv`);
describe('nouns/nounTemplates.tsv', () => {
test('match schema', () => {
if (nounTemplates.length === 0) {
return;
}
const required = [...availableGenders(config),
...availableGenders(config).map((gender) => `${gender}Pl`)];
const actual = Object.keys(nounTemplates[0]);
expect(actual).toEqual(required);
});
test('contains templates when templates are enabled', () => {
expect(nounTemplates.length > 0).toBe(config.nouns.templates?.enabled);
});
test('have exactly one hyphen as placeholder for root', () => {
for (const template of nounTemplates) {
for (const genderWithNumerus of gendersWithNumerus) {
const actual = template[genderWithNumerus];
if (typeof actual === 'string') {
expect(actual).toMatch(/^(?:[^-]*-[^-]*(?:\/|$))+/);
}
}
}
});
});
}
let nounsData: NounsData | undefined;
try {
nounsData = await loadSuml<NounsData>(`locale/${code}/nouns/nounsData.suml`);
} catch (error) {
nounsData = undefined;
}
if (config.nouns.enabled && config.nouns.conventions?.enabled) {
test('config.nouns.conventions.enabled requires properties in nouns/nounsData.suml', () => {
expect(nounsData?.morphemes).toBeDefined();
expect(nounsData?.conventions).toBeDefined();
expect(nounsData?.groups).toBeDefined();
expect(nounsData?.classes).toBeDefined();
expect(nounsData?.classExample).toBeDefined();
expect(nounsData?.examples).toBeDefined();
});
}
if (nounsData) {
describe('nouns/nounsData.suml', () => {
if (nounsData.grammarTables) {
test('grammar tables have valid variant types', async () => {
const grammarTableVariantsConverter = (await import(`~/locale/${code}/language/grammarTableVariantsConverter.ts`)).default as VariantsFromBaseConverter;
for (const grammarTable of nounsData.grammarTables!) {
for (const section of grammarTable.sections) {
if (!Array.isArray(section.variants)) {
expect(Object.keys(grammarTableVariantsConverter)).toContain(section.variants.type);
}
}
}
});
}
if (nounsData.conventions) {
test('conventions have valid morphemes', () => {
for (const convention of Object.values(nounsData.conventions!)) {
expect(nounsData.morphemes)
.toEqual(expect.arrayContaining(Object.keys(convention.morphemes)));
}
});
}
if (nounsData.conventions && nounsData.groups) {
test('convention groups reference conventions by key', () => {
for (const group of Object.values(nounsData.groups!)) {
expect(Object.keys(nounsData.conventions!)).toEqual(expect.arrayContaining(group.conventions));
}
});
}
if (nounsData.cases && nounsData.declensions) {
test('declensions have valid cases', () => {
for (const declension of Object.values(nounsData.declensions!)) {
for (const numerus of ['singular', 'plural'] as const) {
expect(Object.keys(nounsData.cases!))
.toEqual(expect.arrayContaining(Object.keys(declension[numerus] ?? {})));
}
}
});
}
});
}
});