mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-23 04:34:15 -04:00
(nouns) submit regular forms (only stems and class) and display them in the dictionary
This commit is contained in:
parent
23dcc1a454
commit
301a496f30
87
components/nouns/NounsClassSelectTable.vue
Normal file
87
components/nouns/NounsClassSelectTable.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { Noun } from '~/src/classes.ts';
|
||||
import type { NounRawWithLoadedWords } from '~/src/classes.ts';
|
||||
import { loadNounsData } from '~/src/data.ts';
|
||||
import { availableGenders, resolveWordsFromClassInstance } from '~/src/nouns.ts';
|
||||
import type { NounsData, NounClassInstance } from '~/src/nouns.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
stemValues: Record<string, string>;
|
||||
}>();
|
||||
|
||||
const modelValue = defineModel<keyof Required<NounsData>['classes'] | undefined>();
|
||||
|
||||
const config = useConfig();
|
||||
|
||||
const nounsData = await loadNounsData();
|
||||
|
||||
const nouns = computed((): Noun[] => {
|
||||
return withKey(nounsData.classes ?? {})
|
||||
.map((nounClass): NounClassInstance => ({ classKey: nounClass.key, stems: props.stemValues }))
|
||||
.map((classInstance, i): NounRawWithLoadedWords => ({
|
||||
id: `template-${i}`,
|
||||
words: resolveWordsFromClassInstance(classInstance, nounsData),
|
||||
classInstance,
|
||||
}))
|
||||
.filter((nounRaw) => Object.keys(nounRaw.words).length > 0)
|
||||
.map((nounRaw) => new Noun(config, nounRaw));
|
||||
});
|
||||
|
||||
const edit = ref(modelValue.value === undefined);
|
||||
|
||||
watch(modelValue, () => {
|
||||
edit.value = false;
|
||||
});
|
||||
|
||||
const visibleNouns = computed((): Noun[] => {
|
||||
if (edit.value || modelValue.value === undefined) {
|
||||
return nouns.value;
|
||||
}
|
||||
return nouns.value.filter((noun) => noun.classInstance!.classKey === modelValue.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button v-if="!edit" type="button" class="btn btn-outline-primary" @click="edit = true">
|
||||
<Icon v="exchange" />
|
||||
<T>nouns.submit.regular.class.change</T>
|
||||
</button>
|
||||
<Table
|
||||
ref="table"
|
||||
:data="visibleNouns"
|
||||
fixed
|
||||
:marked="noun => modelValue === noun.classInstance!.classKey"
|
||||
:class="['nouns-table mt-0', config.nouns.nonbinary ? 'nouns-table-nonbinary' : '']"
|
||||
>
|
||||
<template #header>
|
||||
<div v-for="gender in availableGenders(config)" :key="gender" class="d-none d-md-block bold">
|
||||
<NounsGenderLabel :gender="gender" />
|
||||
</div>
|
||||
</template>
|
||||
<template #row="{ el: noun }">
|
||||
<NounsTableEntry :noun @click="modelValue = noun.classInstance!.classKey">
|
||||
<template #buttons>
|
||||
<ul v-if="edit" class="list-unstyled list-btn-concise">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-concise btn-outline-primary btn-sm"
|
||||
@click="modelValue = noun.classInstance!.classKey"
|
||||
>
|
||||
<Icon v="check" />
|
||||
<span class="btn-label">
|
||||
<T>nouns.submit.regular.class.apply</T>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</NounsTableEntry>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<Icon v="search" />
|
||||
<T>nouns.empty</T>
|
||||
</template>
|
||||
</Table>
|
||||
</template>
|
@ -3,7 +3,7 @@ import type NounsSubmitForm from '~/components/nouns/NounsSubmitForm.vue';
|
||||
import { filterWordsForConvention, loadWords, Noun } from '~/src/classes.ts';
|
||||
import type { Filter } from '~/src/classes.ts';
|
||||
import { loadNounsData } from '~/src/data.ts';
|
||||
import { availableGenders, buildNounDeclensionsByFirstCase } from '~/src/nouns.ts';
|
||||
import { addWordsFromClassInstance, availableGenders, buildNounDeclensionsByFirstCase } from '~/src/nouns.ts';
|
||||
import type { NounConvention } from '~/src/nouns.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
@ -33,6 +33,13 @@ const nounsAsyncData = useAsyncData(
|
||||
const collator = Intl.Collator(config.locale);
|
||||
return Object.fromEntries((await $fetch('/api/nouns'))
|
||||
.map((nounRaw) => loadWords(nounRaw, config, declensionsByFirstCase))
|
||||
.map((nounRaw) => {
|
||||
if (!nounRaw.classInstance) {
|
||||
return nounRaw;
|
||||
}
|
||||
const words = addWordsFromClassInstance(nounRaw.words, nounRaw.classInstance, nounsData);
|
||||
return { ...nounRaw, words };
|
||||
})
|
||||
.map((nounRaw) => filterWordsForConvention(nounRaw, props.nounConvention))
|
||||
.filter((nounRaw) => nounRaw !== undefined)
|
||||
.map((nounRaw) => new Noun(config, nounRaw))
|
||||
|
55
components/nouns/NounsIrregularWordsSubform.vue
Normal file
55
components/nouns/NounsIrregularWordsSubform.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { availableGenders, availableNumeri, symbolsByNumeri } from '~/src/nouns.ts';
|
||||
import type { Gender, NounWord, Numerus } from '~/src/nouns.ts';
|
||||
|
||||
const modelValue = defineModel<Record<Gender, Record<Numerus, NounWord[]>>>({ required: true });
|
||||
|
||||
const config = useConfig();
|
||||
|
||||
const editDeclensions = ref(false);
|
||||
|
||||
watch(modelValue, () => {
|
||||
editDeclensions.value = !!config.nouns.declension?.enabled && Object.values(modelValue.value)
|
||||
.flatMap((wordsOfNumerus) => Object.values(wordsOfNumerus))
|
||||
.flatMap((words) => words)
|
||||
.some((word) => word.declension);
|
||||
});
|
||||
|
||||
const canRemoveWord = (gender: Gender, numerus: Numerus): boolean => {
|
||||
if (config.nouns.conventions?.enabled) {
|
||||
return true;
|
||||
}
|
||||
if (numerus === 'plural' && !config.nouns.pluralsRequired) {
|
||||
return true;
|
||||
}
|
||||
const wordsOfOtherGenderAndSameNumerus = availableGenders(config).filter((otherGender) => {
|
||||
return otherGender !== gender && (modelValue.value[otherGender]?.[numerus] ?? []).length > 0;
|
||||
});
|
||||
if (wordsOfOtherGenderAndSameNumerus.length > 1) {
|
||||
return true;
|
||||
}
|
||||
return (modelValue.value[gender]?.[numerus] ?? []).length > 1;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="numerus of availableNumeri(config)" :key="numerus" class="row">
|
||||
<div v-if="config.nouns.plurals" class="col-12 text-nowrap">
|
||||
<label><strong>{{ symbolsByNumeri[numerus] }} <T>nouns.{{ numerus }}</T></strong></label>
|
||||
</div>
|
||||
<div v-for="gender in availableGenders(config)" :key="gender" class="col-12 col-md-6 my-2">
|
||||
<label><strong><NounsGenderLabel :gender="gender" /></strong></label>
|
||||
<NounsWordsInput
|
||||
v-model="modelValue[gender][numerus]"
|
||||
:edit-declensions
|
||||
:minitems="canRemoveWord(gender, numerus) ? 0 : 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="config.nouns.declension?.enabled" class="form-check form-switch my-2">
|
||||
<label>
|
||||
<input v-model="editDeclensions" class="form-check-input" type="checkbox" role="switch">
|
||||
<T>nouns.declension.edit</T>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
53
components/nouns/NounsRegularWordsSubform.vue
Normal file
53
components/nouns/NounsRegularWordsSubform.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { loadNounsData } from '~/src/data.ts';
|
||||
import type { NounClassInstance, NounsData } from '~/src/nouns.ts';
|
||||
|
||||
const modelValue = defineModel<NounClassInstance | null>({ required: true });
|
||||
|
||||
const stemValues = ref<NounClassInstance['stems']>({});
|
||||
const classKey = ref<keyof Required<NounsData>['classes']>();
|
||||
|
||||
watch(modelValue, () => {
|
||||
stemValues.value = modelValue.value?.stems ?? {};
|
||||
classKey.value = modelValue.value?.classKey;
|
||||
});
|
||||
|
||||
const hasStems = computed(() => {
|
||||
return Object.values(stemValues.value).some((stem) => stem);
|
||||
});
|
||||
|
||||
watch([classKey, hasStems], () => {
|
||||
if (hasStems.value && classKey.value !== undefined) {
|
||||
modelValue.value = { classKey: classKey.value, stems: stemValues.value };
|
||||
}
|
||||
});
|
||||
|
||||
const nounsData = await loadNounsData();
|
||||
|
||||
const id = useId();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h5><T>nouns.submit.regular.stems.header</T></h5>
|
||||
<p><T>nouns.submit.regular.stems.description</T></p>
|
||||
<template v-for="stem of withKey(nounsData.stems ?? {})" :key="stem.key">
|
||||
<label :for="`${id}-stem-${stem.key}`">{{ stem.name }}</label>
|
||||
<input
|
||||
:id="`${id}-stem-${stem.key}`"
|
||||
v-model="stemValues[stem.key]"
|
||||
type="text"
|
||||
class="form-control form-control-sm"
|
||||
:class="stem.key === 'default' ? 'border-primary' : ''"
|
||||
:placeholder="stem.example"
|
||||
>
|
||||
</template>
|
||||
<template v-if="hasStems">
|
||||
<h5 class="mt-2 mb-0">
|
||||
<T>nouns.submit.regular.class.header</T>
|
||||
</h5>
|
||||
<p>
|
||||
<T>nouns.submit.regular.class.description</T>
|
||||
</p>
|
||||
<NounsClassSelectTable v-model="classKey" :stem-values />
|
||||
</template>
|
||||
</template>
|
@ -3,11 +3,12 @@ import type { Config } from '~/locale/config.ts';
|
||||
import type { Noun, NounRaw } from '~/src/classes.ts';
|
||||
import { loadNounAbbreviations } from '~/src/data.ts';
|
||||
import { fromUnionEntries } from '~/src/helpers.ts';
|
||||
import { availableGenders, availableNumeri, genders, symbolsByNumeri } from '~/src/nouns.ts';
|
||||
import type { Numerus, Gender, NounWord, NounWords } from '~/src/nouns.ts';
|
||||
import { filterIrregularWords, genders } from '~/src/nouns.ts';
|
||||
import type { NounClassInstance, Numerus, Gender, NounWord, NounWords } from '~/src/nouns.ts';
|
||||
|
||||
interface NounFormValue extends Omit<NounRaw, 'id' | 'words'> {
|
||||
words: Record<Gender, Record<Numerus, NounWord[]>>;
|
||||
classInstance: NounClassInstance | null;
|
||||
categories: NonNullable<NounRaw['categories']>;
|
||||
}
|
||||
|
||||
@ -15,10 +16,11 @@ const emptyForm = (config: Config): NounFormValue => {
|
||||
return {
|
||||
words: fromUnionEntries(genders.map((gender) => {
|
||||
return [gender, {
|
||||
singular: [{ spelling: '' }],
|
||||
singular: !config.nouns.conventions?.enabled ? [{ spelling: '' }] : [],
|
||||
plural: config.nouns.pluralsRequired ? [{ spelling: '' }] : [],
|
||||
}];
|
||||
})),
|
||||
classInstance: null,
|
||||
categories: [],
|
||||
sources: [],
|
||||
base: null,
|
||||
@ -46,8 +48,6 @@ const templateFilterInput = useTemplateRef<HTMLInputElement>('templateFilterInpu
|
||||
|
||||
const form = ref(emptyForm(config));
|
||||
|
||||
const editDeclensions = ref(false);
|
||||
|
||||
const submitting = ref(false);
|
||||
const afterSubmit = ref(false);
|
||||
|
||||
@ -55,19 +55,6 @@ const templateBase = ref('');
|
||||
const templateFilter = ref('');
|
||||
const templateVisible = ref(false);
|
||||
|
||||
const canRemoveWord = (gender: Gender, numerus: Numerus): boolean => {
|
||||
if (numerus === 'plural' && !config.nouns.pluralsRequired) {
|
||||
return true;
|
||||
}
|
||||
const wordsOfOtherGenderAndSameNumerus = availableGenders(config).filter((otherGender) => {
|
||||
return otherGender !== gender && (form.value.words[otherGender]?.[numerus] ?? []).length > 0;
|
||||
});
|
||||
if (wordsOfOtherGenderAndSameNumerus.length > 1) {
|
||||
return true;
|
||||
}
|
||||
return (form.value.words[gender]?.[numerus] ?? []).length > 1;
|
||||
};
|
||||
|
||||
const dialogue = useDialogue();
|
||||
const applyTemplate = async (template: Noun): Promise<void> => {
|
||||
if (JSON.stringify(form.value) !== JSON.stringify(emptyForm(config))) {
|
||||
@ -75,6 +62,7 @@ const applyTemplate = async (template: Noun): Promise<void> => {
|
||||
}
|
||||
form.value = {
|
||||
words: fillMissingVariants(template.words),
|
||||
classInstance: null,
|
||||
categories: template.categories ?? [],
|
||||
sources: template.sources,
|
||||
base: template.id,
|
||||
@ -100,15 +88,12 @@ const submit = async () => {
|
||||
};
|
||||
const edit = (noun: Noun): void => {
|
||||
form.value = {
|
||||
words: fillMissingVariants(noun.words),
|
||||
words: fillMissingVariants(filterIrregularWords(noun.words)),
|
||||
classInstance: noun.classInstance,
|
||||
categories: noun.categories,
|
||||
sources: noun.sources,
|
||||
base: noun.id,
|
||||
};
|
||||
editDeclensions.value = !!config.nouns.declension?.enabled && Object.values(noun.words)
|
||||
.flatMap((wordsOfNumerus) => Object.values(wordsOfNumerus))
|
||||
.flatMap((words) => words)
|
||||
.some((word) => word.declension);
|
||||
focus();
|
||||
};
|
||||
const focus = (editable = true): void => {
|
||||
@ -138,25 +123,23 @@ const { data: sourcesKeys } = await useFetch('/api/sources/keys', { lazy: true,
|
||||
</p>
|
||||
</div>
|
||||
<form v-else @submit.prevent="submit">
|
||||
<div v-for="numerus of availableNumeri(config)" :key="numerus" class="row">
|
||||
<div v-if="config.nouns.plurals" class="col-12 text-nowrap mt-md-4">
|
||||
<label><strong>{{ symbolsByNumeri[numerus] }} <T>nouns.{{ numerus }}</T></strong></label>
|
||||
<template v-if="config.nouns.conventions?.enabled">
|
||||
<h5 class="border mb-0 p-3 bg-light">
|
||||
<Icon v="bars" />
|
||||
<T>nouns.submit.regular.header</T>
|
||||
</h5>
|
||||
<div class="p-3 border border-top-0 mb-3">
|
||||
<NounsRegularWordsSubform v-model="form.classInstance" class="mb-2" />
|
||||
</div>
|
||||
<div v-for="gender in availableGenders(config)" :key="gender" class="col-12 col-md-6 mt-2">
|
||||
<label><strong><NounsGenderLabel :gender="gender" /></strong></label>
|
||||
<NounsWordsInput
|
||||
v-model="form.words[gender][numerus]"
|
||||
:edit-declensions
|
||||
:minitems="canRemoveWord(gender, numerus) ? 0 : 1"
|
||||
/>
|
||||
<h5 class="border mb-0 p-3 bg-light">
|
||||
<Icon v="stream" />
|
||||
<T>nouns.submit.irregular.header</T>
|
||||
</h5>
|
||||
<div class="p-3 border border-top-0 mb-3">
|
||||
<NounsIrregularWordsSubform v-model="form.words" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="config.nouns.declension?.enabled" class="form-check form-switch mb-2">
|
||||
<label>
|
||||
<input v-model="editDeclensions" class="form-check-input" type="checkbox" role="switch">
|
||||
<T>nouns.declension.edit</T>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<NounsIrregularWordsSubform v-else v-model="form.words" />
|
||||
<CategoriesSelector
|
||||
v-model="form.categories"
|
||||
:label="$t('nouns.categories')"
|
||||
|
@ -21,7 +21,7 @@ const visibleNumeri = computed(() => availableNumeri(config));
|
||||
|
||||
<template>
|
||||
<template v-for="gender in visibleGenders" :key="gender">
|
||||
<div class="d-md-none bold" :style="{ gridArea: `${gender}Label` }">
|
||||
<div class="d-md-none bold" :style="{ gridArea: `${gender}Label` }" v-bind="$attrs">
|
||||
<NounsGenderLabel :gender="gender" concise />
|
||||
</div>
|
||||
<div
|
||||
@ -29,6 +29,7 @@ const visibleNumeri = computed(() => availableNumeri(config));
|
||||
:key="numerus"
|
||||
:style="{ gridArea: `${gender}${numerus === 'plural' ? 'Pl' : ''}` }"
|
||||
role="cell"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<NounsItem :noun :gender :numerus />
|
||||
|
||||
@ -41,7 +42,7 @@ const visibleNumeri = computed(() => availableNumeri(config));
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="noun.sourcesData?.length" style="grid-area: sources">
|
||||
<div v-if="noun.sourcesData?.length" style="grid-area: sources" v-bind="$attrs">
|
||||
<p><strong><T>sources.referenced</T><T>quotation.colon</T></strong></p>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li v-for="source in noun.sourcesData" :key="source.id">
|
||||
@ -49,7 +50,7 @@ const visibleNumeri = computed(() => availableNumeri(config));
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style="grid-area: buttons">
|
||||
<div style="grid-area: buttons" v-bind="$attrs">
|
||||
<slot name="buttons"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -204,6 +204,22 @@ nouns:
|
||||
submit:
|
||||
action: 'Submit'
|
||||
actionLong: 'Submit a word'
|
||||
regular:
|
||||
header: 'Regular forms'
|
||||
stems:
|
||||
header: 'Stems'
|
||||
description: >
|
||||
The common parts of word spellings are referred to as stems.
|
||||
Most noun conventions can then be derived from a stem.
|
||||
class:
|
||||
header: 'Class'
|
||||
description: >
|
||||
The formation rules from a stem to a word in a noun convention is called a class here.
|
||||
Select the appropriate class that derive the correct words.
|
||||
apply: 'Select class'
|
||||
change: 'Change class'
|
||||
irregular:
|
||||
header: 'Irregular forms'
|
||||
thanks: 'Thank you for contributing!'
|
||||
another: 'Submit another one'
|
||||
moderation: 'Submissions will have to get approved before getting published.'
|
||||
|
@ -241,6 +241,16 @@ declensions:
|
||||
g: ['ne']
|
||||
d: ['ne']
|
||||
a: ['nen']
|
||||
stems:
|
||||
default:
|
||||
name: 'Hauptstamm'
|
||||
example: 'Stamm vor der geschlechtsspezifischen Endung (Arbeit bei Arbeiter*in)'
|
||||
flucht:
|
||||
name: 'Fluchtsubstantiv'
|
||||
example: 'Substantiv, welches die Tätigkeit beschreibt, ggf. inkl. Genitivpartikel (Expertise bei Expert*in)'
|
||||
partizip:
|
||||
name: 'Partizip'
|
||||
example: 'Partizip ohne Endung –ende (Studier bei Studierende)'
|
||||
classes:
|
||||
t1:
|
||||
exampleStems:
|
||||
@ -316,6 +326,7 @@ conventions:
|
||||
maskulinum:
|
||||
name: 'Maskulinum'
|
||||
normative: true
|
||||
gender: 'masc'
|
||||
morphemes:
|
||||
article_n: 'der'
|
||||
article_g: 'des'
|
||||
@ -353,6 +364,7 @@ conventions:
|
||||
femininum:
|
||||
name: 'Femininum'
|
||||
normative: true
|
||||
gender: 'fem'
|
||||
morphemes:
|
||||
article_n: 'die'
|
||||
article_g: 'der'
|
||||
@ -390,6 +402,7 @@ conventions:
|
||||
partizip-formen:
|
||||
name: 'Partizip-Formen'
|
||||
normative: true
|
||||
gender: 'neutr'
|
||||
description:
|
||||
- >
|
||||
Aus dem Partizip I (infinite Verbform) lassen sich substantivierte Adjektive bilden.
|
||||
@ -417,6 +430,7 @@ conventions:
|
||||
person-formen:
|
||||
name: 'Person-Formen'
|
||||
normative: true
|
||||
gender: 'neutr'
|
||||
morphemes:
|
||||
article_n: 'die'
|
||||
article_g: 'der'
|
||||
@ -458,6 +472,7 @@ conventions:
|
||||
mensch-formen:
|
||||
name: 'Mensch-Formen'
|
||||
normative: true
|
||||
gender: 'neutr'
|
||||
morphemes:
|
||||
article_n: 'der'
|
||||
article_g: 'des'
|
||||
@ -499,6 +514,7 @@ conventions:
|
||||
diminuitiv:
|
||||
name: 'Diminuitiv'
|
||||
normative: true
|
||||
gender: 'neutr'
|
||||
warning: >
|
||||
Die Verniedlichungsform benutzt zwar das Neutrum und ist damit eine normativ neutrale Form,
|
||||
jedoch zeichnet diese Form üblicherweise kleine und junge Nomen aus.
|
||||
@ -541,6 +557,7 @@ conventions:
|
||||
y-formen:
|
||||
name: 'Y-Formen'
|
||||
normative: false
|
||||
gender: 'neutr'
|
||||
description:
|
||||
- >
|
||||
Bekannt als {https://www.bpb.de/shop/zeitschriften/apuz/geschlechtergerechte-sprache-2022/346085/entgendern-nach-phettberg/=Entgendern nach Phettberg}.
|
||||
@ -582,6 +599,7 @@ conventions:
|
||||
i-formen:
|
||||
name: 'I-Formen'
|
||||
normative: false
|
||||
gender: 'neutr'
|
||||
description:
|
||||
- >
|
||||
Verwendet u.a. in den Romanen
|
||||
@ -629,6 +647,7 @@ conventions:
|
||||
inklusivum:
|
||||
name: 'Inklusivum'
|
||||
normative: false
|
||||
gender: 'neutr'
|
||||
description:
|
||||
- 'Formen vorgestellt vom {https://geschlechtsneutral.net/=Verein für geschlechtsneutrales Deutsch}.'
|
||||
- 'Siehe auch: {/en/em=Neopronomen „en“}'
|
||||
@ -669,6 +688,7 @@ conventions:
|
||||
indefinitivum:
|
||||
name: 'Indefinitivum'
|
||||
normative: false
|
||||
gender: 'neutr'
|
||||
description:
|
||||
- 'Formen vorgestellt von {https://www.geschlechtsneutral.com/lit/Liminalis-2008-Sylvain-Balzer.pdf=Cabala de Sylvain und Carsten Balzer}'
|
||||
- 'Siehe auch: {/nin=Neopronomen „nin/nim“}.'
|
||||
@ -705,6 +725,7 @@ conventions:
|
||||
ens-formen:
|
||||
name: 'ens-Formen'
|
||||
normative: false
|
||||
gender: 'neutr'
|
||||
description:
|
||||
- 'Formen vorgestellt von Lann Hornscheidt.'
|
||||
- 'Siehe auch: {/ens=Neopronomen „ens“}.'
|
||||
@ -734,6 +755,7 @@ conventions:
|
||||
ex-formen:
|
||||
name: 'ex-Formen'
|
||||
normative: false
|
||||
gender: 'neutr'
|
||||
description:
|
||||
- 'Formen vorgestellt von {https://www.lannhornscheidt.com/w_ortungen/nonbinare-w_ortungen/=Lann Hornscheidt und Lio Oppenländer}.'
|
||||
- 'Siehe auch: {/ex=Neopronomen „ex“}.'
|
||||
@ -757,6 +779,7 @@ conventions:
|
||||
ojum:
|
||||
name: 'Ojum'
|
||||
normative: false
|
||||
gender: 'neutr'
|
||||
description:
|
||||
- 'Formen vorgestellt von {https://www.frumble.de/blog/2021/03/26/ueberlegungen-zu-einer-genderneutralen-deutschen-grammatik=Frumble}.'
|
||||
- 'Siehe auch: {/oj=Neopronomen „oj/ojm“}.'
|
||||
@ -788,6 +811,7 @@ conventions:
|
||||
nona-system:
|
||||
name: 'NoNa-System'
|
||||
normative: false
|
||||
gender: 'neutr'
|
||||
description:
|
||||
- 'Formen vorgestellt von {https://geschlechtsneutralesdeutsch.com/=Geschlechtsneutrales Deutsch}.'
|
||||
morphemes:
|
||||
@ -823,6 +847,7 @@ conventions:
|
||||
genderdoppelpunkt:
|
||||
name: 'Genderdoppelpunkt'
|
||||
normative: false
|
||||
gender: 'neutr'
|
||||
morphemes:
|
||||
article_n: 'der:die'
|
||||
article_g: 'des:der'
|
||||
@ -860,6 +885,7 @@ conventions:
|
||||
gendergap:
|
||||
name: 'Gendergap'
|
||||
normative: false
|
||||
gender: 'neutr'
|
||||
morphemes:
|
||||
article_n: 'der_die'
|
||||
article_g: 'des_der'
|
||||
@ -897,6 +923,7 @@ conventions:
|
||||
gendersternchen:
|
||||
name: 'Gendersternchen'
|
||||
normative: false
|
||||
gender: 'neutr'
|
||||
morphemes:
|
||||
article_n: 'der*die'
|
||||
article_g: 'des*der'
|
||||
@ -934,6 +961,7 @@ conventions:
|
||||
binnen-i:
|
||||
name: 'Binnen-I'
|
||||
normative: false
|
||||
gender: 'neutr'
|
||||
warning: >
|
||||
Das Binnen-I bezieht sich nur auf die männliche und die weibliche Form der Wörter
|
||||
und schließt damit (wie das generische Maskulinum) immer noch sehr viele Menschen aus der Sprache aus.
|
||||
|
@ -224,10 +224,28 @@ nouns:
|
||||
submit:
|
||||
action: 'Einreichen'
|
||||
actionLong: 'Ein Wort einreichen'
|
||||
regular:
|
||||
header: 'Reguläre Formen'
|
||||
stems:
|
||||
header: 'Stämme'
|
||||
description: >
|
||||
Die gemeinsamen Wortteile werden als Stämme bezeichnet.
|
||||
Die meisten Substantivkonventionen können dann von einem Stamm abgeleitet werden.
|
||||
Trage alle vorhandenen Stämme an, es kann allerdings sein,
|
||||
dass einige Wörter keine Fluchtsubstantive oder Partizipformen haben.
|
||||
class:
|
||||
header: 'Klasse'
|
||||
description: >
|
||||
Die Bildungsregeln von einem Stamm zu einem Wort in einer Substantivkonvention
|
||||
wird hier Klasse genannt.
|
||||
Wähle die passende Klasse an, die die richtigen Wörter bildet.
|
||||
apply: 'Klasse wählen'
|
||||
change: 'Klasse wechseln'
|
||||
irregular:
|
||||
header: 'Irreguläre Formen'
|
||||
thanks: 'Danke für deinen Beitrag!'
|
||||
another: 'Einen weiteren Eintrag einreichen'
|
||||
moderation: 'Einreichungen müssen erst genehmigt werden, bevor sie veröffentlicht werden.'
|
||||
|
||||
template:
|
||||
header: 'Eine Vorlage nutzen'
|
||||
root: 'Wurzel'
|
||||
|
9
migrations/091-nouns-stems.sql
Normal file
9
migrations/091-nouns-stems.sql
Normal file
@ -0,0 +1,9 @@
|
||||
-- Up
|
||||
|
||||
ALTER TABLE nouns
|
||||
ADD COLUMN classInstance TEXT NULL;
|
||||
|
||||
-- Down
|
||||
|
||||
ALTER TABLE nouns
|
||||
DROP COLUMN classInstance;
|
@ -1,9 +1,9 @@
|
||||
import { createCanvas, loadImage, registerFont } from 'canvas';
|
||||
import SQL from 'sql-template-strings';
|
||||
|
||||
import { getLocale, loadConfig, loadTranslator } from '~/server/data.ts';
|
||||
import { getLocale, loadConfig, loadNounsData, loadTranslator } from '~/server/data.ts';
|
||||
import { registerLocaleFont } from '~/server/localeFont.ts';
|
||||
import { parseNounRow } from '~/server/nouns.ts';
|
||||
import { buildNoun, displayWord, parseNounRow } from '~/server/nouns.ts';
|
||||
import type { NounRow } from '~/server/nouns.ts';
|
||||
import {
|
||||
availableGenders,
|
||||
@ -16,7 +16,8 @@ import {
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const locale = getLocale(event);
|
||||
const [config, translator] = await Promise.all([loadConfig(locale), loadTranslator(locale)]);
|
||||
const [config, translator, nounsData] =
|
||||
await Promise.all([loadConfig(locale), loadTranslator(locale), loadNounsData(locale)]);
|
||||
checkIsConfigEnabledOr404(await loadConfig(locale), 'nouns');
|
||||
|
||||
const { isGranted } = await useAuthentication(event);
|
||||
@ -45,7 +46,7 @@ export default defineEventHandler(async (event) => {
|
||||
});
|
||||
}
|
||||
|
||||
const noun = parseNounRow(nounRow);
|
||||
const noun = buildNoun(parseNounRow(nounRow), config, nounsData);
|
||||
|
||||
const genders = availableGenders(config);
|
||||
|
||||
@ -99,7 +100,7 @@ export default defineEventHandler(async (event) => {
|
||||
const symbol = symbolsByNumeri[numerus];
|
||||
noun.words[gender]?.[numerus]?.forEach((word) => {
|
||||
context.fillText(
|
||||
`${symbol} ${word}`,
|
||||
`${symbol} ${displayWord(word, numerus, nounsData)}`,
|
||||
column * (width - 2 * padding) / genders.length + padding,
|
||||
padding * 2.5 + i * 48,
|
||||
);
|
||||
|
@ -5,7 +5,7 @@ import { auditLog } from '~/server/audit.ts';
|
||||
import { getLocale, loadConfig } from '~/server/data.ts';
|
||||
import { approveNounEntry } from '~/server/nouns.ts';
|
||||
import { isAllowedToPost } from '~/server/user.ts';
|
||||
import type { NounWord, NounWords, NounWordsRaw } from '~/src/nouns.ts';
|
||||
import type { NounClassInstance, NounWord, NounWords, NounWordsRaw } from '~/src/nouns.ts';
|
||||
|
||||
const minimizeWords = (words: NounWords): NounWordsRaw => {
|
||||
return Object.fromEntries(Object.entries(words)
|
||||
@ -26,7 +26,11 @@ const minimizeWords = (words: NounWords): NounWordsRaw => {
|
||||
}));
|
||||
};
|
||||
|
||||
const generateKey = (words: NounWords): string => {
|
||||
const generateKey = (words: NounWords, classInstance: NounClassInstance | null): string => {
|
||||
const defaultStem = classInstance?.stems.default;
|
||||
if (defaultStem) {
|
||||
return defaultStem.toLowerCase();
|
||||
}
|
||||
const word = Object.values(words)
|
||||
.map((wordsByNumerus) => wordsByNumerus.singular?.[0])
|
||||
.find((word) => word !== undefined);
|
||||
@ -52,12 +56,13 @@ export default defineEventHandler(async (event) => {
|
||||
const id = ulid();
|
||||
await db.get(SQL`
|
||||
INSERT INTO nouns (
|
||||
id, key, words, categories, sources, approved, base_id, locale, author_id
|
||||
id, key, words, classInstance, categories, sources, approved, base_id, locale, author_id
|
||||
)
|
||||
VALUES (
|
||||
${id},
|
||||
${generateKey(body.words)},
|
||||
${generateKey(body.words, body.classInstance)},
|
||||
${JSON.stringify(minimizeWords(body.words))},
|
||||
${body.classInstance ? JSON.stringify(body.classInstance) : null},
|
||||
${body.categories.join('|')},
|
||||
${body.sources ? body.sources.join(',') : null},
|
||||
0, ${body.base}, ${locale}, ${user.id}
|
||||
|
@ -9,18 +9,25 @@ import type { RuntimeConfig } from 'nuxt/schema';
|
||||
import type { Config } from '~/locale/config.ts';
|
||||
import localeDescriptions from '~/locale/locales.ts';
|
||||
import { getPosts } from '~/server/blog.ts';
|
||||
import { getLocale, loadCalendar, loadConfig, loadPronounLibrary, loadTranslator } from '~/server/data.ts';
|
||||
import {
|
||||
getLocale,
|
||||
loadCalendar,
|
||||
loadConfig,
|
||||
loadNounsData,
|
||||
loadPronounLibrary,
|
||||
loadTranslator,
|
||||
} from '~/server/data.ts';
|
||||
import { getInclusiveEntries } from '~/server/inclusive.ts';
|
||||
import { getNounEntries } from '~/server/nouns.ts';
|
||||
import { buildNoun, displayWord, getNounEntries } from '~/server/nouns.ts';
|
||||
import { rootDir } from '~/server/paths.ts';
|
||||
import { getSourcesEntries } from '~/server/sources.ts';
|
||||
import { getTermsEntries } from '~/server/terms.ts';
|
||||
import { shortForVariant } from '~/src/buildPronoun.ts';
|
||||
import { Day } from '~/src/calendar/helpers.ts';
|
||||
import { loadWords, Noun } from '~/src/classes.ts';
|
||||
import { getUrlForLocale } from '~/src/domain.ts';
|
||||
import forbidden from '~/src/forbidden.ts';
|
||||
import { clearLinkedText, buildImageUrl } from '~/src/helpers.ts';
|
||||
import type { Numerus } from '~/src/nouns.ts';
|
||||
import parseMarkdown from '~/src/parseMarkdown.ts';
|
||||
import { normaliseQuery, validateQuery } from '~/src/search.ts';
|
||||
import type { SearchDocument } from '~/src/search.ts';
|
||||
@ -613,11 +620,13 @@ const kinds: SearchKind[] = [
|
||||
return [];
|
||||
}
|
||||
|
||||
const nounsData = await loadNounsData(config.locale);
|
||||
|
||||
const base = encodeURIComponent(config.nouns.route);
|
||||
|
||||
const db = useDatabase();
|
||||
const nouns = (await getNounEntries(db, () => false, config.locale))
|
||||
.map((nounRaw) => new Noun(config, loadWords(nounRaw, config, { singular: {}, plural: {} })));
|
||||
.map((nounRaw) => buildNoun(nounRaw, config, nounsData));
|
||||
return nouns.map((noun, id): SearchDocument => {
|
||||
const firstWords = noun.firstWords;
|
||||
return {
|
||||
@ -626,8 +635,13 @@ const kinds: SearchKind[] = [
|
||||
url: `/${base}?filter=${firstWords[0]}`,
|
||||
title: firstWords.join(' – '),
|
||||
content: Object.values(noun.words)
|
||||
.flatMap((wordsByNumerus) => Object.values(wordsByNumerus))
|
||||
.flatMap((words) => words)
|
||||
.map((wordsByNumerus) => {
|
||||
return Object.entries(wordsByNumerus)
|
||||
.flatMap(([numerus, words]) => {
|
||||
return words.map((word) => displayWord(word, numerus as Numerus, nounsData));
|
||||
})
|
||||
.join(', ');
|
||||
})
|
||||
.join(' – '),
|
||||
};
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ import type { Calendar } from '~/src/calendar/helpers.ts';
|
||||
import { PronounLibrary } from '~/src/classes.ts';
|
||||
import type { Pronoun, PronounGroup } from '~/src/classes.ts';
|
||||
import { getLocaleForUrl, getUrlForLocale } from '~/src/domain.ts';
|
||||
import type { NounsData } from '~/src/nouns.ts';
|
||||
import { Translator } from '~/src/translator.ts';
|
||||
|
||||
const setDefault = async <K, V>(map: Map<K, V>, key: K, supplier: () => Promise<V>): Promise<V> => {
|
||||
@ -80,6 +81,13 @@ export const loadPronounExamples = async (locale: string): Promise<PronounExampl
|
||||
});
|
||||
};
|
||||
|
||||
const nounsDataByLocale: Map<string, NounsData> = new Map();
|
||||
export const loadNounsData = async (locale: string): Promise<NounsData> => {
|
||||
return setDefault(nounsDataByLocale, locale, async () => {
|
||||
return loadSuml<NounsData>(`locale/${locale}/nouns/nounsData.suml`);
|
||||
});
|
||||
};
|
||||
|
||||
const calendarByLocale: Map<string, Calendar> = new Map();
|
||||
export const loadCalendar = async (locale: string): Promise<Calendar> => {
|
||||
return setDefault(calendarByLocale, locale, async () => {
|
||||
|
@ -1,18 +1,23 @@
|
||||
/* eslint-disable camelcase */
|
||||
import SQL from 'sql-template-strings';
|
||||
|
||||
import type { Config } from '~/locale/config.ts';
|
||||
import type { Database } from '~/server/db.ts';
|
||||
import type { UserRow } from '~/server/express/user.ts';
|
||||
import type { SourceRow } from '~/server/sources.ts';
|
||||
import type { IsGrantedFn } from '~/server/utils/useAuthentication.ts';
|
||||
import { loadWords, Noun } from '~/src/classes.ts';
|
||||
import type { NounRaw } from '~/src/classes.ts';
|
||||
import { clearKey } from '~/src/helpers.ts';
|
||||
import { addWordsFromClassInstance } from '~/src/nouns.ts';
|
||||
import type { Numerus, NounWord, NounsData } from '~/src/nouns.ts';
|
||||
import type { User } from '~/src/user.ts';
|
||||
|
||||
export interface NounRow {
|
||||
id: string;
|
||||
key: string;
|
||||
words: string;
|
||||
classInstance: string | null;
|
||||
approved: number;
|
||||
base_id: string | null;
|
||||
locale: string;
|
||||
@ -28,11 +33,22 @@ export const parseNounRow = (nounRow: NounRow): Omit<NounRaw, 'sourcesData'> =>
|
||||
return {
|
||||
id: nounRow.id,
|
||||
words: JSON.parse(nounRow.words),
|
||||
classInstance: nounRow.classInstance ? JSON.parse(nounRow.classInstance) : null,
|
||||
categories: nounRow.categories?.split('|') ?? [],
|
||||
sources: nounRow.sources ? nounRow.sources.split(',') : [],
|
||||
};
|
||||
};
|
||||
|
||||
export const buildNoun = (nounRaw: Omit<NounRaw, 'sourcesData'>, config: Config, nounsData: NounsData): Noun => {
|
||||
const nounRawWithLoadedWords = loadWords(nounRaw, config, { singular: {}, plural: {} });
|
||||
if (!nounRawWithLoadedWords.classInstance) {
|
||||
return new Noun(config, nounRawWithLoadedWords);
|
||||
}
|
||||
const words =
|
||||
addWordsFromClassInstance(nounRawWithLoadedWords.words, nounRawWithLoadedWords.classInstance, nounsData);
|
||||
return new Noun(config, { ...nounRaw, words });
|
||||
};
|
||||
|
||||
const parseNounRowWithAuthor = (nounRow: NounRowWithAuthor, isGranted: IsGrantedFn): Omit<NounRaw, 'sourcesData'> => {
|
||||
const noun = parseNounRow(nounRow);
|
||||
if (isGranted('nouns')) {
|
||||
@ -43,6 +59,17 @@ const parseNounRowWithAuthor = (nounRow: NounRowWithAuthor, isGranted: IsGranted
|
||||
return noun;
|
||||
};
|
||||
|
||||
export const displayWord = (word: NounWord, numerus: Numerus, nounsData: NounsData): string => {
|
||||
if (!nounsData.declensions || !nounsData.cases) {
|
||||
return word.spelling;
|
||||
}
|
||||
|
||||
const declension = typeof word.declension === 'string' ? nounsData.declensions[word.declension] : word.declension;
|
||||
const firstCaseAbbreviation = Object.keys(nounsData.cases)[0];
|
||||
const endings = declension?.[numerus]?.[firstCaseAbbreviation];
|
||||
return `${word.spelling}${endings?.[0] ?? ''}`;
|
||||
};
|
||||
|
||||
export const addVersions = async (
|
||||
db: Database,
|
||||
isGranted: IsGrantedFn,
|
||||
|
@ -11,6 +11,7 @@ import type {
|
||||
NounWordsRaw,
|
||||
NounWord,
|
||||
NounDeclensionsByFirstCase,
|
||||
NounClassInstance,
|
||||
NounConvention,
|
||||
Gender,
|
||||
Numerus,
|
||||
@ -844,6 +845,7 @@ export class PronounLibrary {
|
||||
export interface NounRaw {
|
||||
id: string;
|
||||
words: NounWordsRaw;
|
||||
classInstance?: NounClassInstance | null;
|
||||
categories?: string[];
|
||||
sources?: string[];
|
||||
sourcesData?: SourceRaw[];
|
||||
@ -874,7 +876,7 @@ const loadWord = (
|
||||
return wordRaw;
|
||||
};
|
||||
|
||||
interface NounRawWithLoadedWords extends Omit<NounRaw, 'words'> {
|
||||
export interface NounRawWithLoadedWords extends Omit<NounRaw, 'words'> {
|
||||
words: NounWords;
|
||||
}
|
||||
|
||||
@ -920,6 +922,7 @@ const hasWordOfGender = (words: NounWords, gender: Gender): boolean => {
|
||||
export class Noun implements Entry {
|
||||
id: string;
|
||||
words: NounWords;
|
||||
classInstance: NounClassInstance | null;
|
||||
categories: string[];
|
||||
sources: string[];
|
||||
sourcesData: Source[];
|
||||
@ -930,6 +933,7 @@ export class Noun implements Entry {
|
||||
constructor(config: Config, nounRaw: NounRawWithLoadedWords) {
|
||||
this.id = nounRaw.id;
|
||||
this.words = nounRaw.words;
|
||||
this.classInstance = nounRaw.classInstance ?? null;
|
||||
this.categories = nounRaw.categories ?? [];
|
||||
this.sources = nounRaw.sources ?? [];
|
||||
this.sourcesData = nounRaw.sourcesData?.filter((s) => !!s).map((s) => new Source(config, s)) ?? [];
|
||||
|
@ -163,7 +163,8 @@ export function listMissingTranslations(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.nouns.conventions?.enabled && keyMatches('nouns.conventions.')) {
|
||||
if (!config.nouns.conventions?.enabled &&
|
||||
keyMatches('nouns.conventions.', 'nouns.submit.regular', 'nouns.submit.irregular')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
88
src/nouns.ts
88
src/nouns.ts
@ -48,6 +48,7 @@ export const symbolsByNumeri: Record<Numerus, string> = {
|
||||
|
||||
export interface NounWord {
|
||||
spelling: string;
|
||||
regular?: boolean;
|
||||
convention?: keyof Required<NounsData>['conventions'];
|
||||
declension?: keyof Required<NounsData>['declensions'] | NounDeclension;
|
||||
}
|
||||
@ -65,6 +66,7 @@ export interface NounConventionGroup {
|
||||
export interface NounConvention {
|
||||
name: string;
|
||||
normative: boolean;
|
||||
gender: Gender;
|
||||
warning?: string;
|
||||
description?: string[];
|
||||
morphemes: Record<string, MorphemeValue | string>;
|
||||
@ -77,8 +79,13 @@ interface NounConventionTemplate {
|
||||
declension: string;
|
||||
}
|
||||
|
||||
interface NounStem {
|
||||
name: string;
|
||||
example: string;
|
||||
}
|
||||
|
||||
export interface NounClass {
|
||||
exampleStems: Record<string, string>;
|
||||
exampleStems: Record<keyof Required<NounsData>['stems'], string>;
|
||||
}
|
||||
|
||||
export interface NounClassExample {
|
||||
@ -125,9 +132,88 @@ export interface NounsData {
|
||||
morphemes?: string[];
|
||||
examples?: string[];
|
||||
grammarTables?: GrammarTableDefinition[];
|
||||
stems?: Record<string, NounStem>;
|
||||
classes?: Record<string, NounClass>;
|
||||
classExample?: NounClassExample;
|
||||
declensions?: Record<string, NounDeclension>;
|
||||
groups?: Record<string, NounConventionGroup>;
|
||||
conventions?: Record<string, NounConvention>;
|
||||
}
|
||||
|
||||
export interface NounClassInstance {
|
||||
classKey: keyof Required<NounsData>['classes'];
|
||||
stems: Record<keyof Required<NounsData>['stems'], string>;
|
||||
}
|
||||
|
||||
export const resolveWordsFromClassInstance = (
|
||||
nounClassInstance: NounClassInstance,
|
||||
nounsData: NounsData,
|
||||
): NounWords => {
|
||||
const words: NounWords = {};
|
||||
for (const [conventionKey, convention] of Object.entries(nounsData.conventions ?? {})) {
|
||||
if (!Object.hasOwn(convention.templates, nounClassInstance.classKey)) {
|
||||
continue;
|
||||
}
|
||||
const template = convention.templates[nounClassInstance.classKey];
|
||||
const stem = nounClassInstance.stems[template.stem ?? 'default'];
|
||||
if (!stem) {
|
||||
continue;
|
||||
}
|
||||
const word: NounWord = {
|
||||
spelling: stem + template.suffix,
|
||||
regular: true,
|
||||
convention: conventionKey,
|
||||
declension: template.declension,
|
||||
};
|
||||
|
||||
const declension = nounsData.declensions![template.declension];
|
||||
for (const numerus of numeri) {
|
||||
if (!declension[numerus]) {
|
||||
continue;
|
||||
}
|
||||
insertIntoNounWords(words, word, convention.gender, numerus);
|
||||
}
|
||||
}
|
||||
return words;
|
||||
};
|
||||
|
||||
const insertIntoNounWords = (words: NounWords, word: NounWord, gender: Gender, numerus: Numerus) => {
|
||||
if (!Object.hasOwn(words, gender)) {
|
||||
words[gender] = {};
|
||||
}
|
||||
if (!Object.hasOwn(words[gender]!, numerus)) {
|
||||
words[gender]![numerus] = [];
|
||||
}
|
||||
words[gender]![numerus]!.push(word);
|
||||
};
|
||||
|
||||
export const addWordsFromClassInstance = (
|
||||
nounWords: NounWords,
|
||||
nounClassInstance: NounClassInstance,
|
||||
nounsData: NounsData,
|
||||
): NounWords => {
|
||||
const mergedNounWords = structuredClone(nounWords);
|
||||
const wordsFromTemplate = resolveWordsFromClassInstance(nounClassInstance, nounsData);
|
||||
for (const [gender, wordsOfNumerus] of Object.entries(wordsFromTemplate)) {
|
||||
for (const [numerus, words] of Object.entries(wordsOfNumerus)) {
|
||||
for (const word of words) {
|
||||
insertIntoNounWords(mergedNounWords, word, gender as Gender, numerus as Numerus);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mergedNounWords;
|
||||
};
|
||||
|
||||
export const filterIrregularWords = (words: NounWords) => {
|
||||
const filteredNounWords: NounWords = {};
|
||||
for (const [gender, wordsOfNumerus] of Object.entries(words)) {
|
||||
for (const [numerus, words] of Object.entries(wordsOfNumerus)) {
|
||||
for (const word of words) {
|
||||
if (!word.regular) {
|
||||
insertIntoNounWords(filteredNounWords, word, gender as Gender, numerus as Numerus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredNounWords;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user