import { beforeEach, describe, expect, test } from 'vitest'; import type { NullPronounsConfig } from '~/locale/config.ts'; import type { Translations } from '~/locale/translations.ts'; import { buildPronoun, buildPronounUsage } from '~/src/buildPronoun.ts'; import { PronounGroup, Pronoun, PronounLibrary } from '~/src/classes.ts'; import { Translator } from '~/src/translator.ts'; import { configWithPronouns } from '~/test/fixtures/config.ts'; import pronounsFactory, { generated as generatedPronouns } from '~/test/fixtures/pronouns.ts'; const translations: Translations = {}; beforeEach(() => { translations.pronouns = { any: { short: 'any', }, slashes: { plural: 'plural', pluralHonorific: 'plural-honorific', description: 'description', }, or: 'or', }; }); const translator = new Translator(translations, translations, configWithPronouns); let config = structuredClone(configWithPronouns); const pronouns = Object.fromEntries(Object.entries(pronounsFactory) .map(([name, pronounFactory]) => [name, pronounFactory(config)])); beforeEach(() => { config = structuredClone(configWithPronouns); }); test('finds pronouns by canonical name', () => { const actual = buildPronoun(pronouns, 'they', config, translator); expect(actual).toBeDefined(); expect(actual).toBe(pronouns.they); }); test('finds pronouns case insensitively by canonical name', () => { const actual = buildPronoun(pronouns, 'They', config, translator); expect(actual).toBeDefined(); expect(actual).toBe(pronouns.they); }); test('finds pronouns by alias', () => { const actual = buildPronoun(pronouns, 'they/them', config, translator); expect(actual).toBeDefined(); expect(actual).toBe(pronouns.they); }); test('finds pronouns case insensitively by alias', () => { const actual = buildPronoun(pronouns, 'They/Them', config, translator); expect(actual).toBeDefined(); expect(actual).toBe(pronouns.they); }); const emojiPronounsConfig = { description: 'Emojiself pronouns', history: 'Emojiself pronouns are intended for online communication and not supposed to be pronounced', morphemes: { pronoun_subject: '#', pronoun_object: '#', possessive_determiner: '#\'s', possessive_pronoun: '#\'s', reflexive: '#self', }, examples: ['💫', '💙'], }; describe('when configured that emojiself pronouns are available', () => { beforeEach(() => { config.pronouns.emoji = emojiPronounsConfig; }); test('builds pronouns from emoji', () => { const actual = buildPronoun(pronouns, '💙', config, translator); expect(actual).toEqual(new Pronoun( config, '💙', 'Emojiself pronouns', false, { pronoun_subject: '💙', pronoun_object: '💙', possessive_determiner: '💙\'s', possessive_pronoun: '💙\'s', reflexive: '💙self', }, [false], [false], [], 'Emojiself pronouns are intended for online communication and not supposed to be pronounced@__generator__', false, )); }); }); const nullPronounsConfig: NullPronounsConfig = { routes: ['avoiding', 'no-pronouns', 'null', 'pronounless', 'nullpronominal', 'nameself'], morphemes: { pronoun_subject: '#', pronoun_object: '#', possessive_determiner: '#\'s', possessive_pronoun: '#\'s', reflexive: '#self', }, examples: [':S'], }; describe('when configured that null pronouns are available', () => { beforeEach(() => { config.pronouns.null = nullPronounsConfig; translations.pronouns.null = { description: 'Some people prefer not using any pronouns', }; }); const expectedDescription = '{/avoiding=avoiding} / {/no-pronouns=no-pronouns} / {/null=null} / ' + '{/pronounless=pronounless} / {/nullpronominal=nullpronominal} / {/nameself=nameself}'; test('builds pronouns from name', () => { const actual = buildPronoun(pronouns, ':S', config, translator); expect(actual).toEqual(new Pronoun( config, 'S', expectedDescription, false, { pronoun_subject: 'S', pronoun_object: 'S', possessive_determiner: 'S\'s', possessive_pronoun: 'S\'s', reflexive: 'Sself', }, [false], [false], [], 'Some people prefer not using any pronouns@__generator__', false, null, null, null, false, true, )); }); test('builds nothing if name too long', () => { expect(buildPronoun(pronouns, ':Abcdefghijklmnopqrstuvwxyz0123456', config, translator)).toBeNull(); }); describe('with conditional placeholders', () => { beforeEach(() => { nullPronounsConfig.morphemes!.possessive_pronoun = '#/[sxzß]$/i#’|#s'; }); test.each(['S', 'Ringelnatz', 'Max', 'X'])('builds morpheme conditionally if name matches', (name) => { expect(buildPronoun(pronouns, `:${name}`, config, translator)!.morphemes.possessive_pronoun) .toBe(`${name}’`); }); test.each(['Sofi', 'Xavier'])('builds morpheme by default if name does not match', (name) => { expect(buildPronoun(pronouns, `:${name}`, config, translator)!.morphemes.possessive_pronoun) .toBe(`${name}s`); }); }); describe('when some morphemes are not defined in template', () => { beforeEach(() => { nullPronounsConfig.morphemes = { pronoun_subject: '#', pronoun_object: '#', possessive_determiner: '#\'s', possessive_pronoun: '#\'s', }; }); test('they become null', () => { const actual = buildPronoun(pronouns, ':S', config, translator); expect(actual).toEqual(new Pronoun( config, 'S', expectedDescription, false, { pronoun_subject: 'S', pronoun_object: 'S', possessive_determiner: 'S\'s', possessive_pronoun: 'S\'s', reflexive: null, }, [false], [false], [], 'Some people prefer not using any pronouns@__generator__', false, null, null, null, false, true, )); }); }); }); describe('when configured that slashes contain all morphemes', () => { beforeEach(() => { config.pronouns.honorifics = true; config.pronouns.generator!.slashes = true; }); test('builds generated pronoun from all morphemes', () => { const actual = buildPronoun(pronouns, 'ae/aer/aer/aers/aerself', config, translator); expect(actual).toBeDefined(); expect(actual).toEqual(generatedPronouns.aer(config)); }); test('unescapes morphemes', () => { const actual = buildPronoun(pronouns, 's`/he/hir/hir/hirs/hirself', config, translator); expect(actual).toBeDefined(); expect(actual).toEqual(generatedPronouns.sSlashHe(config)); }); test('builds generated pronoun from all required morphemes and plural modifier', () => { const actual = buildPronoun(pronouns, 'ae/aer/aer/aers/aerselves/:plural', config, translator); expect(actual).toBeDefined(); expect(actual).toEqual(generatedPronouns.aerPlural(config)); }); test('builds generated pronoun from all required morphemes and plural honorific modifier', () => { const actual = buildPronoun(pronouns, 'ae/aer/aer/aers/aerselves/:plural-honorific', config, translator); expect(actual).toBeDefined(); expect(actual).toEqual(generatedPronouns.aerPluralHonorific(config)); }); test('builds generated pronoun from all required morphemes and description', () => { const actual = buildPronoun( pronouns, 'ae/aer/aer/aers/aerself/:description=Neopronoun “ae” `/ “æ”', config, translator, ); expect(actual).toBeDefined(); expect(actual).toEqual(generatedPronouns.aerWithDescription(config)); }); test('builds generated pronoun from all required morphemes and modifiers', () => { const actual = buildPronoun( pronouns, 'ae/aer/:description=Neopronoun “ae” `/ “æ”/:plural/aer/aers/aerselves', config, translator, ); expect(actual).toBeDefined(); expect(actual).toEqual(generatedPronouns.aerPluralWithDescription(config)); }); test('builds generated pronoun with some morphemes empty', () => { const actual = buildPronoun(pronouns, 'ae/aer/aer/ /aerself', config, translator); expect(actual).toBeDefined(); expect(actual).toEqual(generatedPronouns.aerWithEmptyPossessivePronoun(config)); }); test('builds generated pronoun with morpheme at end empty', () => { const actual = buildPronoun(pronouns, 'ae/aer/aer/aers/', config, translator); expect(actual).toBeDefined(); expect(actual).toEqual(generatedPronouns.aerWithEmptyReflexive(config)); }); test('builds generated pronoun with some morphemes unset', () => { const actual = buildPronoun(pronouns, 'ae/aer/aer/~/aerself', config, translator); expect(actual).toBeDefined(); expect(actual).toEqual(generatedPronouns.aerWithUnsetPossessivePronoun(config)); }); test('builds nothing if morphemes are missing', () => { expect(buildPronoun(pronouns, 'ae/aer/aer/aerself', config, translator)).toBeNull(); }); test('builds nothing if too many morphemes are given', () => { expect(buildPronoun(pronouns, 'ae/aer/aer/aers/aerself/aer', config, translator)).toBeNull(); }); test('builds nothing if description too long', () => { expect(buildPronoun( pronouns, 'ae/aer/aer/aers/aerself/:description=Neopronoun “ae” `/ “æ” which is my favorite so you should use it too', config, translator, )).toBeNull(); }); }); describe('when configured that slashes contain some morphemes', () => { beforeEach(() => { config.pronouns.generator!.slashes = ['pronoun_subject', 'pronoun_object', 'possessive_determiner', 'reflexive']; }); test('builds generated pronoun from all required morphemes', () => { const actual = buildPronoun(pronouns, 'ae/aer/aer/aerself', config, translator); expect(actual).toBeDefined(); expect(actual).toEqual(generatedPronouns.aerWithUnsetPossessivePronoun(config)); }); test('builds nothing if morphemes are missing', () => { expect(buildPronoun(pronouns, 'ae/aer/aer', config, translator)).toBeNull(); }); test('builds nothing if too many morphemes are given', () => { expect(buildPronoun(pronouns, 'ae/aer/aer/aers/aerself', config, translator)).toBeNull(); }); }); describe('when configured that slashes cannot contain morphemes', () => { beforeEach(() => { config.pronouns.generator!.slashes = false; }); const pathBase = 'ae/aer/aer/aers/aerself/aer'; test.each([3, 4, 5, 6])('builds nothing if %d morphemes are given', (count) => { const path = pathBase.split('/').slice(0, count) .join('/'); expect(buildPronoun(pronouns, path.slice(0, count), config, translator)).toBeNull(); }); }); describe('building generated pronouns from commas', () => { test('succeeds with all parts present', () => { const actual = buildPronoun(pronouns, 'ae,aer,aer,aers,aerself,0,', config, translator); expect(actual).toBeDefined(); expect(actual).toEqual(generatedPronouns.aer(config)); }); test('succeeds with description missing present', () => { const actual = buildPronoun(pronouns, 'ae,aer,aer,aers,aerself,0', config, translator); expect(actual).toBeDefined(); expect(actual).toEqual(generatedPronouns.aer(config)); }); test('succeeds with base pronoun and some custom morphemes', () => { const actual = buildPronoun(pronouns, 'they,!2,aers,!2,', config, translator); expect(actual).toEqual(new Pronoun( config, 'they/them', '', false, { pronoun_subject: 'they', pronoun_object: 'them', possessive_determiner: 'their', possessive_pronoun: 'aers', reflexive: 'themselves', }, [true], [false], [], '__generator__', false, )); }); test('fails when too few parts are given', () => { expect(buildPronoun(pronouns, 'ae,aer,aer,aers,aerself', config, translator)).toBeNull(); }); test('fails when many few parts are given', () => { expect(buildPronoun(pronouns, 'ae,aer,aer,aers,aerself,aersing,0,', config, translator)).toBeNull(); }); test('fails when base pronoun is unknown', () => { expect(buildPronoun(pronouns, 's/he,!2,aers,!2,', config, translator)).toBeNull(); }); }); describe('builds interchangeable pronouns from ampersand', () => { test('two interchangeable pronouns', () => { const actual = buildPronoun(pronouns, 'he&she', config, translator); expect(actual).toEqual(new Pronoun( config, 'he&she', ['Normative “he/him”', 'Normative “she/her”'], true, { pronoun_subject: 'he&she', pronoun_object: 'him&her', possessive_determiner: 'his&her', possessive_pronoun: 'his&hers', reflexive: 'himself&herself', }, [false, false], [false, false], [], '', false, )); }); test('three interchangeable pronouns', () => { const actual = buildPronoun(pronouns, 'he&she&they', config, translator); expect(actual).toEqual(new Pronoun( config, 'he&she&they', ['Normative “he/him”', 'Normative “she/her”', 'Singular “they”'], true, { pronoun_subject: 'he&she&they', pronoun_object: 'him&her&them', possessive_determiner: 'his&her&their', possessive_pronoun: 'his&hers&theirs', reflexive: 'himself&herself&themselves', }, [false, false, true], [false, false, true], [], '', false, )); }); }); describe('building the short of a pronoun usage', () => { const pronounGroups = [new PronounGroup('Binary forms', ['he', 'she'], null, 'normative')]; const pronounLibrary = new PronounLibrary(config, pronounGroups, pronouns); test('of a canonical pronoun', () => { const actual = buildPronounUsage(pronounLibrary, 'she', config, translator); expect(actual).toEqual({ short: { options: ['she/her'], glue: ' or ' }, pronoun: pronouns.she }); }); test('of a generated pronoun in slash format', () => { config.pronouns.honorifics = true; config.pronouns.generator!.slashes = true; const actual = buildPronounUsage(pronounLibrary, 'ae/aer/aer/aers/aerself', config, translator); expect(actual).toEqual({ short: { options: ['ae/aer'], glue: ' or ' }, pronoun: generatedPronouns.aer(config) }); }); test('of interchangeable', () => { const actual = buildPronounUsage(pronounLibrary, 'he&they', config, translator); expect(actual).toMatchObject({ short: { options: ['he/him', 'they/them'], glue: ' or ' } }); }); test('of null pronoun', () => { config.pronouns.null = nullPronounsConfig; const actual = buildPronounUsage(pronounLibrary, ':A', config, translator); expect(actual).toMatchObject({ short: { options: ['A/A\'s'], glue: ' or ' } }); }); describe('of null route', () => { beforeEach(() => { config.pronouns.null = nullPronounsConfig; }); test('without specific translation', () => { const actual = buildPronounUsage(pronounLibrary, 'avoiding', config, translator); expect(actual).toEqual({ short: { options: ['avoiding'] } }); }); test('with specific translation', () => { translations.pronouns.null = { short: { 'no-pronouns': 'no pronouns', }, }; const actual = buildPronounUsage(pronounLibrary, 'no-pronouns', config, translator); expect(actual).toEqual({ short: { options: ['no pronouns'] } }); }); test('is case insensitive', () => { const actual = buildPronounUsage(pronounLibrary, 'Avoiding', config, translator); expect(actual).toEqual({ short: { options: ['avoiding'] } }); }); }); test('of emojiself', () => { config.pronouns.emoji = emojiPronounsConfig; const actual = buildPronounUsage(pronounLibrary, '💫', config, translator); expect(actual).toMatchObject({ short: { options: ['💫/💫\'s'] } }); }); describe('of mirror pronouns', () => { beforeEach(() => { config.pronouns.mirror = { route: 'mirror', name: 'Mirror pronouns / Mirrorpronominal', description: 'A person who uses mirror pronouns wants to be referred to with the same pronouns as the person talking.', example: [ 'Person A uses mirror pronouns.', 'Person B uses {/she=she/her}, so when she talks about person A, she uses “she/her” to refer to her.', 'Person C uses {/ze=ze/hir} interchangeably with {/fae=fæ/fær}, so when ze talks about person A, fæ uses either ze/hir or fæ/fær to refer to fær.', ], }; }); test('without specific translation', () => { const actual = buildPronounUsage(pronounLibrary, 'mirror', config, translator); expect(actual).toEqual({ short: { options: ['mirror'] } }); }); test('with specific translation', () => { translations.pronouns.mirror = { short: 'mirror pronouns', }; const actual = buildPronounUsage(pronounLibrary, 'mirror', config, translator); expect(actual).toEqual({ short: { options: ['mirror pronouns'] } }); }); test('is case insensitive', () => { const actual = buildPronounUsage(pronounLibrary, 'Mirror', config, translator); expect(actual).toEqual({ short: { options: ['mirror'] } }); }); }); describe('of any route', () => { test('using route name', () => { const actual = buildPronounUsage(pronounLibrary, 'any', config, translator); expect(actual).toEqual({ short: { options: ['any'] } }); }); test('is case insensitive', () => { const actual = buildPronounUsage(pronounLibrary, 'Any', config, translator); expect(actual).toEqual({ short: { options: ['any'] } }); }); }); describe('of any group', () => { test('without specific translation', () => { const actual = buildPronounUsage(pronounLibrary, 'any:normative', config, translator); expect(actual).toEqual({ short: { options: ['any normative'] } }); }); test('with specific translation', () => { translations.pronouns.any.group = { normative: { short: 'both binaries', }, }; const actual = buildPronounUsage(pronounLibrary, 'any:normative', config, translator); expect(actual).toEqual({ short: { options: ['both binaries'] } }); }); test('is case insensitive', () => { const actual = buildPronounUsage(pronounLibrary, 'Any:Normative', config, translator); expect(actual).toEqual({ short: { options: ['any normative'] } }); }); test('of unknown yields nothing', () => { const actual = buildPronounUsage(pronounLibrary, 'any:unknown', config, translator); expect(actual).toBeNull(); }); }); });