Merge branch 'pronouns-consistency-and-picker' into 'main'

(pronouns) consistent styling across pages and a picker for the editor

See merge request PronounsPage/PronounsPage!531
This commit is contained in:
Valentyne Stigloher 2024-10-27 10:08:29 +00:00
commit 65fe7d7dff
52 changed files with 1005 additions and 996 deletions

View File

@ -36,7 +36,7 @@ import type { Source } from '../src/classes.ts';
export default defineComponent({
props: {
pronoun: { },
sources: { required: true, type: Object as PropType<Record<string, Source[]>> },
sources: { required: true, type: Object as PropType<Record<string, Source[] | undefined>> },
},
data() {
return {
@ -47,7 +47,9 @@ export default defineComponent({
},
computed: {
visibleSources(): Record<string, Source[]> {
return Object.fromEntries(Object.entries(this.sources).filter(([_, sources]) => sources));
return Object.fromEntries(
Object.entries(this.sources).filter(([_, sources]) => sources),
) as Record<string, Source[]>;
},
},
});

View File

@ -1,32 +1,24 @@
<script setup lang="ts">
import type { PronounGroup, Pronoun } from '~/src/classes.ts';
const props = defineProps<{
pronounGroup: { group: PronounGroup, groupPronouns: Record<string, Pronoun> | Pronoun[] };
defineProps<{
pronounGroup?: PronounGroup;
pronouns: Record<string, Pronoun> | Pronoun[] | string[];
}>();
const pronouns = computed(() => {
if (Array.isArray(props.pronounGroup.groupPronouns)) {
return props.pronounGroup.groupPronouns;
}
return Object.values(props.pronounGroup.groupPronouns);
});
</script>
<template>
<section v-if="pronounGroup && pronounGroup.group.name">
<section>
<ul class="list-group mt-4">
<li class="list-group-item">
<p class="h5">
<Spelling :text="pronounGroup.group.name" />
<p v-if="pronounGroup?.name" class="h5">
<Spelling :text="pronounGroup.name" />
</p>
<p v-if="pronounGroup.group.description" class="small my-1">
<p v-if="pronounGroup?.description" class="small my-1">
<Icon v="info-circle" />
<LinkedText :text="pronounGroup.group.description" />
<LinkedText :text="pronounGroup.description" />
</p>
<ul class="list-unstyled">
<SimplePronounList :pronouns="pronouns" />
</ul>
<SimplePronounList :pronouns="Array.isArray(pronouns) ? pronouns : Object.values(pronouns)" />
</li>
<nuxt-link :to="{ name: 'pronouns' }" class="list-group-item list-group-item-action text-center">
<Icon v="ellipsis-h-alt" />

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { buildPronoun } from '~/src/buildPronoun.ts';
import { buildPronounUsage } from '~/src/buildPronoun.ts';
import type { Pronoun } from '~/src/classes.ts';
import { pronouns as pronounsData } from '~/src/data.ts';
import { pronounLibrary } from '~/src/data.ts';
const props = defineProps<{
pronouns: Pronoun[] | string[];
@ -10,40 +10,43 @@ const props = defineProps<{
const { $translator: translator } = useNuxtApp();
const config = useConfig();
const glue = ` ${translator.translate('pronouns.or')} `;
const visiblePronouns = computed(() => {
return props.pronouns
.map((pronoun): Pronoun | undefined => {
.map((pronoun) => {
if (typeof pronoun === 'string') {
const parsed = buildPronoun(pronounsData, pronoun, config, translator);
if (parsed) {
parsed.canonicalName = pronoun;
parsed.description = '';
return parsed;
const usage = buildPronounUsage(pronounLibrary, pronoun, config, translator);
if (usage) {
return {
path: pronoun,
short: usage.short.options.join(usage.short.glue),
};
}
} else if (!pronoun.hidden) {
return pronoun;
return {
path: pronoun.canonicalName,
short: pronoun.name(glue),
description: pronoun.description,
normative: pronoun.normative,
smallForm: pronoun.smallForm ? pronoun.morphemes[pronoun.smallForm] : undefined,
};
}
})
.filter((entry) => entry !== undefined);
});
const glue = ` ${translator.translate('pronouns.or')} `;
const addSlash = (link: string): string => {
return link + (['*', '\''].includes(link.substring(link.length - 1)) ? '/' : '');
};
</script>
<template>
<ul>
<li v-for="pronoun in visiblePronouns" :key="pronoun.canonicalName">
<nuxt-link :to="addSlash(`${config.pronouns.prefix || ''}/${pronoun.canonicalName}`)">
<strong><Spelling :text="pronoun.name(glue)" /></strong><small v-if="pronoun.smallForm">/<Spelling :text="pronoun.morphemes[pronoun.smallForm] ?? undefined" /></small>
<li v-for="pronoun in visiblePronouns" :key="pronoun.path">
<PronounsIndexLink :path="pronoun.path">
<strong><Spelling :text="pronoun.short" /></strong><small v-if="pronoun.smallForm">/<Spelling :text="pronoun.smallForm" /></small>
<template v-if="pronoun.description">
<small><Spelling :text="pronoun.description as string" /></small>
</template>
</nuxt-link>
</PronounsIndexLink>
<NormativeBadge v-if="pronoun.normative" />
</li>
</ul>

View File

@ -0,0 +1,201 @@
<script setup lang="ts">
import { ExampleCategory, ExamplePart, Pronoun } from '~/src/classes.ts';
import Compressor from '~/src/compressor.ts';
import { examples, pronounLibrary, pronouns } from '~/src/data.ts';
import MORPHEMES from '~/data/pronouns/morphemes.ts';
const config = useConfig();
if (!config.pronouns.enabled || !config.pronouns.generator.enabled) {
throw new Error('config.pronouns.generator is disabled');
}
const { $translator: translator } = useNuxtApp();
const exampleCategories = ExampleCategory.from(examples, config);
const examplesByHonorific = [false, true].map((isHonorific) => {
const examples = exampleCategories
.filter((exampleCategory) => !exampleCategory.comprehensive)
.map((exampleCategory) => exampleCategory.examples[0])
.filter((example) => example.isHonorific === isHonorific);
return { examples, isHonorific };
}).filter(({ examples }) => examples.length > 0);
const selectedPronoun = ref(pronouns[config.pronouns.default].clone(true));
const selectedMorpheme = ref('');
const glue = ` ${translator.translate('pronouns.or')} `;
const clearExampleParts = (parts: ExamplePart[]): ExamplePart[] => {
return parts.map((p) => new ExamplePart(p.variable, p.str.replace(/^'/, '')));
};
const deduplicatePronounGroup = (pronounGroup: Pronoun[]): Pronoun[] => {
const dict: Record<string, Pronoun> = {};
for (const pronoun of pronounGroup) {
if (Object.hasOwn(dict, pronoun.name(glue))) {
continue;
}
dict[pronoun.name(glue)] = pronoun;
}
return Object.values(dict);
};
const usedBase = computed((): string | null => {
const name = selectedPronoun.value.name(glue);
for (const key in pronouns) {
if (Object.hasOwn(pronouns, key)) {
if (key === name) {
return key;
}
for (const alias of pronouns[key].aliases) {
if (alias === name) {
return key;
}
}
}
}
return null;
});
const usedBaseEquals = computed((): boolean => {
return !!usedBase.value && selectedPronoun.value.equals(pronouns[usedBase.value], true);
});
const longPath = computed((): string => {
const base = pronouns[selectedPronoun.value.morphemes[MORPHEMES[0]]!];
return base
? Compressor.compress(
selectedPronoun.value.toArray().map((x) => x.split('|')[0]),
base.toArray().map((x) => x.split('|')[0]),
).join(',')
: selectedPronoun.value.toString();
});
const path = computed((): string | null => {
if (!selectedPronoun.value.pronoun()) {
return null;
}
const slashes = selectedPronoun.value.toStringSlashes(translator);
if (usedBaseEquals.value) {
return usedBase.value;
} else if (slashes) {
return slashes;
} else {
return longPath.value;
}
});
</script>
<template>
<PronounsGenerator
v-if="config.pronouns.enabled && config.pronouns.generator.enabled"
type="generator"
:path="path"
:initially-open="config.pronouns.generator.autoOpen ?? false"
:disclaimer="!usedBaseEquals"
>
<div class="card-title border-bottom pb-3">
<p><strong><T>home.generator.base</T><T>quotation.colon</T></strong></p>
<ul class="list-unstyled">
<template v-for="[group, groupPronouns] in pronounLibrary.split()" :key="group.key">
<li v-if="!group.hidden">
<ul class="list-inline">
<li class="list-inline-item">
<Spelling :text="group.name" />
</li>
<template
v-for="pronoun in deduplicatePronounGroup(groupPronouns)"
:key="pronoun.canonicalName"
>
<li v-if="!pronoun.hidden" class="list-inline-item">
<button
:class="['btn', pronoun.name(glue) === selectedPronoun.name(glue) ? 'btn-primary' : 'btn-outline-primary', 'btn-sm', 'my-1']"
@click="selectedPronoun = pronoun.clone(true)"
>
<Spelling :text="pronoun.name(glue)" />
</button>
</li>
</template>
</ul>
</li>
</template>
</ul>
</div>
<div class="alert alert-primary">
<p class="h3 mb-0 text-center">
<Spelling escape :text="selectedPronoun.name(glue)" />
<template v-if="config.pronouns.generator.description ?? true">
<br>
<input
v-model="selectedPronoun.description"
class="form-control form-input p-0 form-control-sm"
:size="selectedPronoun.description.length ? selectedPronoun.description.length + 3 : 16"
:maxlength="Pronoun.DESCRIPTION_MAXLENGTH"
:placeholder="$t('profile.description')"
>
</template>
</p>
</div>
<p>
<T>pronouns.examples.header</T><T>quotation.colon</T>
</p>
<template
v-for="{ examples: examplesOfHonorific, isHonorific } in examplesByHonorific"
:key="isHonorific"
>
<ul>
<li v-for="(example, i) in examplesOfHonorific" :key="i">
<template v-for="(part, j) in clearExampleParts(example.parts(selectedPronoun))" :key="j">
<input
v-if="part.variable"
v-model="selectedPronoun.morphemes[part.str]"
:class="['form-control form-input p-0', { active: selectedMorpheme === part.str }]"
:size="selectedPronoun.morphemes[part.str]?.length ?? 0"
maxlength="24"
@focus="selectedMorpheme = part.str"
@blur="selectedMorpheme = ''"
>
<span v-else><Spelling :text="part.str" /></span>
</template>
</li>
</ul>
<div v-if="config.pronouns.plurals" class="my-3">
<div v-if="isHonorific" class="custom-control custom-switch">
<input
id="pluralHonorific"
v-model="selectedPronoun.pluralHonorific[0]"
type="checkbox"
class="custom-control-input"
>
<label class="custom-control-label" for="pluralHonorific">
<T>pronouns.plural</T>
<Icon v="level-up" />
</label>
</div>
<div v-else class="custom-control custom-switch">
<input
id="plural"
v-model="selectedPronoun.plural[0]"
type="checkbox"
class="custom-control-input"
>
<label class="custom-control-label" for="plural">
<T>pronouns.plural</T>
<Icon v="level-up" />
</label>
</div>
</div>
</template>
<p class="small">
<T icon="info-circle">home.generator.alt</T>
</p>
<!-- TODO #136
<p class="small" v-if="config.pronunciation.enabled && $te('home.generator.pronunciation')">
<Icon v="info-circle"/>
<T>home.generator.pronunciation</T>
</p>
-->
</PronounsGenerator>
</template>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import { isEmoji } from '~/src/helpers.ts';
const base = ref('');
const path = computed((): string | null => {
if (!isEmoji(base.value)) {
return null;
}
return base.value;
});
</script>
<template>
<PronounsGenerator type="emoji" :path="path" disclaimer>
<div class="form-group">
<label for="emojiPronounsBase"><T>pronouns.emoji.base</T></label>
<input
id="emojiPronounsBase"
v-model="base"
class="form-control"
>
</div>
</PronounsGenerator>
</template>

View File

@ -0,0 +1,63 @@
<script setup lang="ts">
import { addSlash } from '~/src/helpers.ts';
import { addPronounInjectionKey } from '~/src/injectionKeys.ts';
const props = defineProps<{
type: 'generator' | 'alt' | 'null' | 'emoji';
path: string | null;
disclaimer?: boolean;
initiallyOpen?: boolean;
}>();
const addPronoun = inject(addPronounInjectionKey, undefined);
const runtimeConfig = useRuntimeConfig();
const config = useConfig();
const open = ref(props.initiallyOpen ?? false);
const link = computed((): string | null => {
if (!props.path) {
return null;
}
return addSlash(`${runtimeConfig.public.baseUrl + (config.pronouns.prefix || '')}/${props.path}`);
});
</script>
<template>
<button v-if="!open" type="button" class="btn btn-outline-primary w-100" @click="open = true">
<Icon v="sliders-h-square" />
<T v-if="type === 'generator'">home.generator.button</T>
<T v-else>pronouns.{{ type }}.button</T>
</button>
<div v-else class="card">
<div class="card-header">
<Icon v="sliders-h-square" />
<T v-if="type === 'generator'">home.generator.header2</T>
<template v-else>
<T>pronouns.{{ type }}.button</T><T>quotation.colon</T>
</template>
</div>
<div class="card-body">
<slot></slot>
</div>
<div v-if="path" class="card-footer">
<LinkInput v-if="addPronoun === undefined" :link="link" />
<div v-else class="input-group my-2">
<input class="form-control" readonly :value="path">
<button
type="button"
class="btn btn-outline-success"
@click="addPronoun(path)"
>
<Icon v="plus-circle" />
</button>
</div>
<div v-if="disclaimer && (config.pronouns.generator?.disclaimer ?? true)" class="alert alert-warning">
<Icon v="exclamation-triangle" />
<T>pronouns.generated</T>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,115 @@
<script setup lang="ts">
import { buildAnyPronounsList, headerForVariant } from '~/src/buildPronoun.ts';
import { pronounLibrary } from '~/src/data.ts';
defineProps<{
examples?: boolean;
}>();
const config = useConfig();
const anyPronouns = buildAnyPronounsList(config, pronounLibrary);
</script>
<template>
<ul v-if="config.pronouns.enabled" class="list-group mt-4">
<template v-for="[group, groupPronouns] in pronounLibrary.split()" :key="group.key">
<li v-if="!group.hidden" class="list-group-item">
<p class="h5">
<Spelling :text="group.name" />
</p>
<p v-if="group.description" class="small my-1">
<Icon v="info-circle" />
<LinkedText :text="group.description" />
</p>
<SimplePronounList :pronouns="groupPronouns" />
</li>
</template>
<li v-if="config.pronouns.generator.enabled" id="generator" class="list-group-item">
<p class="h5">
<T>home.generator.header</T>
</p>
<p class="small my-1">
<Icon v="info-circle" />
<T>home.generator.description</T>
</p>
<PronounsCustomGenerator />
</li>
<li id="multiple" class="list-group-item">
<p class="h5">
<Spelling :text="config.pronouns.multiple.name" />
</p>
<p v-if="config.pronouns.multiple.description" class="small my-1">
<Icon v="info-circle" />
<LinkedText :text="config.pronouns.multiple.description" />
</p>
<SimplePronounList v-if="examples" :pronouns="config.pronouns.multiple.examples" class="mb-3" />
<PronounsMultipleGenerator />
</li>
<li v-if="config.pronouns.null !== false" id="nameself" class="list-group-item">
<p class="h5">
<template v-for="(variant, i) in config.pronouns.null.routes" :key="variant">
{{ i === 0 ? '' : ' / ' }}
<PronounsIndexLink :path="variant">
<Spelling :text="headerForVariant('null', variant, $translator)" />
</PronounsIndexLink>
</template>
<NormativeBadge />
</p>
<p class="small my-1">
<Icon v="info-circle" />
<T>pronouns.null.description</T>
</p>
<SimplePronounList v-if="examples" :pronouns="config.pronouns.null.examples ?? []" class="mb-3" />
<PronounsNullGenerator />
</li>
<li v-if="config.pronouns.emoji !== false" class="list-group-item">
<p class="h5">
<Spelling :text="config.pronouns.emoji.description" />
</p>
<div v-if="config.pronouns.emoji.history" class="small my-1">
<Icon v="info-circle" />
<LinkedText :text="config.pronouns.emoji.history" />
</div>
<SimplePronounList v-if="examples" :pronouns="config.pronouns.emoji.examples" class="mb-3" />
<PronounsEmojiGenerator />
</li>
<li v-if="config.pronouns.mirror" id="mirror" class="list-group-item">
<p class="h5">
<PronounsIndexLink :path="config.pronouns.mirror.route">
<LinkedText :text="config.pronouns.mirror.name" />
</PronounsIndexLink>
</p>
<p v-if="config.pronouns.mirror.description" class="small my-1">
<Icon v="info-circle" />
<LinkedText :text="config.pronouns.mirror.description" />
</p>
</li>
<li v-if="config.pronouns.any" class="list-group-item">
<p class="h5">
<PronounsIndexLink :path="config.pronouns.any">
<T>pronouns.any.header</T>
</PronounsIndexLink>
</p>
<p class="small my-1">
<Icon v="info-circle" />
<T>pronouns.any.description</T>
</p>
<SimplePronounList :pronouns="anyPronouns" />
</li>
<li v-if="config.pronouns.ask" class="list-group-item">
<p class="h5">
<template v-for="(variant, i) in config.pronouns.ask.routes" :key="variant">
{{ i === 0 ? '' : ' / ' }}
<PronounsIndexLink :path="variant">
<Spelling :text="headerForVariant('ask', variant, $translator)" />
</PronounsIndexLink>
</template>
</p>
<p class="small my-1">
<Icon v="info-circle" />
<T>pronouns.ask.description</T>
</p>
</li>
</ul>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import { addSlash } from '~/src/helpers.ts';
import { addPronounInjectionKey } from '~/src/injectionKeys.ts';
defineProps<{
path: string;
}>();
const addPronoun = inject(addPronounInjectionKey, undefined);
const config = useConfig();
</script>
<template>
<nuxt-link
v-if="addPronoun === undefined"
:to="addSlash(`${config.pronouns.prefix || ''}/${encodeURIComponent(path)}`)"
>
<slot></slot>
</nuxt-link>
<template v-else>
<span class="text-primary"><slot></slot></span>
<button type="button" class="btn btn-sm p-0 text-success" @click="addPronoun(path)">
<Icon v="plus-circle" hover />
</button>
</template>
</template>

View File

@ -0,0 +1,44 @@
<script setup lang="ts">
import { pronouns } from '~/src/data.ts';
const config = useConfig();
const multiple = ref(config.pronouns.multiple ? config.pronouns.multiple.examples[0].split('&') : []);
const path = computed((): string | null => {
if (!multiple.value.length) {
return null;
}
return multiple.value.join('&');
});
const togglePronoun = (name: string): void => {
const index = multiple.value.indexOf(name);
if (index > -1) {
multiple.value.splice(index, 1);
} else {
multiple.value.push(name);
}
};
</script>
<template>
<PronounsGenerator type="alt" :path="path">
<div class="card-title">
<ul class="list-inline d-inline mb-0">
<template v-for="(pronoun, pronounName) in pronouns" :key="pronoun.canonicalName">
<li v-if="!pronoun.hidden" class="list-inline-item">
<button
type="button"
:class="['btn', multiple.includes(pronounName) ? 'btn-primary' : 'btn-outline-primary', 'btn-sm', 'my-1']"
@click="togglePronoun(pronounName)"
>
<Spelling :text="pronoun.name('')" />
</button>
</li>
</template>
</ul>
</div>
</PronounsGenerator>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import { NULL_PRONOUNS_MAXLENGTH } from '~/src/buildPronoun.ts';
const config = useConfig();
const base = ref('');
const path = computed((): string | null => {
if (!base.value) {
return null;
}
return `:${base.value}`;
});
</script>
<template>
<PronounsGenerator
v-if="config.pronouns.enabled && config.pronouns.null && config.pronouns.null.morphemes"
type="null"
:path="path"
disclaimer
>
<div class="form-group">
<label for="nullPronounsBase"><T>pronouns.null.base</T></label>
<input
id="nullPronounsBase"
v-model="base"
class="form-control"
:maxlength="NULL_PRONOUNS_MAXLENGTH"
>
</div>
</PronounsGenerator>
</template>

View File

@ -0,0 +1,24 @@
export default () => {
const config = useConfig();
const route = useRoute();
const router = useRouter();
const comprehensive = computed({
get() {
return config.pronouns.comprehensive ? Object.hasOwn(route.query, config.pronouns.comprehensive) : false;
},
set(value) {
if (value === comprehensive.value || !config.pronouns.comprehensive) {
// prevent warning that $router.replace has no effect
return;
}
const query = structuredClone(route.query);
if (value) {
query[config.pronouns.comprehensive] = null;
} else {
delete query[config.pronouns.comprehensive];
}
router.replace({ query });
},
});
return comprehensive;
};

View File

@ -21,14 +21,7 @@ pronouns:
Some like when others alternate between those forms when talking about them.
examples: ['he&she', 'he&they', 'she&they']
null:
description: >
{/avoiding=Avoiding gendered forms} / {/no-pronouns=no pronouns} / {/null=null pronouns} /
{/pronounless=pronounless} / {/nullpronominal=nullpronominal} / {/nameself=nameself}
history: >
Some people prefer not using any pronouns, instead being referred by name, initial,
omitting pronouns with passive voice, or restructuring the sentence.
See: {https://web.archive.org/web/20211214214513/https://twitter.com/lypomania/status/1290274971642544128=lypomania's guide},
{https://web.archive.org/web/20230326021305/https://www.lgbtqia.wiki/wiki/Nullpronominal=Nullpronominal on LGBTA Wiki}
routes: ['avoiding', 'no-pronouns', 'null', 'pronounless', 'nullpronominal', 'nameself']
morphemes:
pronoun_subject: '#'
pronoun_object: '#'
@ -36,7 +29,6 @@ pronouns:
possessive_pronoun: '#''s'
reflexive: '#self'
examples: [':Andrea', ':S']
routes: ['avoiding']
ideas:
-
header: 'Use names or initials instead of pronouns'

View File

@ -76,6 +76,11 @@ pronouns:
header: 'Interchangeable forms'
raw: 'interchangeable'
null:
description: >
Some people prefer not using any pronouns, instead being referred by name, initial,
omitting pronouns with passive voice, or restructuring the sentence.
See: {https://web.archive.org/web/20211214214513/https://twitter.com/lypomania/status/1290274971642544128=lypomania's guide},
{https://web.archive.org/web/20230326021305/https://www.lgbtqia.wiki/wiki/Nullpronominal=Nullpronominal on LGBTA Wiki}
button: 'Generate a link with a name or initial'
base: 'Name or initial'
emoji:

View File

@ -196,20 +196,12 @@ interface MultiplePronounsConfig {
}
export type NullPronounsConfig = {
/**
* short description in the header (translated)
*/
description: string;
/**
* longer description as text (translated)
*/
history: string;
/**
* route paths for null pronoun usage (translated).
* when empty, disables page
* @default []
*/
routes?: string[];
routes: string[];
/**
* different strategies of avoiding pronouns
*/

View File

@ -91,12 +91,7 @@ pronouns:
description: 'Einige nichtbinäre Menschen nutzen mehr als eine Form von Pronomen und sind damit einverstanden, mit irgendwelchen dieser Pronomen angesprochen zu werden.'
examples: ['er&sie', 'er&dey', 'sie&xier']
null:
description: >
{/name=Name} / {/initialen=Initialen} / {/keine-pronomen=keine Pronomen} / {/vermeiden=Pronomen vermeiden}
history: >
Einige Leute ziehen es vor, keine Pronomen zu verwenden.
Stattdessen wird der Name oder der Initiale verwendet
oder Sätze so umformuliert, dass diese keine geschlechtsspezifischen Pronomen enthalten.
routes: ['name', 'initialen', 'keine-pronomen', 'vermeiden']
morphemes:
pronoun_n: '#'
pronoun_d: '#'
@ -144,7 +139,6 @@ pronouns:
possessive_pronoun_pl_d: '#/[sxzß]$/i#|#s'
adverb_because: 'wegen #'
examples: [':Andrea', ':A', ':S']
routes: ['name', 'initialen', 'keine-pronomen', 'vermeiden']
ideas:
-
header: 'Name oder Initialen anstelle von Pronomen'

View File

@ -77,11 +77,20 @@ pronouns:
header: 'Austauschbare Formen'
raw: 'austauschbar'
null:
header:
name: 'Name'
initialen: 'Initialen'
keine-pronomen: 'keine Pronomen'
vermeiden: 'Pronomen vermeiden'
short:
name: 'Name'
initialen: 'Initialen'
keine-pronomen: 'keine Pronomen'
vermeiden: 'Pronomen vermeiden'
description: >
Einige Leute ziehen es vor, keine Pronomen zu verwenden.
Stattdessen wird der Name oder der Initiale verwendet
oder Sätze so umformuliert, dass diese keine geschlechtsspezifischen Pronomen enthalten.
button: 'Generiere einen Link mit einem Namen oder einer Initiale'
base: 'Name oder Initiale'
emoji:

View File

@ -38,14 +38,7 @@ pronouns:
Some like when others alternate between those forms when talking about them.
examples: ['he&she', 'he&they', 'she&they']
null:
description: >
{/avoiding=Avoiding gendered forms} / {/no-pronouns=no pronouns} / {/null=null pronouns} /
{/pronounless=pronounless} / {/nullpronominal=nullpronominal} / {/nameself=nameself}
history: >
Some people prefer not using any pronouns, instead being referred by name, initial,
omitting pronouns with passive voice, or restructuring the sentence.
See: {https://web.archive.org/web/20211214214513/https://twitter.com/lypomania/status/1290274971642544128=lypomania's guide},
{https://web.archive.org/web/20230326021305/https://www.lgbtqia.wiki/wiki/Nullpronominal=Nullpronominal on LGBTA Wiki}
routes: ['avoiding', 'no-pronouns', 'null', 'pronounless', 'nullpronominal', 'nameself']
morphemes:
pronoun_subject: '#'
pronoun_object: '#'
@ -53,7 +46,6 @@ pronouns:
possessive_pronoun: '#''s'
reflexive: '#self'
examples: [':Andrea', ':S']
routes: ['avoiding', 'no-pronouns', 'null', 'pronounless', 'nullpronominal', 'nameself']
ideas:
-
header: 'Use names or initials instead of pronouns'

View File

@ -76,8 +76,18 @@ pronouns:
header: 'Interchangeable forms'
raw: 'interchangeable'
null:
header:
avoiding: 'Avoiding gendered forms'
no-pronouns: 'no pronouns'
null: 'null pronouns'
short:
no-pronouns: 'no pronouns'
null: 'null pronouns'
description: >
Some people prefer not using any pronouns, instead being referred by name, initial,
omitting pronouns with passive voice, or restructuring the sentence.
See: {https://web.archive.org/web/20211214214513/https://twitter.com/lypomania/status/1290274971642544128=lypomania's guide},
{https://web.archive.org/web/20230326021305/https://www.lgbtqia.wiki/wiki/Nullpronominal=Nullpronominal on LGBTA Wiki}
button: 'Generate a link with a name or initial'
base: 'Name or initial'
emoji:

View File

@ -20,19 +20,11 @@ pronouns:
Some like when others alternate between those forms when talking about them.
examples: ['li&ŝi', 'li&ĝi', 'ĝi&ri']
null:
description: >
{/avoiding=Avoiding gendered forms} / {/no-pronouns=no pronouns} / {/null=null pronouns} /
{/pronounless=pronounless} / {/nullpronominal=nullpronominal} / {/nameself=nameself}
history: >
Some people prefer not using any pronouns, instead being referred by name, initial,
omitting pronouns with passive voice, or restructuring the sentence.
See: {https://web.archive.org/web/20211214214513/https://twitter.com/lypomania/status/1290274971642544128=lypomania's guide},
{https://web.archive.org/web/20230326021305/https://www.lgbtqia.wiki/wiki/Nullpronominal=Nullpronominal on LGBTA Wiki}
routes: ['avoiding']
morphemes:
pronoun_n: '#'
pronoun_a: '#'
examples: [':Andrea', ':S']
routes: ['avoiding']
ideas:
-
header: 'Use names or initials instead of pronouns'

View File

@ -17,11 +17,6 @@ pronouns:
description: 'Muchas personas no binarias usan más de una forma intercambiablemente y se sienten cómodas con cualquiera de las opciones.'
examples: ['él&ella', 'él&elle', 'ella&elle']
null:
description: >
{/evitar=Evitar formas de género}
history: >
Algunas personas prefieren no usar pronombres, sino ser referidas por su nombre, por su inicial,
omitir oraciones en voz activa o reestructurar la frase.
routes: ['evitar']
ideas:
-

View File

@ -88,6 +88,11 @@ pronouns:
sentence: >
Puedes modificar los links predeterminados para que la URL se lea como esta frase; por ejemplo:
null:
header:
evitar: 'Evitar formas de género'
description: >
Algunas personas prefieren no usar pronombres, sino ser referidas por su nombre, por su inicial,
omitir oraciones en voz activa o reestructurar la frase.
button: 'Generar un enlace con un nombre o una inicial'
base: 'Nombre o inicial'
emoji:

View File

@ -19,60 +19,7 @@ pronouns:
and are fine with being called either of them.
Some like when others alternate between those forms when talking about them.
examples: ['tema&temake', 'tema&see']
null:
description: >
{/avoiding=Avoiding gendered forms} / {/no-pronouns=no pronouns} / {/null=null pronouns} /
{/pronounless=pronounless} / {/nullpronominal=nullpronominal} / {/nameself=nameself}
history: >
Some people prefer not using any pronouns, instead being referred by name, initial,
omitting pronouns with passive voice, or restructuring the sentence.
See: {https://web.archive.org/web/20211214214513/https://twitter.com/lypomania/status/1290274971642544128=lypomania's guide},
{https://web.archive.org/web/20230326021305/https://www.lgbtqia.wiki/wiki/Nullpronominal=Nullpronominal on LGBTA Wiki}
morphemes:
pronoun_subject: '#'
pronoun_object: '#'
possessive_determiner: '#''s'
possessive_pronoun: '#''s'
reflexive: '#self'
examples: [':Andrea', ':S']
routes: ['avoiding']
ideas:
-
header: 'Use names or initials instead of pronouns'
normative: true
examples:
- ['I talked to him yesterday', 'I talked to {/:Sky=Sky} yesterday']
- ['She is really beautiful', '{/:Soph=Soph} is really beautiful']
-
- 'Her graduation starts soon'
- '{/:J=J}''s graduation starts soon'
-
header: 'Passive voice'
normative: true
examples:
- ['He answered the phone', 'The phone was answered']
-
- 'Wen takes good care of her cat'
- 'Wen''s cat is well cared for'
-
header: 'Rephrasing the sentence (circumlocution)'
normative: true
examples:
- ['Lior did it all by himself', 'Lior did it all without any help']
- ['Gael talks in his sleep', 'Gael talks while sleeping']
-
header: 'Replacing a pronoun with a descriptive noun or phrase'
normative: true
examples:
- ['She landed the plane safely', 'The pilot landed the plane safely']
- ['This is Lea, she is into painting', 'This is Lea. My friend is into painting']
- ['She argues that…', 'The person who started this discussion argues that…']
-
header: 'Dropping pronouns'
normative: true
examples:
- ['Did you buy Tex her gift?', 'Did you buy Tex a gift?']
- ['Yes, I bought it for her. I will give it to her tomorrow.', 'Yes, I bought it. I will give it tomorrow.']
null: false
emoji: false
others: 'Teised asesõnad'

View File

@ -19,70 +19,8 @@ pronouns:
and are fine with being called either of them.
Some like when others alternate between those forms when talking about them.
examples: ['he&she', 'he&they', 'she&they']
null:
description: >
{/avoiding=Avoiding gendered forms} / {/no-pronouns=no pronouns} / {/null=null pronouns} /
{/pronounless=pronounless} / {/nullpronominal=nullpronominal} / {/nameself=nameself}
history: >
Some people prefer not using any pronouns, instead being referred by name, initial,
omitting pronouns with passive voice, or restructuring the sentence.
See: {https://web.archive.org/web/20211214214513/https://twitter.com/lypomania/status/1290274971642544128=lypomania's guide},
{https://web.archive.org/web/20230326021305/https://www.lgbtqia.wiki/wiki/Nullpronominal=Nullpronominal on LGBTA Wiki}
morphemes:
pronoun_subject: '#'
pronoun_object: '#'
possessive_determiner: '#''s'
possessive_pronoun: '#''s'
reflexive: '#self'
examples: [':Andrea', ':S']
routes: ['avoiding']
ideas:
-
header: 'Use names or initials instead of pronouns'
normative: true
examples:
- ['I talked to him yesterday', 'I talked to {/:Sky=Sky} yesterday']
- ['She is really beautiful', '{/:Soph=Soph} is really beautiful']
-
- 'Her graduation starts soon'
- '{/:J=J}''s graduation starts soon'
-
header: 'Passive voice'
normative: true
examples:
- ['He answered the phone', 'The phone was answered']
-
- 'Wen takes good care of her cat'
- 'Wen''s cat is well cared for'
-
header: 'Rephrasing the sentence (circumlocution)'
normative: true
examples:
- ['Lior did it all by himself', 'Lior did it all without any help']
- ['Gael talks in his sleep', 'Gael talks while sleeping']
-
header: 'Replacing a pronoun with a descriptive noun or phrase'
normative: true
examples:
- ['She landed the plane safely', 'The pilot landed the plane safely']
- ['This is Lea, she is into painting', 'This is Lea. My friend is into painting']
- ['She argues that…', 'The person who started this discussion argues that…']
-
header: 'Dropping pronouns'
normative: true
examples:
- ['Did you buy Tex her gift?', 'Did you buy Tex a gift?']
- ['Yes, I bought it for her. I will give it to her tomorrow.', 'Yes, I bought it. I will give it tomorrow.']
emoji:
description: 'Emojiself pronouns'
history: '{https://lgbta.wikia.org/wiki/Emojiself_Pronouns=Emojiself} pronouns are intended for online communication and not supposed to be pronounced.'
morphemes:
pronoun_subject: '#'
pronoun_object: '#'
possessive_determiner: '#''s'
possessive_pronoun: '#''s'
reflexive: '#self'
examples: ['💫', '💙']
null: false
emoji: false
mirror:
route: 'mirror'
name: 'Mirror pronouns / Mirrorpronominal'

View File

@ -22,19 +22,12 @@ pronouns:
alternando fra queste.
examples: ['lui&lei', 'lui&ləi', 'lei&ləi']
null:
description: >
{/avoiding=Evitare sostantivi che siano “genderati”} / {/no-pronouns=Niente pronomi} / {/null=Nessun pronome} / {/nameself=Il proprio nome}
history: >
Alcune persone preferiscono non usare pronomi, preferiscono invece essere chiamate col proprio nome, liniziale,
omettendo pronomi con voce passiva o ristrutturando la frase.
Guarda: {https://twitter.com/lypomania/status/1290274971642544128=Guida di lypomania},
{https://lgbta.wikia.org/wiki/Nullpronominal=”Senza pronomi” su wiki LGBTQIA+}
routes: ['evitare', 'niente-pronomi', 'nessun-pronome', 'nome']
morphemes:
pronoun_n: '#'
pronoun_d: 'a(d) #'
pronoun_a: '#'
examples: [':Di', ':Andrea']
routes: ['Evitare']
ideas:
-
header: 'Usare nomi o iniziali invece che i pronomi'

View File

@ -72,6 +72,20 @@ pronouns:
header: 'forme intercambiabili'
raw: 'intercambiabile'
null:
header:
evitare: 'Evitare sostantivi che siano “genderati”'
niente-pronomi: 'Niente pronomi'
nessun-pronome: 'Nessun pronome'
nome: 'Il proprio nome'
short:
niente-pronomi: 'niente pronomi'
nessun-pronome: 'nessun pronome'
nome: 'nome'
description: >
Alcune persone preferiscono non usare pronomi, preferiscono invece essere chiamate col proprio nome,
liniziale, omettendo pronomi con voce passiva o ristrutturando la frase.
Guarda: {https://twitter.com/lypomania/status/1290274971642544128=Guida di lypomania},
{https://lgbta.wikia.org/wiki/Nullpronominal=”Senza pronomi” su wiki LGBTQIA+}
button: 'Genera un collegamento con un nome o un''iniziale'
base: 'Nome o iniziale'
emoji:

View File

@ -20,8 +20,7 @@ pronouns:
어떤 사람들은 다른 사람들이 그들에 대해 이야기할 때 이러한 형태를 번갈아 가며 사용하는 것을 좋아합니다.
examples: ['그와 그녀', '그와 그들', '그녀와 그들']
null:
description: '대명사 없음 / 무효 대명사 / 자기 이름'
history: '어떤 사람들은 대명사를 사용하지 않고 대신 이름, 이니셜 또는 수동태로 대명사를 생략하는 것을 선호합니다({https://web.archive.org/web/20230326021305/https://www.lgbtqia.wiki/wiki/Nullpronominal=널 의례적인}.'
routes: ['avoiding']
morphemes:
pronoun: '#'
examples: [':안드레아', ':S']

View File

@ -78,6 +78,12 @@ pronouns:
많은 사람들한테는 똑바로 쓴 대명사가 중요하는데,
어떤 사람들은 아무 대명사가 괜찮습니다.
options: '옵션을 확인하세요. [share]{/pronouns=here}.'
null:
header:
avoiding: '대명사 없음 / 무효 대명사 / 자기 이름'
description: >
어떤 사람들은 대명사를 사용하지 않고 대신 이름, 이니셜 또는 수동태로 대명사를 생략하는 것을 선호합니다
({https://web.archive.org/web/20230326021305/https://www.lgbtqia.wiki/wiki/Nullpronominal=널 의례적인}.
others: '기타 양식'
othersRaw: '기타'
or: '아님'

View File

@ -17,8 +17,7 @@ pronouns:
description: 'Veel non-binaire personen gebruiken afwisselend meer dan een vorm en vinden het prima dat je beiden gebruikt.'
examples: ['hij&zij', 'hij&hen', 'zij&hen']
null:
description: 'Geen voornaamwoorden'
history: 'Sommige mensen gebruiken liever geen enkel voornaamwoord, en worden liever aangesproken met hun naam, initialen, of willen liever dat je voornaamwoorden ontwijkt door passieve zinnen te maken.'
routes: ['geen-voornaamwoorden']
morphemes:
nominative: '#'
accusative: '#'

View File

@ -77,6 +77,14 @@ pronouns:
vinden anderen het geen probleem om op wat voor manier dan ook aangesproken te worden
- zolang de context duidelijk maakt wie hiermee wordt gerefereerd.
options: 'bekijk de mogelijkheden [share]{/voornaamwoorden=hier}.'
null:
header:
geen-voornaamwoorden: 'Geen voornaamwoorden'
short:
geen-voornaamwoorden: 'geen voornaamwoorden'
description: >
Sommige mensen gebruiken liever geen enkel voornaamwoord, en worden liever aangesproken met
hun naam, initialen, of willen liever dat je voornaamwoorden ontwijkt door passieve zinnen te maken.
others: 'Andere vormen'
othersRaw: 'andere'
or: 'of'

View File

@ -48,14 +48,6 @@ pronouns:
honorific: '#'
examples: ['💫', '💙', '🐿']
null:
description: '{/unikanie=Unikanie form nacechowanych płciowo}'
history: >
Niektóre osoby niebinarne preferują używanie form unikających precyzowania płci
zamiast tych nacechowanych czy to binarnie czy niebinarnie.
Warto też używać ich mówiąc o osobach, których płci nie znamy.
Choć w silnie zgenderyzowanym języku polskim jest to względnie trudne do zrobienia
i może lekko zmieniać znaczenie zdania,
to jednak wcale nie jest niemożliwe.
routes: ['unikanie']
ideas:
-

View File

@ -100,6 +100,15 @@ pronouns:
Oprócz domyślnych linków możesz też podlinkować do swoich zaimków tak,
aby URL czytało się niemal jak zdanie; na przykład:
null:
header:
unikanie: 'Unikanie form nacechowanych płciowo'
description: >
Niektóre osoby niebinarne preferują używanie form unikających precyzowania płci
zamiast tych nacechowanych czy to binarnie czy niebinarnie.
Warto też używać ich mówiąc o osobach, których płci nie znamy.
Choć w silnie zgenderyzowanym języku polskim jest to względnie trudne do zrobienia
i może lekko zmieniać znaczenie zdania,
to jednak wcale nie jest niemożliwe.
button: 'Wygeneruj link z imieniem albo inicjałem'
base: 'Imię lub inicjał'
emoji:

View File

@ -20,13 +20,7 @@ pronouns:
Unora le place când alții alternează între aceste forme.
examples: ['el&ea', 'el&ei', 'ea&ei']
null:
description: >
{/evitare=Evitarea pronumelor cu gen} / {/fara-pronume=fără pronume} / {/nume=doar nume}
history: >
Unele persoane preferă să nu folosească niciun pronume, fiind referiți după nume, după inițială sau
evitând pronumele folosind voce pasivă sau restructurând propozițiile.
Vezi: {https://web.archive.org/web/20211214214513/https://twitter.com/lypomania/status/1290274971642544128=ghidul lui lypomania (în lb. engleză)},
{https://web.archive.org/web/20230326021305/https://www.lgbtqia.wiki/wiki/Nullpronominal=Nul-pronominal pe LGBTA Wiki (în lb. engleză)}
routes: ['evitare', 'fara-pronume', 'nume']
morphemes:
pronoun_n: '#'
pronoun_dg: 'lui #'
@ -34,7 +28,6 @@ pronouns:
first_article: 'L-am'
second_article: ''
examples: [':Andrea', ':S']
routes: ['evitare', 'fara-pronume', 'nume']
ideas:
-
header: 'Folosește nume sau inițiale în loc de pronume'

View File

@ -79,6 +79,18 @@ pronouns:
pentru ei, pe alții nu îi deranjează să le fie adresați în orice fel atâta timp cât îți poți da seama din
context despre cine se vorbește.
options: 'vezi pronumele [share]{/pronume=aici}.'
null:
header:
evitare: 'Evitarea pronumelor cu gen'
fara-pronume: 'fără pronume'
nume: 'doar nume'
short:
fara-pronume: 'fără pronume'
description: >
Unele persoane preferă să nu folosească niciun pronume, fiind referiți după nume, după inițială sau
evitând pronumele folosind voce pasivă sau restructurând propozițiile.
Vezi: {https://web.archive.org/web/20211214214513/https://twitter.com/lypomania/status/1290274971642544128=ghidul lui lypomania (în lb. engleză)},
{https://web.archive.org/web/20230326021305/https://www.lgbtqia.wiki/wiki/Nullpronominal=Nul-pronominal pe LGBTA Wiki (în lb. engleză)}
others: 'Alte forme'
othersRaw: 'altele'
or: 'sau'

View File

@ -30,11 +30,6 @@ pronouns:
- 'Персона В использует {/она=она/её}, так что, говоря о персоне А, она использует в её отношениии местоимения “она/её”.'
- 'Персона С использует {/зе=зе/зир} и {/фае=фае/фер}, так что, когда зе говорит о персоне А, фае может использовать к фер и фае/фер, и зе/зир.'
null:
description: '{/avoiding=Избегание гендерных форм}'
history: >
Некоторые небинарные персоны предпочитают использовать формы, в которых не указывается гендер, вместо форм с гендерными окончаниями.
Также предпочтительно использовать такие формы, говоря о персонах, чьи местоимения неизвестны.
Так как в русском языке род играет важную роль, полностью избавиться от гендерных окончаний довольно сложно, но не невозможно.
routes: ['avoiding']
ideas:
-

View File

@ -66,6 +66,12 @@ pronouns:
header: 'Заменяемые формы'
raw: 'заменяемое'
null:
header:
avoiding: 'Избегание гендерных форм'
description: >
Некоторые небинарные персоны предпочитают использовать формы, в которых не указывается гендер, вместо форм с гендерными окончаниями.
Также предпочтительно использовать такие формы, говоря о персонах, чьи местоимения неизвестны.
Так как в русском языке род играет важную роль, полностью избавиться от гендерных окончаний довольно сложно, но не невозможно.
button: 'Сгенерировать ссылку с именем или инициалами'
base: 'Имя или инициалы'
emoji:

View File

@ -31,11 +31,6 @@ pronouns:
- 'Людина В використовує {/вона=вона/її}, отже, говорячи про людину А, вона використовує до її відношенні займенник “вона/її”.'
- 'Людина С використовує {/зі=зі/зір} та {/фае=фае/фер}, отже, коли зі говорить про людину А, фае може використовувати до фер і фае/фер, і зе/зір.'
null:
description: '{/avoiding=Уникнення ґендерних форм}'
history: >
Деякі небінарні люди воліють використовувати форми, в яких не вказується ґендер замість форм з ґендерними закінченнями.
Також важливо використовувати такі форми, говорячи про людей, чиї займенники невідомі.
Так як у українській мові рід грає важливу роль, повністю позбавиться ґендерних закінчень досить складно, але не неможливо.
routes: ['avoiding']
ideas:
-

View File

@ -78,6 +78,12 @@ pronouns:
or: 'або'
grammarTable: 'Таблиця'
null:
header:
avoiding: 'Уникнення ґендерних форм'
description: >
Деякі небінарні люди воліють використовувати форми, в яких не вказується ґендер замість форм з ґендерними закінченнями.
Також важливо використовувати такі форми, говорячи про людей, чиї займенники невідомі.
Так як у українській мові рід грає важливу роль, повністю позбавиться ґендерних закінчень досить складно, але не неможливо.
button: 'Згенерувати посилання з іменем або ініціалом'
base: 'Імʼя або ініціал'
emoji:

View File

@ -20,16 +20,11 @@ pronouns:
Một vài người lại muốn người khác dùng đan xen các danh xưng khi đang nói về họ.
examples: ['anh&cô', 'anh&họ', 'cô&họ']
null:
description: >
{/tránh=Tránh các từ chỉ giới tính} / {/bỏ-danh-xưng=không dùng danh xưng} / {/tên=danh xưng là tên}
history: >
Có người không muốn dùng bất cứ danh xưng nào, thay vào đó là tên, tên viết tắt/kí hiệu,
việc lược bỏ danh xưng bằng cấu trúc câu bị động, hoặc thay đổi cấu trúc câu nói.
routes: ['tránh', 'bỏ-danh-xưng', 'tên']
morphemes:
3rd_person: '#'
2nd_person: '#'
examples: [':Vanh', ':A']
routes: ['tránh', 'bỏ-danh-xưng', 'tên']
ideas:
-
header: 'Dùng tên hoặc kí hiệu thay cho danh xưng'

View File

@ -75,6 +75,15 @@ pronouns:
header: 'Danh xưng hoán đổi'
raw: 'hoán đổi'
null:
header:
tránh: 'Tránh các từ chỉ giới tính'
bỏ-danh-xưng: 'không dùng danh xưng'
tên: 'danh xưng là tên'
short:
bỏ-danh-xưng: 'bỏ danh xưng'
description: >
Có người không muốn dùng bất cứ danh xưng nào, thay vào đó là tên, tên viết tắt/kí hiệu,
việc lược bỏ danh xưng bằng cấu trúc câu bị động, hoặc thay đổi cấu trúc câu nói.
button: 'Tạo đường link với tên hoặc tên viết tắt'
base: 'Tên hoặc tên viết tắt'
generated: 'Những danh xưng này đã được tạo bằng trình tạo danh xưng. Tập thể pronouns.page không chịu bất kì trách nhiệm gì về những danh xưng này.'

View File

@ -17,10 +17,7 @@ pronouns:
name: 'Interchangeable forms'
description: 'Many nonbinary people use more than one form interchangeably and are fine with being called either of them.'
examples: ['ער&זי', 'זי&זיי', 'ער&זיי']
null:
description: 'No pronouns / null pronouns / pronounless / nameself'
history: 'Some people prefer not using any pronouns, instead being referred by name, initial, or by omitting pronouns with passive voice, see: {https://web.archive.org/web/20230326021305/https://www.lgbtqia.wiki/wiki/Nullpronominal=nullpronominal}.'
examples: [':Andrea', ':S']
null: false
emoji: false
others: 'Other pronouns'

View File

@ -156,6 +156,7 @@
</p>
</div>
<OpinionListInput v-model="formData.pronouns" :validation="validatePronoun" :custom-opinions="formData.opinions" :maxitems="128" :maxlength="192" />
<PronounsIndex />
</template>
<template #description-header>
@ -382,6 +383,7 @@ import { buildList, isValidLink } from '~/src/helpers.ts';
import { pronouns, pronounLibrary } from '~/src/data.ts';
import { buildPronoun, buildPronounUsage } from '~/src/buildPronoun.ts';
import { birthdateRange, formatDate, parseDate } from '~/src/birthdate.ts';
import { addPronounInjectionKey } from '~/src/injectionKeys.ts';
import opinions from '~/src/opinions.ts';
import { buildCalendar } from '~/src/calendar/calendar.ts';
import type { Config } from '~/locale/config.ts';
@ -564,6 +566,10 @@ export default defineComponent({
const { isDark } = useDark();
const { recommendedLinkProviders } = useLinkUtils();
provide(addPronounInjectionKey, (pronoun) => {
formData.value.pronouns.push({ value: pronoun, opinion: 'meh' });
});
let profilesData;
if (store.user) {
profilesData = (await useFetch<{

View File

@ -1,9 +1,9 @@
<script setup lang="ts">
import { buildAnyPronounsList } from '~/src/buildPronoun.ts';
import { ExampleCategory } from '~/src/classes.ts';
import { examples, pronouns, pronounLibrary } from '~/src/data.ts';
import type { Pronoun, MergedPronounGroup } from '~/src/classes.ts';
definePageMeta({
translatedPaths: (config) => {
if (!config.pronouns.enabled || !config.pronouns.any) {
@ -22,7 +22,6 @@ definePageMeta({
const { $translator: translator } = useNuxtApp();
const route = useRoute();
const router = useRouter();
const config = useConfig();
const groupKey = route.params.group as string;
@ -44,24 +43,8 @@ useSimpleHead({
}, translator);
const comprehensive = computed({
get() {
return config.pronouns.comprehensive ? Object.hasOwn(route.query, config.pronouns.comprehensive) : false;
},
set(value) {
if (value === comprehensive.value || !config.pronouns.comprehensive) {
// prevent warning that $router.replace has no effect
return;
}
const query = structuredClone(route.query);
if (value) {
query[config.pronouns.comprehensive] = null;
} else {
delete query[config.pronouns.comprehensive];
}
router.replace({ query });
},
});
const comprehensive = useComprehensive();
const pronounsChoice = computed(() => {
if (!pronounGroups.length) {
return Object.values(pronouns).filter((p) => !p.hidden);
@ -73,6 +56,8 @@ const pronounsChoice = computed(() => {
}
return Object.values(choice).filter((p) => !p.hidden);
});
const anyPronouns = buildAnyPronounsList(config, pronounLibrary);
</script>
<template>
@ -97,12 +82,6 @@ const pronounsChoice = computed(() => {
<Spelling :text="short" />
</strong>
</h2>
<p class="h6 small text-center mb-0 mt-2">
<em>
<T>pronouns.any.description</T>
(<T>pronouns.any.options</T>)
</em>
</p>
</div>
</section>
@ -124,14 +103,25 @@ const pronounsChoice = computed(() => {
</ul>
</section>
<section>
<div class="alert alert-light">
<Icon v="info-circle" />
<T>pronouns.any.description</T>
<em>(<T>pronouns.any.options</T>)</em>
</div>
</section>
<PronounGroup
v-for="pronounGroup in pronounGroups"
:key="pronounGroup.group.name"
:pronoun-group="pronounGroup"
:pronoun-group="pronounGroup.group"
:pronouns="pronounGroup.groupPronouns"
/>
<AdPlaceholder :phkey="['content-0', 'content-mobile-0']" />
<PronounGroup :pronouns="anyPronouns" />
<section>
<Share :title="`${$t('pronouns.intro')}: ${$t('pronouns.any.short')}`" />
</section>

View File

@ -1,4 +1,6 @@
<script setup lang="ts">
import { shortForVariant } from '~/src/buildPronoun.ts';
definePageMeta({
translatedPaths: (config) => {
if (!config.pronouns.enabled || !config.pronouns.ask) {
@ -15,10 +17,10 @@ if (!config.pronouns.enabled || !config.pronouns.ask) {
const { $translator: translator } = useNuxtApp();
const route = useRoute();
const variant = route.meta.variant;
const variant = route.meta.variant as string;
useSimpleHead({
title: translator.translate(`pronouns.ask.header.${variant}`),
title: `${translator.translate('pronouns.intro')}: ${shortForVariant('ask', variant, translator)}`,
description: translator.translate('pronouns.ask.description'),
banner: `api/banner/${variant}.png`,
}, translator);
@ -34,13 +36,9 @@ useSimpleHead({
<section>
<div class="alert alert-primary">
<h2 class="text-center mb-0">
<strong><T>pronouns.ask.header.{{ variant }}</T></strong>
<strong><Spelling :text="shortForVariant('ask', variant, translator)" /></strong>
</h2>
</div>
<div class="alert alert-light">
<Icon v="info-circle" />
<T>pronouns.ask.description</T>
</div>
</section>
<section>
@ -55,27 +53,19 @@ useSimpleHead({
</ul>
</section>
<AdPlaceholder :phkey="['content-0', 'content-mobile-0']" />
<section>
<ul class="list-group mt-4">
<li class="list-group-item">
<ul>
<li v-for="otherVariant in config.pronouns.ask.routes" :key="otherVariant">
<nuxt-link :to="{ name: `pronouns-${otherVariant}` }">
<strong><T>pronouns.ask.short.{{ otherVariant }}</T></strong>
</nuxt-link>
</li>
</ul>
</li>
<nuxt-link :to="{ name: 'pronouns' }" class="list-group-item list-group-item-action text-center">
<Icon v="ellipsis-h-alt" />
</nuxt-link>
</ul>
<div class="alert alert-light">
<Icon v="info-circle" />
<T>pronouns.ask.description</T>
</div>
</section>
<AdPlaceholder :phkey="['content-0', 'content-mobile-0']" />
<PronounGroup :pronouns="config.pronouns.ask.routes" />
<section>
<Share :title="`${$t('pronouns.intro')}: ${$t('pronouns.ask.description')}`" />
<Share :title="`${$t('pronouns.intro')}: ${shortForVariant('ask', variant, translator)}`" />
</section>
<Separator icon="info" />

View File

@ -1,53 +1,82 @@
<script setup lang="ts">
import { useNuxtApp } from 'nuxt/app';
import useConfig from '~/composables/useConfig.ts';
import useSimpleHead from '~/composables/useSimpleHead.ts';
import { shortForVariant } from '~/src/buildPronoun.ts';
definePageMeta({
translatedPaths: (config) => {
if (!config.pronouns.enabled || !config.pronouns.null || !config.pronouns.null.routes) {
return [];
}
return withPronounPrefixesWithVariant(config, config.pronouns.null.routes);
},
});
const { $translator: translator } = useNuxtApp();
const route = useRoute();
const variant = route.meta.variant as string;
const config = useConfig();
if (!config.pronouns.enabled || !config.pronouns.null) {
throw new Error('config.pronouns.null is disabled');
}
useSimpleHead({
title: `${translator.translate('pronouns.intro')}: ${shortForVariant('null', variant, translator)}`,
description: translator.translate('pronouns.null.description'),
banner: `api/banner${route.path.replace(/\/$/, '')}.png`,
}, translator);
</script>
<template>
<Page>
<Page v-if="config.pronouns.enabled && config.pronouns.null && config.pronouns.null.routes">
<h2>
{{ clearLinkedText(config.pronouns.null.description) }}
<Icon v="tag" />
<T>pronouns.intro</T><T>quotation.colon</T>
</h2>
<p>
<LinkedText :text="config.pronouns.null.history" />
</p>
<section>
<div class="alert alert-primary">
<h2 class="text-center mb-0">
<strong>
<Spelling :text="shortForVariant('null', variant, translator)" />
</strong>
</h2>
</div>
</section>
<section>
<div class="alert alert-light">
<Icon v="info-circle" />
<T>pronouns.null.description</T>
</div>
</section>
<AdPlaceholder :phkey="['content-0', 'content-mobile-0']" />
<section>
<PronounsNullGenerator />
</section>
<Avoiding />
<PronounGroup :pronouns="config.pronouns.null.routes" />
<section>
<Share :title="`${$t('pronouns.intro')}: ${shortForVariant('null', variant, translator)}`" />
</section>
<AdPlaceholder :phkey="['content-1', 'content-mobile-1']" />
<Separator icon="info" />
<section class="mb-0">
<h2 class="h4">
<Icon v="info-circle" />
<T>home.whatisit</T>
</h2>
<T>home.about</T>
<Homepage align="center" />
</section>
</Page>
</template>
<script>
import { useNuxtApp } from 'nuxt/app';
import { clearLinkedText } from '~/src/helpers.ts';
import useConfig from '~/composables/useConfig.ts';
import useSimpleHead from '~/composables/useSimpleHead.ts';
export default {
setup() {
definePageMeta({
translatedPaths: (config) => {
if (!config.pronouns.enabled || !config.pronouns.null || !config.pronouns.null.routes) {
return [];
}
const nullPaths = config.pronouns.null.routes.map((route) => `/${encodeURIComponent(route)}`);
return withPronounPrefixes(config, nullPaths);
},
});
const { $translator: translator } = useNuxtApp();
const route = useRoute();
const config = useConfig();
useSimpleHead({
title: config.pronouns.null.description,
description: config.pronouns.null.history,
banner: `api/banner${route.path.replace(/\/$/, '')}.png`,
}, translator);
return {
config,
clearLinkedText,
};
},
};
</script>

View File

@ -1,3 +1,14 @@
<script setup lang="ts">
import Suggested from '~/data/pronouns/Suggested.vue';
import useConfig from '~/composables/useConfig.ts';
definePageMeta({
translatedPaths: (config) => translatedPathByConfigModule(config.pronouns),
});
const config = useConfig();
</script>
<template>
<Page v-if="config.pronouns.enabled">
<section>
@ -21,318 +32,7 @@
<AdPlaceholder :phkey="['content-0', 'content-mobile-0']" />
<ul class="list-group mt-4">
<template v-for="[group, groupPronouns] in pronounLibrary.split()">
<li v-if="!group.hidden" class="list-group-item">
<p class="h5">
<Spelling :text="group.name" />
</p>
<p v-if="group.description" class="small my-1">
<Icon v="info-circle" />
<LinkedText :text="group.description" />
</p>
<SimplePronounList :pronouns="groupPronouns" />
</li>
</template>
<li v-if="config.pronouns.generator.enabled" id="generator" class="list-group-item">
<p class="h5">
<T>home.generator.header</T>
</p>
<p>
<T>home.generator.description</T>
</p>
<a v-if="!customise" href="#" class="btn btn-outline-primary w-100" @click.prevent="customise = true">
<Icon v="sliders-h-square" />
<T>home.generator.button</T>
</a>
<div v-else class="card mb-5">
<div class="card-header">
<Icon v="sliders-h-square" />
<T>home.generator.header2</T>
</div>
<div class="card-body">
<div class="card-title border-bottom pb-3">
<p><strong><T>home.generator.base</T><T>quotation.colon</T></strong></p>
<ul class="list-unstyled">
<template v-for="[group, groupPronouns] in pronounLibrary.split()">
<li v-if="!group.hidden">
<ul class="list-inline">
<li class="list-inline-item">
<Spelling :text="group.name" />
</li>
<template v-for="pronoun in deduplicatePronounGroup(groupPronouns)">
<li v-if="!pronoun.hidden" class="list-inline-item">
<button
:class="['btn', pronoun.name(glue) === selectedPronoun.name(glue) ? 'btn-primary' : 'btn-outline-primary', 'btn-sm', 'my-1']"
@click="selectedPronoun = pronoun.clone(true)"
>
<Spelling :text="pronoun.name(glue)" />
</button>
</li>
</template>
</ul>
</li>
</template>
</ul>
</div>
<div class="alert alert-primary">
<p class="h3 mb-0 text-center">
<Spelling escape :text="selectedPronoun.name(glue)" />
<template v-if="config.pronouns.generator.description ?? true">
<br>
<input
v-model="selectedPronoun.description"
class="form-control form-input p-0 form-control-sm"
:size="selectedPronoun.description.length ? selectedPronoun.description.length + 3 : 16"
:maxlength="DESCRIPTION_MAXLENGTH"
:placeholder="$t('profile.description')"
>
</template>
</p>
</div>
<p>
<T>pronouns.examples.header</T><T>quotation.colon</T>
</p>
<template v-for="{ examples, isHonorific } in examplesByHonorific">
<ul>
<li v-for="example in examples">
<template v-for="part in clearExampleParts(example.parts(selectedPronoun))">
<input
v-if="part.variable"
v-model="selectedPronoun.morphemes[part.str]"
:class="['form-control form-input p-0', { active: selectedMorpheme === part.str }]"
:size="selectedPronoun.morphemes[part.str]?.length ?? 0"
maxlength="24"
@focus="selectedMorpheme = part.str"
@blur="selectedMorpheme = ''"
>
<span v-else><Spelling :text="part.str" /></span>
</template>
</li>
</ul>
<div v-if="config.pronouns.plurals" class="my-3">
<div v-if="isHonorific" class="custom-control custom-switch">
<input
id="pluralHonorific"
v-model="selectedPronoun.pluralHonorific[0]"
type="checkbox"
class="custom-control-input"
>
<label class="custom-control-label" for="pluralHonorific">
<T>pronouns.plural</T>
<Icon v="level-up" />
</label>
</div>
<div v-else class="custom-control custom-switch">
<input
id="plural"
v-model="selectedPronoun.plural[0]"
type="checkbox"
class="custom-control-input"
>
<label class="custom-control-label" for="plural">
<T>pronouns.plural</T>
<Icon v="level-up" />
</label>
</div>
</div>
</template>
<p class="small">
<T icon="info-circle">home.generator.alt</T>
</p>
<!-- TODO #136
<p class="small" v-if="config.pronunciation.enabled && $te('home.generator.pronunciation')">
<Icon v="info-circle"/>
<T>home.generator.pronunciation</T>
</p>
-->
</div>
<div v-if="link" class="card-footer">
<LinkInput :link="link" />
<div
v-if="!usedBaseEquals && (config.pronouns.generator.disclaimer ?? true)"
class="alert alert-warning"
>
<Icon v="exclamation-triangle" />
<T>pronouns.generated</T>
</div>
</div>
</div>
</li>
<li id="multiple" class="list-group-item">
<p class="h5">
<Spelling :text="config.pronouns.multiple.name" />
</p>
<p v-if="config.pronouns.multiple.description" class="small my-1">
<Icon v="info-circle" />
<LinkedText :text="config.pronouns.multiple.description" />
</p>
<SimplePronounList :pronouns="config.pronouns.multiple.examples" class="mb-3" />
<a v-if="!customiseMultiple" href="#" class="btn btn-outline-primary w-100" @click.prevent="customiseMultiple = true">
<Icon v="sliders-h-square" />
<T>pronouns.alt.button</T>
</a>
<div v-else class="card">
<div class="card-header">
<Icon v="sliders-h-square" />
<T>pronouns.alt.button</T><T>quotation.colon</T>
</div>
<div class="card-body">
<div class="card-title">
<ul class="list-inline d-inline mb-0">
<template v-for="(pronoun, pronounName) in pronouns">
<li v-if="!pronoun.hidden" class="list-inline-item">
<button
:class="['btn', multiple.includes(pronounName) ? 'btn-primary' : 'btn-outline-primary', 'btn-sm', 'my-1']"
@click="toggleMultiple(pronounName)"
>
<Spelling :text="pronoun.name('')" />
</button>
</li>
</template>
</ul>
</div>
</div>
<div v-if="linkMultiple" class="card-footer">
<LinkInput :link="linkMultiple" />
</div>
</div>
</li>
<li v-if="config.pronouns.null !== false" id="nameself" class="list-group-item">
<p class="h5">
<LinkedText :text="config.pronouns.null.description" />
<NormativeBadge />
</p>
<p v-if="config.pronouns.null.history" class="small my-1">
<Icon v="info-circle" />
<LinkedText :text="config.pronouns.null.history" />
</p>
<SimplePronounList :pronouns="config.pronouns.null.examples ?? []" class="mb-3" />
<button
v-if="!customiseNullPronouns"
type="button"
class="btn btn-outline-primary w-100"
@click.prevent="customiseNullPronouns = true"
>
<Icon v="sliders-h-square" />
<T>pronouns.null.button</T>
</button>
<div v-else class="card">
<div class="card-header">
<Icon v="sliders-h-square" />
<T>pronouns.null.button</T><T>quotation.colon</T>
</div>
<div class="card-body">
<div class="form-group">
<label for="nullPronounsBase"><T>pronouns.null.base</T></label>
<input
id="nullPronounsBase"
v-model="nullPronounsBase"
class="form-control"
:maxlength="NULL_PRONOUNS_MAXLENGTH"
>
</div>
</div>
<div v-if="nullPronounsLink" class="card-footer">
<LinkInput :link="nullPronounsLink" />
<div v-if="config.pronouns.generator.disclaimer ?? true" class="alert alert-warning">
<Icon v="exclamation-triangle" />
<T>pronouns.generated</T>
</div>
</div>
</div>
</li>
<li v-if="config.pronouns.emoji !== false" class="list-group-item">
<p class="h5">
<Spelling :text="config.pronouns.emoji.description" />
</p>
<div v-if="config.pronouns.emoji.history" class="small my-1">
<Icon v="info-circle" />
<LinkedText :text="config.pronouns.emoji.history" />
</div>
<SimplePronounList :pronouns="config.pronouns.emoji.examples" class="mb-3" />
<button
v-if="!customiseEmojiPronouns"
type="button"
class="btn btn-outline-primary w-100"
@click.prevent="customiseEmojiPronouns = true"
>
<Icon v="sliders-h-square" />
<T>pronouns.emoji.button</T>
</button>
<div v-else class="card">
<div class="card-header">
<Icon v="sliders-h-square" />
<T>pronouns.emoji.button</T><T>quotation.colon</T>
</div>
<div class="card-body">
<div class="form-group">
<label for="emojiPronounsBase"><T>pronouns.emoji.base</T></label>
<input
id="emojiPronounsBase"
v-model="emojiPronounsBase"
class="form-control"
>
</div>
</div>
<div v-if="emojiPronounsLink" class="card-footer">
<LinkInput :link="emojiPronounsLink" />
<div v-if="config.pronouns.generator.disclaimer ?? true" class="alert alert-warning">
<Icon v="exclamation-triangle" />
<T>pronouns.generated</T>
</div>
</div>
</div>
</li>
<li v-if="config.pronouns.mirror" id="mirror" class="list-group-item">
<p class="h5">
<nuxt-link :to="{ name: 'pronouns-mirror' }">
<LinkedText :text="config.pronouns.mirror.name" />
</nuxt-link>
</p>
<p v-if="config.pronouns.mirror.description" class="small my-1">
<Icon v="info-circle" />
<LinkedText :text="config.pronouns.mirror.description" />
</p>
</li>
<li v-if="config.pronouns.any" class="list-group-item">
<p class="h5">
<nuxt-link :to="{ name: 'pronouns-any' }">
<T>pronouns.any.header</T>
</nuxt-link>
</p>
<p class="small my-1">
<Icon v="info-circle" />
<T>pronouns.any.description</T>
</p>
<ul v-if="Object.keys(pronounLibrary.byKey()).length" class="small">
<li>
<nuxt-link :to="{ name: 'pronouns-any' }">
<T>pronouns.any.short</T>
</nuxt-link>
</li>
<li v-for="(merged, key) in pronounLibrary.byKey()" :key="key">
<nuxt-link :to="{ name: 'pronouns-any-group', params: { group: key } }">
<Spelling :text="merged.short($translator)" />
</nuxt-link>
</li>
</ul>
</li>
<li v-if="config.pronouns.ask" class="list-group-item">
<p class="h5">
<template v-for="(variant, i) in config.pronouns.ask.routes" :key="variant">
{{ i === 0 ? '' : ' / ' }}
<nuxt-link :to="{ name: `pronouns-${variant}` }">
<T>pronouns.ask.header.{{ variant }}</T>
</nuxt-link>
</template>
</p>
<p class="small my-1">
<Icon v="info-circle" />
<T>pronouns.ask.description</T>
</p>
</li>
</ul>
<PronounsIndex examples />
</section>
<AdPlaceholder :phkey="['content-1', 'content-mobile-1']" />
@ -349,171 +49,6 @@
</Page>
</template>
<script lang="ts">
import { useNuxtApp } from 'nuxt/app';
import { defineComponent, ref } from 'vue';
import { examples, pronouns, pronounLibrary } from '~/src/data.ts';
import { NULL_PRONOUNS_MAXLENGTH } from '~/src/buildPronoun.ts';
import { ExampleCategory, ExamplePart, Pronoun } from '~/src/classes.ts';
import { isEmoji } from '~/src/helpers.ts';
import Compressor from '~/src/compressor.ts';
import MORPHEMES from '~/data/pronouns/morphemes.ts';
import { mapState } from 'pinia';
import Suggested from '~/data/pronouns/Suggested.vue';
import useConfig from '~/composables/useConfig.ts';
import { useMainStore } from '~/store/index.ts';
export default defineComponent({
components: { Suggested },
setup() {
definePageMeta({
translatedPaths: (config) => translatedPathByConfigModule(config.pronouns),
});
const { $translator: translator } = useNuxtApp();
const config = useConfig();
if (!config.pronouns.enabled) {
throw null;
}
const exampleCategories = ExampleCategory.from(examples, config);
const examplesByHonorific = [false, true].map((isHonorific) => {
const examples = exampleCategories
.filter((exampleCategory) => !exampleCategory.comprehensive)
.map((exampleCategory) => exampleCategory.examples[0])
.filter((example) => example.isHonorific === isHonorific);
return { examples, isHonorific };
}).filter(({ examples }) => examples.length > 0);
return {
config,
examplesByHonorific,
pronouns,
pronounLibrary,
selectedPronoun: ref(pronouns[config.pronouns.default].clone(true)),
selectedMorpheme: ref(''),
customiseMultiple: ref(false),
multiple: ref(config.pronouns.multiple ? config.pronouns.multiple.examples[0].split('&') : []),
customise: ref(config.pronouns.generator.autoOpen ?? false),
customiseNullPronouns: ref(false),
nullPronounsBase: ref(''),
customiseEmojiPronouns: false,
emojiPronounsBase: ref(''),
glue: ` ${translator.translate('pronouns.or')} `,
DESCRIPTION_MAXLENGTH: Pronoun.DESCRIPTION_MAXLENGTH,
NULL_PRONOUNS_MAXLENGTH,
};
},
computed: {
...mapState(useMainStore, [
'user',
]),
usedBase(): string | null {
const name = this.selectedPronoun.name(this.glue);
for (const key in this.pronouns) {
if (this.pronouns.hasOwnProperty(key)) {
if (key === name) {
return key;
}
for (const alias of this.pronouns[key].aliases) {
if (alias === name) {
return key;
}
}
}
}
return null;
},
usedBaseEquals(): boolean {
return !!this.usedBase && this.selectedPronoun.equals(this.pronouns[this.usedBase], true);
},
longLink(): string {
const base = this.pronouns[this.selectedPronoun.morphemes[MORPHEMES[0]]!];
return base
? Compressor.compress(
this.selectedPronoun.toArray().map((x) => x.split('|')[0]),
base.toArray().map((x) => x.split('|')[0]),
).join(',')
: this.selectedPronoun.toString();
},
link(): string | null {
if (!this.selectedPronoun.pronoun()) {
return null;
}
const slashes = this.selectedPronoun.toStringSlashes(this.$translator);
let link;
if (this.usedBaseEquals) {
link = this.usedBase;
} else if (slashes) {
link = slashes;
} else {
link = this.longLink;
}
return this.addSlash(`${this.$config.public.baseUrl + (this.config.pronouns.prefix || '')}/${link}`);
},
linkMultiple(): string | null {
if (!this.multiple.length) {
return null;
}
return this.addSlash(`${this.$config.public.baseUrl + (this.config.pronouns.prefix || '')}/${this.multiple.join('&')}`);
},
nullPronounsLink(): string | null {
if (!this.nullPronounsBase) {
return null;
}
return `${this.$config.public.baseUrl + (this.config.pronouns.prefix || '')}/:${this.nullPronounsBase}`;
},
emojiPronounsLink(): string | null {
if (!isEmoji(this.emojiPronounsBase)) {
return null;
}
return `${this.$config.public.baseUrl + (this.config.pronouns.prefix || '')}/${this.emojiPronounsBase}`;
},
},
methods: {
toggleMultiple(name: string): void {
const index = this.multiple.indexOf(name);
if (index > -1) {
this.multiple.splice(index, 1);
} else {
this.multiple.push(name);
}
},
addSlash(link: string): string {
return link + (['*', '\''].includes(link.substr(link.length - 1)) ? '/' : '');
},
clearExampleParts(parts: ExamplePart[]): ExamplePart[] {
return parts.map((p) => new ExamplePart(p.variable, p.str.replace(/^'/, '')));
},
deduplicatePronounGroup(pronounGroup: Pronoun[]): Pronoun[] {
const dict: Record<string, Pronoun> = {};
for (const pronoun of pronounGroup) {
if (dict.hasOwnProperty(pronoun.name(this.glue))) {
continue;
}
dict[pronoun.name(this.glue)] = pronoun;
}
return Object.values(dict);
},
},
});
</script>
<style lang="scss">
@import "../assets/variables";

View File

@ -1,6 +1,4 @@
<script setup lang="ts">
import { clearLinkedText } from '~/src/helpers.ts';
definePageMeta({
translatedPaths: (config) => {
if (!config.pronouns.enabled || !config.pronouns.mirror) {
@ -18,7 +16,7 @@ if (!config.pronouns.enabled || !config.pronouns.mirror) {
const { $translator: translator } = useNuxtApp();
const route = useRoute();
useSimpleHead({
title: config.pronouns.mirror.name,
title: `${translator.translate('pronouns.intro')}: ${config.pronouns.mirror.name}`,
description: config.pronouns.mirror.description,
banner: `api/banner${route.path.replace(/\/$/, '')}.png`,
}, translator);
@ -27,19 +25,51 @@ useSimpleHead({
<template>
<Page v-if="config.pronouns.enabled && config.pronouns.mirror">
<h2>
{{ clearLinkedText(config.pronouns.mirror.name) }}
<Icon v="tag" />
<T>pronouns.intro</T><T>quotation.colon</T>
</h2>
<p>
<LinkedText :text="config.pronouns.mirror.description" />
</p>
<section>
<div class="alert alert-primary">
<h2 class="text-center mb-0">
<strong><Spelling :text="config.pronouns.mirror.name" /></strong>
</h2>
</div>
</section>
<ul>
<li v-for="(example, i) in config.pronouns.mirror.example" :key="i">
<LinkedText :text="example" />
</li>
</ul>
<section>
<h2 class="h4">
<Icon v="file-signature" />
<T>pronouns.examples.header</T><T>quotation.colon</T>
</h2>
<ul>
<li v-for="(example, i) in config.pronouns.mirror.example" :key="i">
<LinkedText :text="example" />
</li>
</ul>
</section>
<section>
<div class="alert alert-light">
<Icon v="info-circle" />
<LinkedText :text="config.pronouns.mirror.description" />
</div>
</section>
<AdPlaceholder :phkey="['content-0', 'content-mobile-0']" />
<section>
<Share :title="`${$t('pronouns.intro')}: ${config.pronouns.mirror.name}`" />
</section>
<Separator icon="info" />
<section class="mb-0">
<h2 class="h4">
<Icon v="info-circle" />
<T>home.whatisit</T>
</h2>
<T>home.about</T>
<Homepage align="center" />
</section>
</Page>
</template>

View File

@ -1,3 +1,111 @@
<script setup lang="ts">
import { examples, pronouns, pronounLibrary } from '~/src/data.ts';
import { buildPronoun } from '~/src/buildPronoun.ts';
import { useNuxtApp } from 'nuxt/app';
import useSimpleHead from '~/composables/useSimpleHead.ts';
import GrammarTables from '~/data/pronouns/GrammarTables.vue';
import LinkedText from '~/components/LinkedText.vue';
import { ExampleCategory, SourceLibrary } from '~/src/classes.ts';
import useConfig from '~/composables/useConfig.ts';
definePageMeta({
name: 'all',
path: '/:path(.*)',
});
const { $translator: translator } = useNuxtApp();
const route = useRoute();
const config = useConfig();
const key = computed(() => {
let url = route.path;
if (config.pronouns.prefix) {
if (!url.startsWith(config.pronouns.prefix)) {
return null;
}
url = url.substring(config.pronouns.prefix.length);
}
return decodeURIComponent(url.substr(1).replace(/\/$/, ''));
});
const selectedPronoun = config.pronouns.enabled && key
? buildPronoun(pronouns, key.value, config, translator)
: null;
const glue = ` ${translator.translate('pronouns.or')} `;
if (selectedPronoun) {
useSimpleHead({
title: `${translator.translate('pronouns.intro')}: ${selectedPronoun.name(glue)}`,
description: [
translator.translate('pronouns.examples.header', {}, false),
translator.translate('pronouns.grammarTable', {}, false),
translator.translate('sources.headerLong', {}, false),
].filter((x) => !!x).join(', '),
banner: `api/banner${route.path.replace(/\/$/, '')}.png`,
}, translator);
}
const { data: sources } = useFetch('/api/sources', { lazy: true });
const isNull = key.value?.startsWith(':');
const exampleCategories = ExampleCategory.from(examples, config);
const nameOptions = selectedPronoun ? selectedPronoun.nameOptions() : [];
const pronounGroup = pronounLibrary.find(selectedPronoun);
const comprehensive = useComprehensive();
const sourceLibrary = computed(() => {
if (sources.value === null) {
return null;
}
return new SourceLibrary(config, sources.value);
});
const groupedSources = computed(() => {
if (!selectedPronoun || sourceLibrary.value === null || Object.hasOwn(route.query, 'nosources')) {
return {};
}
let key = selectedPronoun.canonicalName;
if (config.sources.enabled && config.sources.mergePronouns[key] !== undefined) {
key = config.sources.mergePronouns[key];
}
return sourceLibrary.value.getForPronounExtended(key);
});
const addSlash = (link: string) => {
return link + (['*', '\''].includes(link.substr(link.length - 1)) ? '/' : '');
};
const counter = ref(0);
const counterHandle = ref<number | undefined>();
const counterSpeed = ref(3000);
const setCounterInterval = () => {
if (counterHandle.value) {
clearInterval(counterHandle.value);
}
if (counterSpeed.value > 0) {
counter.value++;
counterHandle.value = setInterval((_) => counter.value++, counterSpeed.value);
}
};
watch(counterSpeed, () => setCounterInterval());
onMounted(() => setCounterInterval());
const counterPause = () => {
counterSpeed.value = 0;
};
const counterSlow = () => {
counterSpeed.value = 3000;
};
const counterFast = () => {
counterSpeed.value = 1000;
};
</script>
<template>
<Page>
<NotFound v-if="!selectedPronoun" />
@ -28,7 +136,7 @@
<div class="alert alert-primary">
<h2 class="text-center mb-0">
<template v-if="nameOptions.length === 1">
<strong><Spelling escape :text="selectedPronoun.name(glue)" /></strong><small v-if="selectedPronoun.smallForm">/<Spelling :text="selectedPronoun.morphemes[selectedPronoun.smallForm]" /></small>
<strong><Spelling escape :text="selectedPronoun.name(glue)" /></strong><small v-if="selectedPronoun.smallForm">/<Spelling :text="selectedPronoun.morphemes[selectedPronoun.smallForm] ?? ''" /></small>
</template>
<template v-else>
<template v-for="(nameOption, i) in nameOptions">
@ -97,7 +205,11 @@
<AdPlaceholder :phkey="['content-1', 'content-mobile-1']" />
<PronounGroup :pronoun-group="pronounGroup" />
<PronounGroup
v-if="pronounGroup"
:pronoun-group="pronounGroup.group"
:pronouns="pronounGroup.groupPronouns"
/>
<Avoiding v-if="isNull" />
@ -123,147 +235,3 @@
</div>
</Page>
</template>
<script>
import { examples, pronouns, pronounLibrary } from '~/src/data.ts';
import { buildPronoun } from '~/src/buildPronoun.ts';
import { useNuxtApp } from 'nuxt/app';
import useSimpleHead from '~/composables/useSimpleHead.ts';
import GrammarTables from '~/data/pronouns/GrammarTables.vue';
import LinkedText from '~/components/LinkedText.vue';
import { ExampleCategory, SourceLibrary } from '~/src/classes.ts';
import useConfig from '~/composables/useConfig.ts';
export default {
components: { LinkedText, GrammarTables },
setup() {
definePageMeta({
name: 'all',
path: '/:path(.*)',
});
const { $translator: translator } = useNuxtApp();
const route = useRoute();
const config = useConfig();
const key = computed(() => {
let url = route.path;
if (config.pronouns.prefix) {
if (!url.startsWith(config.pronouns.prefix)) {
return null;
}
url = url.substring(config.pronouns.prefix.length);
}
return decodeURIComponent(url.substr(1).replace(/\/$/, ''));
});
const selectedPronoun = config.pronouns.enabled && key
? buildPronoun(pronouns, key.value, config, translator)
: null;
const glue = ` ${translator.translate('pronouns.or')} `;
if (selectedPronoun) {
useSimpleHead({
title: `${translator.translate('pronouns.intro')}: ${selectedPronoun.name(glue)}`,
description: [
translator.translate('pronouns.examples.header', {}, false),
translator.translate('pronouns.grammarTable', {}, false),
translator.translate('sources.headerLong', {}, false),
].filter((x) => !!x).join(', '),
banner: `api/banner${route.path.replace(/\/$/, '')}.png`,
}, translator);
}
const { data: sources } = useFetch('/api/sources', { lazy: true });
return {
config,
glue,
selectedPronoun,
isNull: key.value?.startsWith(':'),
sources,
};
},
data() {
return {
exampleCategories: ExampleCategory.from(examples, this.config),
pronouns,
nameOptions: this.selectedPronoun ? this.selectedPronoun.nameOptions() : [],
pronounGroup: pronounLibrary.find(this.selectedPronoun),
counter: 0,
counterHandle: null,
counterSpeed: 1000,
};
},
computed: {
comprehensive: {
get() {
return Object.hasOwn(this.$route.query, this.config.pronouns.comprehensive);
},
set(value) {
if (value === this.comprehensive) {
// prevent warning that $router.replace has no effect
return;
}
const query = structuredClone(this.$route.query);
if (value) {
query[this.config.pronouns.comprehensive] = null;
} else {
delete query[this.config.pronouns.comprehensive];
}
this.$router.replace({ query });
},
},
sourceLibrary() {
if (this.sources === null) {
return null;
}
return new SourceLibrary(this.config, this.sources);
},
groupedSources() {
if (this.sourceLibrary === null || this.$route.query.hasOwnProperty('nosources')) {
return {};
}
let key = this.selectedPronoun.canonicalName;
if (this.config.sources.mergePronouns[key] !== undefined) {
key = this.config.sources.mergePronouns[key];
}
return this.sourceLibrary.getForPronounExtended(key);
},
},
mounted() {
if (process.client) {
this.counterSlow();
}
},
methods: {
addSlash(link) {
return link + (['*', '\''].includes(link.substr(link.length - 1)) ? '/' : '');
},
counterClear() {
if (this.counterHandle) {
clearInterval(this.counterHandle);
}
},
counterPause() {
this.counterSpeed = 0;
this.counterClear();
},
counterSlow() {
this.counterSpeed = 3000;
this.counterClear();
this.counter++;
this.counterHandle = setInterval((_) => this.counter++, this.counterSpeed);
},
counterFast() {
this.counterSpeed = 1000;
this.counterClear();
this.counter++;
this.counterHandle = setInterval((_) => this.counter++, this.counterSpeed);
},
},
};
</script>

View File

@ -3,7 +3,7 @@ import type { PronounLibrary, PronounUsage } from './classes.ts';
import Compressor from './compressor.ts';
import { buildDict, buildList, isEmoji, splitSlashes, unescapeControlSymbols } from './helpers.ts';
import MORPHEMES from '../data/pronouns/morphemes.ts';
import type { Config, NullPronounsConfig } from '../locale/config.ts';
import type { Config } from '../locale/config.ts';
import type { Translator } from './translator.ts';
import type { PronounData, PronounGroupData } from '../locale/data.ts';
@ -243,8 +243,13 @@ export const buildPronoun = (
if (!pronoun && config.pronouns.null && config.pronouns.null.morphemes && path.startsWith(':') &&
path.length <= NULL_PRONOUNS_MAXLENGTH + 1) {
const template = config.pronouns.null as NullPronounsConfig & { morphemes: Record<string, string> };
pronoun = buildPronounFromTemplate(config, path.substring(1), template);
pronoun = buildPronounFromTemplate(config, path.substring(1), {
description: config.pronouns.null.routes.map((variant) => {
return `{/${variant}=${headerForVariant('null', variant, translator)}}`;
}).join(' / '),
history: translator.translate('pronouns.null.description'),
morphemes: config.pronouns.null.morphemes,
});
}
if (!pronoun && config.pronouns.generator.slashes !== false) {
@ -276,9 +281,7 @@ const buildPronounUsageInternal = (
translator: Translator,
): PronounUsage | null => {
if (config.pronouns.null && config.pronouns.null.routes?.includes(path)) {
const specificTranslationKey = `pronouns.null.short.${path}`;
const short = translator.has(specificTranslationKey) ? translator.translate(specificTranslationKey) : path;
return { short: { options: [short] } };
return { short: { options: [shortForVariant('null', path, translator)] } };
}
if (config.pronouns.mirror && config.pronouns.mirror.route === path) {
const specificTranslationKey = 'pronouns.mirror.short';
@ -298,8 +301,7 @@ const buildPronounUsageInternal = (
}
}
if (config.pronouns.ask && config.pronouns.ask.routes.includes(path)) {
const short = translator.translate(`pronouns.ask.short.${path}`);
return { short: { options: [short] } };
return { short: { options: [shortForVariant('ask', path, translator)] } };
}
const pronoun = buildPronoun(pronounLibrary.pronouns, path, config, translator);
@ -309,6 +311,24 @@ const buildPronounUsageInternal = (
return null;
};
export const headerForVariant = (usage: 'null' | 'ask', variant: string, translator: Translator) => {
const specificTranslationKey = `pronouns.${usage}.header.${variant}`;
return translator.has(specificTranslationKey) ? translator.translate(specificTranslationKey) : variant;
};
export const shortForVariant = (usage: 'null' | 'ask', variant: string, translator: Translator) => {
const specificTranslationKey = `pronouns.${usage}.short.${variant}`;
return translator.has(specificTranslationKey) ? translator.translate(specificTranslationKey) : variant;
};
export const buildAnyPronounsList = (config: Config, pronounLibrary: PronounLibrary): string[] => {
if (!config.pronouns.any) {
return [];
}
return [config.pronouns.any, ...Object.keys(pronounLibrary.byKey()).map((key) => `${config.pronouns.any}:${key}`)];
};
export const parsePronouns = (
config: Config,
pronounsRaw: PronounData<string>[],

View File

@ -453,6 +453,10 @@ export const isValidLink = (url: string | URL): boolean => {
}
};
export const addSlash = (link: string): string => {
return link + (['*', '\''].includes(link.substring(link.length - 1)) ? '/' : '');
};
export const parseUserJwt = (token: string, publicKey: string, allLocalesUrls: string[]): User | null => {
try {
const parsed = jwt.verify(token, publicKey, {

3
src/injectionKeys.ts Normal file
View File

@ -0,0 +1,3 @@
import type { InjectionKey } from 'vue';
export const addPronounInjectionKey = Symbol() as InjectionKey<(pronoun: string) => void>;

View File

@ -106,6 +106,11 @@ export function listMissingTranslations(
return false;
}
if ((!config.pronouns.null || !config.pronouns.null.morphemes) &&
keyMatches('pronouns.null.button', 'pronouns.null.base')) {
return false;
}
if (!config.pronouns.emoji && keyMatches('pronouns.emoji.')) {
return false;
}

View File

@ -94,8 +94,7 @@ describe('when configured that emojiself pronouns are available', () => {
});
const nullPronounsConfig: NullPronounsConfig = {
description: 'Avoiding gendered forms',
history: 'Some people prefer not using any pronouns',
routes: ['avoiding', 'no-pronouns', 'null', 'pronounless', 'nullpronominal', 'nameself'],
morphemes: {
pronoun_subject: '#',
pronoun_object: '#',
@ -109,14 +108,20 @@ const nullPronounsConfig: NullPronounsConfig = {
describe('when configured that null pronouns are available', () => {
beforeEach(() => {
config.pronouns.null = nullPronounsConfig;
translations.pronouns.null = {
description: 'Some people prefer not using any pronouns',
};
});
const expectedDescription = '{/avoiding=avoiding} / {/no-pronouns=no-pronouns} / {/null=null} / ' +
'{/pronounless=pronounless} / {/nullpronominal=nullpronominal} / {/nameself=nameself}';
test('builds pronouns from name', () => {
const actual = buildPronoun(pronouns, ':S', config, translator);
expect(actual).toEqual(new Pronoun(
config,
'S',
'Avoiding gendered forms',
expectedDescription,
false,
{
pronoun_subject: 'S',
@ -165,7 +170,7 @@ describe('when configured that null pronouns are available', () => {
expect(actual).toEqual(new Pronoun(
config,
'S',
'Avoiding gendered forms',
expectedDescription,
false,
{
pronoun_subject: 'S',
@ -407,11 +412,7 @@ describe('building the short of a pronoun usage', () => {
});
describe('of null route without specific translation', () => {
beforeEach(() => {
config.pronouns.null = {
description: nullPronounsConfig.description,
history: nullPronounsConfig.history,
routes: ['avoiding', 'no-pronouns', 'null', 'pronounless', 'nullpronominal', 'nameself'],
};
config.pronouns.null = nullPronounsConfig;
});
test('without specific translation', () => {