import type { SyncExpectationResult } from '@vitest/expect'; import { describe, expect, test } from 'vitest'; import { normaliseKey } from '#shared/buildPronoun.ts'; import { Example } from '#shared/language/examples.ts'; import type { VariantsFromBaseConverter } from '#shared/language/grammarTables.ts'; import { availableGenders, gendersWithNumerus, numeri } from '#shared/nouns.ts'; import type { NounsData } from '#shared/nouns.ts'; 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'; 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 { toHaveValidMorphemes(morphemes: string[]): R; } declare module 'vitest' { interface Assertion extends CustomMatchers {} } expect.extend({ toHaveValidMorphemes }); describe.each(allLocales)('data files of $code', async ({ code }) => { const config = await loadSuml(`locale/${code}/config.suml`); const pronouns = await loadTsv>(`locale/${code}/pronouns/pronouns.tsv`); const pronounGroups = await loadTsv(`locale/${code}/pronouns/pronounGroups.tsv`); const examples = await loadTsv(`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(`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(`locale/${code}/nouns/nounsData.suml`); } catch (error) { nounsData = undefined; } if (config.nouns.enabled && config.nouns.declension) { test('config.nouns.declension.enabled requires properties in nouns/nounsData.suml', () => { expect(nounsData?.declensions).toBeDefined(); }); } 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 numeri) { expect(Object.keys(nounsData.cases!)) .toEqual(expect.arrayContaining(Object.keys(declension[numerus] ?? {}))); } } }); } }); } });