mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-24 05:05:20 -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 { filterWordsForConvention, loadWords, Noun } from '~/src/classes.ts';
|
||||||
import type { Filter } from '~/src/classes.ts';
|
import type { Filter } from '~/src/classes.ts';
|
||||||
import { loadNounsData } from '~/src/data.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';
|
import type { NounConvention } from '~/src/nouns.ts';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -33,6 +33,13 @@ const nounsAsyncData = useAsyncData(
|
|||||||
const collator = Intl.Collator(config.locale);
|
const collator = Intl.Collator(config.locale);
|
||||||
return Object.fromEntries((await $fetch('/api/nouns'))
|
return Object.fromEntries((await $fetch('/api/nouns'))
|
||||||
.map((nounRaw) => loadWords(nounRaw, config, declensionsByFirstCase))
|
.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))
|
.map((nounRaw) => filterWordsForConvention(nounRaw, props.nounConvention))
|
||||||
.filter((nounRaw) => nounRaw !== undefined)
|
.filter((nounRaw) => nounRaw !== undefined)
|
||||||
.map((nounRaw) => new Noun(config, nounRaw))
|
.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 type { Noun, NounRaw } from '~/src/classes.ts';
|
||||||
import { loadNounAbbreviations } from '~/src/data.ts';
|
import { loadNounAbbreviations } from '~/src/data.ts';
|
||||||
import { fromUnionEntries } from '~/src/helpers.ts';
|
import { fromUnionEntries } from '~/src/helpers.ts';
|
||||||
import { availableGenders, availableNumeri, genders, symbolsByNumeri } from '~/src/nouns.ts';
|
import { filterIrregularWords, genders } from '~/src/nouns.ts';
|
||||||
import type { Numerus, Gender, NounWord, NounWords } from '~/src/nouns.ts';
|
import type { NounClassInstance, Numerus, Gender, NounWord, NounWords } from '~/src/nouns.ts';
|
||||||
|
|
||||||
interface NounFormValue extends Omit<NounRaw, 'id' | 'words'> {
|
interface NounFormValue extends Omit<NounRaw, 'id' | 'words'> {
|
||||||
words: Record<Gender, Record<Numerus, NounWord[]>>;
|
words: Record<Gender, Record<Numerus, NounWord[]>>;
|
||||||
|
classInstance: NounClassInstance | null;
|
||||||
categories: NonNullable<NounRaw['categories']>;
|
categories: NonNullable<NounRaw['categories']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,10 +16,11 @@ const emptyForm = (config: Config): NounFormValue => {
|
|||||||
return {
|
return {
|
||||||
words: fromUnionEntries(genders.map((gender) => {
|
words: fromUnionEntries(genders.map((gender) => {
|
||||||
return [gender, {
|
return [gender, {
|
||||||
singular: [{ spelling: '' }],
|
singular: !config.nouns.conventions?.enabled ? [{ spelling: '' }] : [],
|
||||||
plural: config.nouns.pluralsRequired ? [{ spelling: '' }] : [],
|
plural: config.nouns.pluralsRequired ? [{ spelling: '' }] : [],
|
||||||
}];
|
}];
|
||||||
})),
|
})),
|
||||||
|
classInstance: null,
|
||||||
categories: [],
|
categories: [],
|
||||||
sources: [],
|
sources: [],
|
||||||
base: null,
|
base: null,
|
||||||
@ -46,8 +48,6 @@ const templateFilterInput = useTemplateRef<HTMLInputElement>('templateFilterInpu
|
|||||||
|
|
||||||
const form = ref(emptyForm(config));
|
const form = ref(emptyForm(config));
|
||||||
|
|
||||||
const editDeclensions = ref(false);
|
|
||||||
|
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const afterSubmit = ref(false);
|
const afterSubmit = ref(false);
|
||||||
|
|
||||||
@ -55,19 +55,6 @@ const templateBase = ref('');
|
|||||||
const templateFilter = ref('');
|
const templateFilter = ref('');
|
||||||
const templateVisible = ref(false);
|
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 dialogue = useDialogue();
|
||||||
const applyTemplate = async (template: Noun): Promise<void> => {
|
const applyTemplate = async (template: Noun): Promise<void> => {
|
||||||
if (JSON.stringify(form.value) !== JSON.stringify(emptyForm(config))) {
|
if (JSON.stringify(form.value) !== JSON.stringify(emptyForm(config))) {
|
||||||
@ -75,6 +62,7 @@ const applyTemplate = async (template: Noun): Promise<void> => {
|
|||||||
}
|
}
|
||||||
form.value = {
|
form.value = {
|
||||||
words: fillMissingVariants(template.words),
|
words: fillMissingVariants(template.words),
|
||||||
|
classInstance: null,
|
||||||
categories: template.categories ?? [],
|
categories: template.categories ?? [],
|
||||||
sources: template.sources,
|
sources: template.sources,
|
||||||
base: template.id,
|
base: template.id,
|
||||||
@ -100,15 +88,12 @@ const submit = async () => {
|
|||||||
};
|
};
|
||||||
const edit = (noun: Noun): void => {
|
const edit = (noun: Noun): void => {
|
||||||
form.value = {
|
form.value = {
|
||||||
words: fillMissingVariants(noun.words),
|
words: fillMissingVariants(filterIrregularWords(noun.words)),
|
||||||
|
classInstance: noun.classInstance,
|
||||||
categories: noun.categories,
|
categories: noun.categories,
|
||||||
sources: noun.sources,
|
sources: noun.sources,
|
||||||
base: noun.id,
|
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();
|
focus();
|
||||||
};
|
};
|
||||||
const focus = (editable = true): void => {
|
const focus = (editable = true): void => {
|
||||||
@ -138,25 +123,23 @@ const { data: sourcesKeys } = await useFetch('/api/sources/keys', { lazy: true,
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form v-else @submit.prevent="submit">
|
<form v-else @submit.prevent="submit">
|
||||||
<div v-for="numerus of availableNumeri(config)" :key="numerus" class="row">
|
<template v-if="config.nouns.conventions?.enabled">
|
||||||
<div v-if="config.nouns.plurals" class="col-12 text-nowrap mt-md-4">
|
<h5 class="border mb-0 p-3 bg-light">
|
||||||
<label><strong>{{ symbolsByNumeri[numerus] }} <T>nouns.{{ numerus }}</T></strong></label>
|
<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>
|
||||||
<div v-for="gender in availableGenders(config)" :key="gender" class="col-12 col-md-6 mt-2">
|
<h5 class="border mb-0 p-3 bg-light">
|
||||||
<label><strong><NounsGenderLabel :gender="gender" /></strong></label>
|
<Icon v="stream" />
|
||||||
<NounsWordsInput
|
<T>nouns.submit.irregular.header</T>
|
||||||
v-model="form.words[gender][numerus]"
|
</h5>
|
||||||
:edit-declensions
|
<div class="p-3 border border-top-0 mb-3">
|
||||||
:minitems="canRemoveWord(gender, numerus) ? 0 : 1"
|
<NounsIrregularWordsSubform v-model="form.words" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div v-if="config.nouns.declension?.enabled" class="form-check form-switch mb-2">
|
<NounsIrregularWordsSubform v-else v-model="form.words" />
|
||||||
<label>
|
|
||||||
<input v-model="editDeclensions" class="form-check-input" type="checkbox" role="switch">
|
|
||||||
<T>nouns.declension.edit</T>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<CategoriesSelector
|
<CategoriesSelector
|
||||||
v-model="form.categories"
|
v-model="form.categories"
|
||||||
:label="$t('nouns.categories')"
|
:label="$t('nouns.categories')"
|
||||||
|
@ -21,7 +21,7 @@ const visibleNumeri = computed(() => availableNumeri(config));
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<template v-for="gender in visibleGenders" :key="gender">
|
<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 />
|
<NounsGenderLabel :gender="gender" concise />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -29,6 +29,7 @@ const visibleNumeri = computed(() => availableNumeri(config));
|
|||||||
:key="numerus"
|
:key="numerus"
|
||||||
:style="{ gridArea: `${gender}${numerus === 'plural' ? 'Pl' : ''}` }"
|
:style="{ gridArea: `${gender}${numerus === 'plural' ? 'Pl' : ''}` }"
|
||||||
role="cell"
|
role="cell"
|
||||||
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<NounsItem :noun :gender :numerus />
|
<NounsItem :noun :gender :numerus />
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ const visibleNumeri = computed(() => availableNumeri(config));
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<p><strong><T>sources.referenced</T><T>quotation.colon</T></strong></p>
|
||||||
<ul class="list-unstyled mb-0">
|
<ul class="list-unstyled mb-0">
|
||||||
<li v-for="source in noun.sourcesData" :key="source.id">
|
<li v-for="source in noun.sourcesData" :key="source.id">
|
||||||
@ -49,7 +50,7 @@ const visibleNumeri = computed(() => availableNumeri(config));
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div style="grid-area: buttons">
|
<div style="grid-area: buttons" v-bind="$attrs">
|
||||||
<slot name="buttons"></slot>
|
<slot name="buttons"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -204,6 +204,22 @@ nouns:
|
|||||||
submit:
|
submit:
|
||||||
action: 'Submit'
|
action: 'Submit'
|
||||||
actionLong: 'Submit a word'
|
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!'
|
thanks: 'Thank you for contributing!'
|
||||||
another: 'Submit another one'
|
another: 'Submit another one'
|
||||||
moderation: 'Submissions will have to get approved before getting published.'
|
moderation: 'Submissions will have to get approved before getting published.'
|
||||||
|
@ -241,6 +241,16 @@ declensions:
|
|||||||
g: ['ne']
|
g: ['ne']
|
||||||
d: ['ne']
|
d: ['ne']
|
||||||
a: ['nen']
|
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:
|
classes:
|
||||||
t1:
|
t1:
|
||||||
exampleStems:
|
exampleStems:
|
||||||
@ -316,6 +326,7 @@ conventions:
|
|||||||
maskulinum:
|
maskulinum:
|
||||||
name: 'Maskulinum'
|
name: 'Maskulinum'
|
||||||
normative: true
|
normative: true
|
||||||
|
gender: 'masc'
|
||||||
morphemes:
|
morphemes:
|
||||||
article_n: 'der'
|
article_n: 'der'
|
||||||
article_g: 'des'
|
article_g: 'des'
|
||||||
@ -353,6 +364,7 @@ conventions:
|
|||||||
femininum:
|
femininum:
|
||||||
name: 'Femininum'
|
name: 'Femininum'
|
||||||
normative: true
|
normative: true
|
||||||
|
gender: 'fem'
|
||||||
morphemes:
|
morphemes:
|
||||||
article_n: 'die'
|
article_n: 'die'
|
||||||
article_g: 'der'
|
article_g: 'der'
|
||||||
@ -390,6 +402,7 @@ conventions:
|
|||||||
partizip-formen:
|
partizip-formen:
|
||||||
name: 'Partizip-Formen'
|
name: 'Partizip-Formen'
|
||||||
normative: true
|
normative: true
|
||||||
|
gender: 'neutr'
|
||||||
description:
|
description:
|
||||||
- >
|
- >
|
||||||
Aus dem Partizip I (infinite Verbform) lassen sich substantivierte Adjektive bilden.
|
Aus dem Partizip I (infinite Verbform) lassen sich substantivierte Adjektive bilden.
|
||||||
@ -417,6 +430,7 @@ conventions:
|
|||||||
person-formen:
|
person-formen:
|
||||||
name: 'Person-Formen'
|
name: 'Person-Formen'
|
||||||
normative: true
|
normative: true
|
||||||
|
gender: 'neutr'
|
||||||
morphemes:
|
morphemes:
|
||||||
article_n: 'die'
|
article_n: 'die'
|
||||||
article_g: 'der'
|
article_g: 'der'
|
||||||
@ -458,6 +472,7 @@ conventions:
|
|||||||
mensch-formen:
|
mensch-formen:
|
||||||
name: 'Mensch-Formen'
|
name: 'Mensch-Formen'
|
||||||
normative: true
|
normative: true
|
||||||
|
gender: 'neutr'
|
||||||
morphemes:
|
morphemes:
|
||||||
article_n: 'der'
|
article_n: 'der'
|
||||||
article_g: 'des'
|
article_g: 'des'
|
||||||
@ -499,6 +514,7 @@ conventions:
|
|||||||
diminuitiv:
|
diminuitiv:
|
||||||
name: 'Diminuitiv'
|
name: 'Diminuitiv'
|
||||||
normative: true
|
normative: true
|
||||||
|
gender: 'neutr'
|
||||||
warning: >
|
warning: >
|
||||||
Die Verniedlichungsform benutzt zwar das Neutrum und ist damit eine normativ neutrale Form,
|
Die Verniedlichungsform benutzt zwar das Neutrum und ist damit eine normativ neutrale Form,
|
||||||
jedoch zeichnet diese Form üblicherweise kleine und junge Nomen aus.
|
jedoch zeichnet diese Form üblicherweise kleine und junge Nomen aus.
|
||||||
@ -541,6 +557,7 @@ conventions:
|
|||||||
y-formen:
|
y-formen:
|
||||||
name: 'Y-Formen'
|
name: 'Y-Formen'
|
||||||
normative: false
|
normative: false
|
||||||
|
gender: 'neutr'
|
||||||
description:
|
description:
|
||||||
- >
|
- >
|
||||||
Bekannt als {https://www.bpb.de/shop/zeitschriften/apuz/geschlechtergerechte-sprache-2022/346085/entgendern-nach-phettberg/=Entgendern nach Phettberg}.
|
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:
|
i-formen:
|
||||||
name: 'I-Formen'
|
name: 'I-Formen'
|
||||||
normative: false
|
normative: false
|
||||||
|
gender: 'neutr'
|
||||||
description:
|
description:
|
||||||
- >
|
- >
|
||||||
Verwendet u.a. in den Romanen
|
Verwendet u.a. in den Romanen
|
||||||
@ -629,6 +647,7 @@ conventions:
|
|||||||
inklusivum:
|
inklusivum:
|
||||||
name: 'Inklusivum'
|
name: 'Inklusivum'
|
||||||
normative: false
|
normative: false
|
||||||
|
gender: 'neutr'
|
||||||
description:
|
description:
|
||||||
- 'Formen vorgestellt vom {https://geschlechtsneutral.net/=Verein für geschlechtsneutrales Deutsch}.'
|
- 'Formen vorgestellt vom {https://geschlechtsneutral.net/=Verein für geschlechtsneutrales Deutsch}.'
|
||||||
- 'Siehe auch: {/en/em=Neopronomen „en“}'
|
- 'Siehe auch: {/en/em=Neopronomen „en“}'
|
||||||
@ -669,6 +688,7 @@ conventions:
|
|||||||
indefinitivum:
|
indefinitivum:
|
||||||
name: 'Indefinitivum'
|
name: 'Indefinitivum'
|
||||||
normative: false
|
normative: false
|
||||||
|
gender: 'neutr'
|
||||||
description:
|
description:
|
||||||
- 'Formen vorgestellt von {https://www.geschlechtsneutral.com/lit/Liminalis-2008-Sylvain-Balzer.pdf=Cabala de Sylvain und Carsten Balzer}'
|
- '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“}.'
|
- 'Siehe auch: {/nin=Neopronomen „nin/nim“}.'
|
||||||
@ -705,6 +725,7 @@ conventions:
|
|||||||
ens-formen:
|
ens-formen:
|
||||||
name: 'ens-Formen'
|
name: 'ens-Formen'
|
||||||
normative: false
|
normative: false
|
||||||
|
gender: 'neutr'
|
||||||
description:
|
description:
|
||||||
- 'Formen vorgestellt von Lann Hornscheidt.'
|
- 'Formen vorgestellt von Lann Hornscheidt.'
|
||||||
- 'Siehe auch: {/ens=Neopronomen „ens“}.'
|
- 'Siehe auch: {/ens=Neopronomen „ens“}.'
|
||||||
@ -734,6 +755,7 @@ conventions:
|
|||||||
ex-formen:
|
ex-formen:
|
||||||
name: 'ex-Formen'
|
name: 'ex-Formen'
|
||||||
normative: false
|
normative: false
|
||||||
|
gender: 'neutr'
|
||||||
description:
|
description:
|
||||||
- 'Formen vorgestellt von {https://www.lannhornscheidt.com/w_ortungen/nonbinare-w_ortungen/=Lann Hornscheidt und Lio Oppenländer}.'
|
- 'Formen vorgestellt von {https://www.lannhornscheidt.com/w_ortungen/nonbinare-w_ortungen/=Lann Hornscheidt und Lio Oppenländer}.'
|
||||||
- 'Siehe auch: {/ex=Neopronomen „ex“}.'
|
- 'Siehe auch: {/ex=Neopronomen „ex“}.'
|
||||||
@ -757,6 +779,7 @@ conventions:
|
|||||||
ojum:
|
ojum:
|
||||||
name: 'Ojum'
|
name: 'Ojum'
|
||||||
normative: false
|
normative: false
|
||||||
|
gender: 'neutr'
|
||||||
description:
|
description:
|
||||||
- 'Formen vorgestellt von {https://www.frumble.de/blog/2021/03/26/ueberlegungen-zu-einer-genderneutralen-deutschen-grammatik=Frumble}.'
|
- 'Formen vorgestellt von {https://www.frumble.de/blog/2021/03/26/ueberlegungen-zu-einer-genderneutralen-deutschen-grammatik=Frumble}.'
|
||||||
- 'Siehe auch: {/oj=Neopronomen „oj/ojm“}.'
|
- 'Siehe auch: {/oj=Neopronomen „oj/ojm“}.'
|
||||||
@ -788,6 +811,7 @@ conventions:
|
|||||||
nona-system:
|
nona-system:
|
||||||
name: 'NoNa-System'
|
name: 'NoNa-System'
|
||||||
normative: false
|
normative: false
|
||||||
|
gender: 'neutr'
|
||||||
description:
|
description:
|
||||||
- 'Formen vorgestellt von {https://geschlechtsneutralesdeutsch.com/=Geschlechtsneutrales Deutsch}.'
|
- 'Formen vorgestellt von {https://geschlechtsneutralesdeutsch.com/=Geschlechtsneutrales Deutsch}.'
|
||||||
morphemes:
|
morphemes:
|
||||||
@ -823,6 +847,7 @@ conventions:
|
|||||||
genderdoppelpunkt:
|
genderdoppelpunkt:
|
||||||
name: 'Genderdoppelpunkt'
|
name: 'Genderdoppelpunkt'
|
||||||
normative: false
|
normative: false
|
||||||
|
gender: 'neutr'
|
||||||
morphemes:
|
morphemes:
|
||||||
article_n: 'der:die'
|
article_n: 'der:die'
|
||||||
article_g: 'des:der'
|
article_g: 'des:der'
|
||||||
@ -860,6 +885,7 @@ conventions:
|
|||||||
gendergap:
|
gendergap:
|
||||||
name: 'Gendergap'
|
name: 'Gendergap'
|
||||||
normative: false
|
normative: false
|
||||||
|
gender: 'neutr'
|
||||||
morphemes:
|
morphemes:
|
||||||
article_n: 'der_die'
|
article_n: 'der_die'
|
||||||
article_g: 'des_der'
|
article_g: 'des_der'
|
||||||
@ -897,6 +923,7 @@ conventions:
|
|||||||
gendersternchen:
|
gendersternchen:
|
||||||
name: 'Gendersternchen'
|
name: 'Gendersternchen'
|
||||||
normative: false
|
normative: false
|
||||||
|
gender: 'neutr'
|
||||||
morphemes:
|
morphemes:
|
||||||
article_n: 'der*die'
|
article_n: 'der*die'
|
||||||
article_g: 'des*der'
|
article_g: 'des*der'
|
||||||
@ -934,6 +961,7 @@ conventions:
|
|||||||
binnen-i:
|
binnen-i:
|
||||||
name: 'Binnen-I'
|
name: 'Binnen-I'
|
||||||
normative: false
|
normative: false
|
||||||
|
gender: 'neutr'
|
||||||
warning: >
|
warning: >
|
||||||
Das Binnen-I bezieht sich nur auf die männliche und die weibliche Form der Wörter
|
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.
|
und schließt damit (wie das generische Maskulinum) immer noch sehr viele Menschen aus der Sprache aus.
|
||||||
|
@ -224,10 +224,28 @@ nouns:
|
|||||||
submit:
|
submit:
|
||||||
action: 'Einreichen'
|
action: 'Einreichen'
|
||||||
actionLong: 'Ein Wort 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!'
|
thanks: 'Danke für deinen Beitrag!'
|
||||||
another: 'Einen weiteren Eintrag einreichen'
|
another: 'Einen weiteren Eintrag einreichen'
|
||||||
moderation: 'Einreichungen müssen erst genehmigt werden, bevor sie veröffentlicht werden.'
|
moderation: 'Einreichungen müssen erst genehmigt werden, bevor sie veröffentlicht werden.'
|
||||||
|
|
||||||
template:
|
template:
|
||||||
header: 'Eine Vorlage nutzen'
|
header: 'Eine Vorlage nutzen'
|
||||||
root: 'Wurzel'
|
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 { createCanvas, loadImage, registerFont } from 'canvas';
|
||||||
import SQL from 'sql-template-strings';
|
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 { 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 type { NounRow } from '~/server/nouns.ts';
|
||||||
import {
|
import {
|
||||||
availableGenders,
|
availableGenders,
|
||||||
@ -16,7 +16,8 @@ import {
|
|||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const locale = getLocale(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');
|
checkIsConfigEnabledOr404(await loadConfig(locale), 'nouns');
|
||||||
|
|
||||||
const { isGranted } = await useAuthentication(event);
|
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);
|
const genders = availableGenders(config);
|
||||||
|
|
||||||
@ -99,7 +100,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const symbol = symbolsByNumeri[numerus];
|
const symbol = symbolsByNumeri[numerus];
|
||||||
noun.words[gender]?.[numerus]?.forEach((word) => {
|
noun.words[gender]?.[numerus]?.forEach((word) => {
|
||||||
context.fillText(
|
context.fillText(
|
||||||
`${symbol} ${word}`,
|
`${symbol} ${displayWord(word, numerus, nounsData)}`,
|
||||||
column * (width - 2 * padding) / genders.length + padding,
|
column * (width - 2 * padding) / genders.length + padding,
|
||||||
padding * 2.5 + i * 48,
|
padding * 2.5 + i * 48,
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,7 @@ import { auditLog } from '~/server/audit.ts';
|
|||||||
import { getLocale, loadConfig } from '~/server/data.ts';
|
import { getLocale, loadConfig } from '~/server/data.ts';
|
||||||
import { approveNounEntry } from '~/server/nouns.ts';
|
import { approveNounEntry } from '~/server/nouns.ts';
|
||||||
import { isAllowedToPost } from '~/server/user.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 => {
|
const minimizeWords = (words: NounWords): NounWordsRaw => {
|
||||||
return Object.fromEntries(Object.entries(words)
|
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)
|
const word = Object.values(words)
|
||||||
.map((wordsByNumerus) => wordsByNumerus.singular?.[0])
|
.map((wordsByNumerus) => wordsByNumerus.singular?.[0])
|
||||||
.find((word) => word !== undefined);
|
.find((word) => word !== undefined);
|
||||||
@ -52,12 +56,13 @@ export default defineEventHandler(async (event) => {
|
|||||||
const id = ulid();
|
const id = ulid();
|
||||||
await db.get(SQL`
|
await db.get(SQL`
|
||||||
INSERT INTO nouns (
|
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 (
|
VALUES (
|
||||||
${id},
|
${id},
|
||||||
${generateKey(body.words)},
|
${generateKey(body.words, body.classInstance)},
|
||||||
${JSON.stringify(minimizeWords(body.words))},
|
${JSON.stringify(minimizeWords(body.words))},
|
||||||
|
${body.classInstance ? JSON.stringify(body.classInstance) : null},
|
||||||
${body.categories.join('|')},
|
${body.categories.join('|')},
|
||||||
${body.sources ? body.sources.join(',') : null},
|
${body.sources ? body.sources.join(',') : null},
|
||||||
0, ${body.base}, ${locale}, ${user.id}
|
0, ${body.base}, ${locale}, ${user.id}
|
||||||
|
@ -9,18 +9,25 @@ import type { RuntimeConfig } from 'nuxt/schema';
|
|||||||
import type { Config } from '~/locale/config.ts';
|
import type { Config } from '~/locale/config.ts';
|
||||||
import localeDescriptions from '~/locale/locales.ts';
|
import localeDescriptions from '~/locale/locales.ts';
|
||||||
import { getPosts } from '~/server/blog.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 { 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 { rootDir } from '~/server/paths.ts';
|
||||||
import { getSourcesEntries } from '~/server/sources.ts';
|
import { getSourcesEntries } from '~/server/sources.ts';
|
||||||
import { getTermsEntries } from '~/server/terms.ts';
|
import { getTermsEntries } from '~/server/terms.ts';
|
||||||
import { shortForVariant } from '~/src/buildPronoun.ts';
|
import { shortForVariant } from '~/src/buildPronoun.ts';
|
||||||
import { Day } from '~/src/calendar/helpers.ts';
|
import { Day } from '~/src/calendar/helpers.ts';
|
||||||
import { loadWords, Noun } from '~/src/classes.ts';
|
|
||||||
import { getUrlForLocale } from '~/src/domain.ts';
|
import { getUrlForLocale } from '~/src/domain.ts';
|
||||||
import forbidden from '~/src/forbidden.ts';
|
import forbidden from '~/src/forbidden.ts';
|
||||||
import { clearLinkedText, buildImageUrl } from '~/src/helpers.ts';
|
import { clearLinkedText, buildImageUrl } from '~/src/helpers.ts';
|
||||||
|
import type { Numerus } from '~/src/nouns.ts';
|
||||||
import parseMarkdown from '~/src/parseMarkdown.ts';
|
import parseMarkdown from '~/src/parseMarkdown.ts';
|
||||||
import { normaliseQuery, validateQuery } from '~/src/search.ts';
|
import { normaliseQuery, validateQuery } from '~/src/search.ts';
|
||||||
import type { SearchDocument } from '~/src/search.ts';
|
import type { SearchDocument } from '~/src/search.ts';
|
||||||
@ -613,11 +620,13 @@ const kinds: SearchKind[] = [
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nounsData = await loadNounsData(config.locale);
|
||||||
|
|
||||||
const base = encodeURIComponent(config.nouns.route);
|
const base = encodeURIComponent(config.nouns.route);
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
const nouns = (await getNounEntries(db, () => false, config.locale))
|
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 => {
|
return nouns.map((noun, id): SearchDocument => {
|
||||||
const firstWords = noun.firstWords;
|
const firstWords = noun.firstWords;
|
||||||
return {
|
return {
|
||||||
@ -626,8 +635,13 @@ const kinds: SearchKind[] = [
|
|||||||
url: `/${base}?filter=${firstWords[0]}`,
|
url: `/${base}?filter=${firstWords[0]}`,
|
||||||
title: firstWords.join(' – '),
|
title: firstWords.join(' – '),
|
||||||
content: Object.values(noun.words)
|
content: Object.values(noun.words)
|
||||||
.flatMap((wordsByNumerus) => Object.values(wordsByNumerus))
|
.map((wordsByNumerus) => {
|
||||||
.flatMap((words) => words)
|
return Object.entries(wordsByNumerus)
|
||||||
|
.flatMap(([numerus, words]) => {
|
||||||
|
return words.map((word) => displayWord(word, numerus as Numerus, nounsData));
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
})
|
||||||
.join(' – '),
|
.join(' – '),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -9,6 +9,7 @@ import type { Calendar } from '~/src/calendar/helpers.ts';
|
|||||||
import { PronounLibrary } from '~/src/classes.ts';
|
import { PronounLibrary } from '~/src/classes.ts';
|
||||||
import type { Pronoun, PronounGroup } from '~/src/classes.ts';
|
import type { Pronoun, PronounGroup } from '~/src/classes.ts';
|
||||||
import { getLocaleForUrl, getUrlForLocale } from '~/src/domain.ts';
|
import { getLocaleForUrl, getUrlForLocale } from '~/src/domain.ts';
|
||||||
|
import type { NounsData } from '~/src/nouns.ts';
|
||||||
import { Translator } from '~/src/translator.ts';
|
import { Translator } from '~/src/translator.ts';
|
||||||
|
|
||||||
const setDefault = async <K, V>(map: Map<K, V>, key: K, supplier: () => Promise<V>): Promise<V> => {
|
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();
|
const calendarByLocale: Map<string, Calendar> = new Map();
|
||||||
export const loadCalendar = async (locale: string): Promise<Calendar> => {
|
export const loadCalendar = async (locale: string): Promise<Calendar> => {
|
||||||
return setDefault(calendarByLocale, locale, async () => {
|
return setDefault(calendarByLocale, locale, async () => {
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
import SQL from 'sql-template-strings';
|
import SQL from 'sql-template-strings';
|
||||||
|
|
||||||
|
import type { Config } from '~/locale/config.ts';
|
||||||
import type { Database } from '~/server/db.ts';
|
import type { Database } from '~/server/db.ts';
|
||||||
import type { UserRow } from '~/server/express/user.ts';
|
import type { UserRow } from '~/server/express/user.ts';
|
||||||
import type { SourceRow } from '~/server/sources.ts';
|
import type { SourceRow } from '~/server/sources.ts';
|
||||||
import type { IsGrantedFn } from '~/server/utils/useAuthentication.ts';
|
import type { IsGrantedFn } from '~/server/utils/useAuthentication.ts';
|
||||||
|
import { loadWords, Noun } from '~/src/classes.ts';
|
||||||
import type { NounRaw } from '~/src/classes.ts';
|
import type { NounRaw } from '~/src/classes.ts';
|
||||||
import { clearKey } from '~/src/helpers.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';
|
import type { User } from '~/src/user.ts';
|
||||||
|
|
||||||
export interface NounRow {
|
export interface NounRow {
|
||||||
id: string;
|
id: string;
|
||||||
key: string;
|
key: string;
|
||||||
words: string;
|
words: string;
|
||||||
|
classInstance: string | null;
|
||||||
approved: number;
|
approved: number;
|
||||||
base_id: string | null;
|
base_id: string | null;
|
||||||
locale: string;
|
locale: string;
|
||||||
@ -28,11 +33,22 @@ export const parseNounRow = (nounRow: NounRow): Omit<NounRaw, 'sourcesData'> =>
|
|||||||
return {
|
return {
|
||||||
id: nounRow.id,
|
id: nounRow.id,
|
||||||
words: JSON.parse(nounRow.words),
|
words: JSON.parse(nounRow.words),
|
||||||
|
classInstance: nounRow.classInstance ? JSON.parse(nounRow.classInstance) : null,
|
||||||
categories: nounRow.categories?.split('|') ?? [],
|
categories: nounRow.categories?.split('|') ?? [],
|
||||||
sources: nounRow.sources ? nounRow.sources.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 parseNounRowWithAuthor = (nounRow: NounRowWithAuthor, isGranted: IsGrantedFn): Omit<NounRaw, 'sourcesData'> => {
|
||||||
const noun = parseNounRow(nounRow);
|
const noun = parseNounRow(nounRow);
|
||||||
if (isGranted('nouns')) {
|
if (isGranted('nouns')) {
|
||||||
@ -43,6 +59,17 @@ const parseNounRowWithAuthor = (nounRow: NounRowWithAuthor, isGranted: IsGranted
|
|||||||
return noun;
|
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 (
|
export const addVersions = async (
|
||||||
db: Database,
|
db: Database,
|
||||||
isGranted: IsGrantedFn,
|
isGranted: IsGrantedFn,
|
||||||
|
@ -11,6 +11,7 @@ import type {
|
|||||||
NounWordsRaw,
|
NounWordsRaw,
|
||||||
NounWord,
|
NounWord,
|
||||||
NounDeclensionsByFirstCase,
|
NounDeclensionsByFirstCase,
|
||||||
|
NounClassInstance,
|
||||||
NounConvention,
|
NounConvention,
|
||||||
Gender,
|
Gender,
|
||||||
Numerus,
|
Numerus,
|
||||||
@ -844,6 +845,7 @@ export class PronounLibrary {
|
|||||||
export interface NounRaw {
|
export interface NounRaw {
|
||||||
id: string;
|
id: string;
|
||||||
words: NounWordsRaw;
|
words: NounWordsRaw;
|
||||||
|
classInstance?: NounClassInstance | null;
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
sources?: string[];
|
sources?: string[];
|
||||||
sourcesData?: SourceRaw[];
|
sourcesData?: SourceRaw[];
|
||||||
@ -874,7 +876,7 @@ const loadWord = (
|
|||||||
return wordRaw;
|
return wordRaw;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface NounRawWithLoadedWords extends Omit<NounRaw, 'words'> {
|
export interface NounRawWithLoadedWords extends Omit<NounRaw, 'words'> {
|
||||||
words: NounWords;
|
words: NounWords;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -920,6 +922,7 @@ const hasWordOfGender = (words: NounWords, gender: Gender): boolean => {
|
|||||||
export class Noun implements Entry {
|
export class Noun implements Entry {
|
||||||
id: string;
|
id: string;
|
||||||
words: NounWords;
|
words: NounWords;
|
||||||
|
classInstance: NounClassInstance | null;
|
||||||
categories: string[];
|
categories: string[];
|
||||||
sources: string[];
|
sources: string[];
|
||||||
sourcesData: Source[];
|
sourcesData: Source[];
|
||||||
@ -930,6 +933,7 @@ export class Noun implements Entry {
|
|||||||
constructor(config: Config, nounRaw: NounRawWithLoadedWords) {
|
constructor(config: Config, nounRaw: NounRawWithLoadedWords) {
|
||||||
this.id = nounRaw.id;
|
this.id = nounRaw.id;
|
||||||
this.words = nounRaw.words;
|
this.words = nounRaw.words;
|
||||||
|
this.classInstance = nounRaw.classInstance ?? null;
|
||||||
this.categories = nounRaw.categories ?? [];
|
this.categories = nounRaw.categories ?? [];
|
||||||
this.sources = nounRaw.sources ?? [];
|
this.sources = nounRaw.sources ?? [];
|
||||||
this.sourcesData = nounRaw.sourcesData?.filter((s) => !!s).map((s) => new Source(config, s)) ?? [];
|
this.sourcesData = nounRaw.sourcesData?.filter((s) => !!s).map((s) => new Source(config, s)) ?? [];
|
||||||
|
@ -163,7 +163,8 @@ export function listMissingTranslations(
|
|||||||
return false;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
88
src/nouns.ts
88
src/nouns.ts
@ -48,6 +48,7 @@ export const symbolsByNumeri: Record<Numerus, string> = {
|
|||||||
|
|
||||||
export interface NounWord {
|
export interface NounWord {
|
||||||
spelling: string;
|
spelling: string;
|
||||||
|
regular?: boolean;
|
||||||
convention?: keyof Required<NounsData>['conventions'];
|
convention?: keyof Required<NounsData>['conventions'];
|
||||||
declension?: keyof Required<NounsData>['declensions'] | NounDeclension;
|
declension?: keyof Required<NounsData>['declensions'] | NounDeclension;
|
||||||
}
|
}
|
||||||
@ -65,6 +66,7 @@ export interface NounConventionGroup {
|
|||||||
export interface NounConvention {
|
export interface NounConvention {
|
||||||
name: string;
|
name: string;
|
||||||
normative: boolean;
|
normative: boolean;
|
||||||
|
gender: Gender;
|
||||||
warning?: string;
|
warning?: string;
|
||||||
description?: string[];
|
description?: string[];
|
||||||
morphemes: Record<string, MorphemeValue | string>;
|
morphemes: Record<string, MorphemeValue | string>;
|
||||||
@ -77,8 +79,13 @@ interface NounConventionTemplate {
|
|||||||
declension: string;
|
declension: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NounStem {
|
||||||
|
name: string;
|
||||||
|
example: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface NounClass {
|
export interface NounClass {
|
||||||
exampleStems: Record<string, string>;
|
exampleStems: Record<keyof Required<NounsData>['stems'], string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NounClassExample {
|
export interface NounClassExample {
|
||||||
@ -125,9 +132,88 @@ export interface NounsData {
|
|||||||
morphemes?: string[];
|
morphemes?: string[];
|
||||||
examples?: string[];
|
examples?: string[];
|
||||||
grammarTables?: GrammarTableDefinition[];
|
grammarTables?: GrammarTableDefinition[];
|
||||||
|
stems?: Record<string, NounStem>;
|
||||||
classes?: Record<string, NounClass>;
|
classes?: Record<string, NounClass>;
|
||||||
classExample?: NounClassExample;
|
classExample?: NounClassExample;
|
||||||
declensions?: Record<string, NounDeclension>;
|
declensions?: Record<string, NounDeclension>;
|
||||||
groups?: Record<string, NounConventionGroup>;
|
groups?: Record<string, NounConventionGroup>;
|
||||||
conventions?: Record<string, NounConvention>;
|
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