Merge remote-tracking branch 'origin/main'

This commit is contained in:
Andrea Vos 2023-12-31 12:32:22 +01:00
commit 8437c47bf7
8 changed files with 144 additions and 35 deletions

View File

@ -36,8 +36,11 @@ home:
button: 'Zeige den Generator'
header2: 'Generiere einen Link'
base: 'Ausgehend von'
alt: 'Du kannst auch austauschbare Formen in jedes Feld eintragen, z.B. <code>er&sie</code> = „er“ or „sie“.'
pronunciation: 'Du kannst auch die Aussprache spezifizieren, die indem du nach einem senkrechten Strich „|“ (mit der Tastenkombination [Alt Gr] und [<>] ) hinzufügst. Benutze dafür die Lautschrift IPA (Internationales Phonetisches Alphabet). Ein Beispiel wäre: <code>sier|siˑʁ</code> = „sier“, ausgesprochen wie /siˑʁ/.'
alt: 'Du kannst auch austauschbare Formen in jedes Feld eintragen, z.B. <code>er&sie</code> = „er“ oder „sie“.'
pronunciation: >
Du kannst auch die Aussprache spezifizieren, die indem du nach einem senkrechten Strich „|“ (mit der Tastenkombination <kbd>Alt Gr+&lt;</kbd>) hinzufügst.
Benutze dafür die {https://de.wikipedia.org/wiki/Liste_der_IPA-Zeichen=Lautschrift IPA (Internationales Phonetisches Alphabet)}.
Ein Beispiel wäre: <code>sier|zi:ɐ̯</code> = „sier“, ausgesprochen wie /zi:ɐ̯/.
whatisit: 'Was ist das Problem mit Pronomen?'
mission:
header: 'Unsere Mission'
@ -246,10 +249,10 @@ faq:
Online ist es noch einfacher: Füge einfach deine Pronomen (oder einen Link zu Beispielen von unserer Website) zu deiner „Bio“ hinzu.
- >
Denke auch daran, dass manche Menschen je nach Situation einen anderen Namen und andere Pronomen verwenden könnten.
Sie hatten vielleicht vor manchen Freund*innen oder Arbeitskolleg*innen noch kein Coming Out, aber bei anderen Freund*innen fühlen sie sich wohl und gehen damit offen um.
Sie hatten vielleicht vor manchen Freund*innen oder Arbeitskolleg*innen noch kein Coming-out, aber bei anderen Freund*innen fühlen sie sich wohl und gehen damit offen um.
Sei rücksichtsvoll. Du kannst zum Beispiel fragen: „Welche Pronomen soll ich vor deiner*m Chef*in benutzen?“, etc.
- >
Manche Menschen geben mehrere Pronomen an, z.B. „{/er&sie=er/sie}“ oder „{/sier&er=sier/er}“.
Manche Menschen geben mehrere Pronomen an, z.B. „{/er&sie=er oder sie}“ oder auch „{/sier&er=sier oder er}“.
Das bedeutet, dass sie alle diese Formen mögen. Normalerweise ist die erste die bevorzugte.
who-uses-it:
question: 'Benutzt das überhaupt irgendwer?'
@ -386,7 +389,7 @@ contact:
language: >
Wir sind ein internationales Team - die Personen, die eine bestimmte Sprachversion erstellt haben, sind
möglicherweise nicht dieselben, die deine Nachricht lesen werden. Daher würden wir es begrüßen, wenn du uns in
<strong>Englisch or Polnisch</strong> kontaktierst.
<strong>Englisch oder Polnisch</strong> kontaktierst.
team:
name: 'Das „Rat für neutrale Sprache“ Kollektiv'
nameShort: 'Kollektiv'
@ -569,7 +572,7 @@ profile:
names: 'Namen'
pronouns: 'Pronomen'
pronounsInfo: >
Du kannst entweder ein <strong>Pronomen</strong> (z.B. „sier“ or „sie/ihr“) oder einen <strong>Link</strong> (z.B. „https://de.pronouns.page/dey“)
Du kannst entweder ein <strong>Pronomen</strong> (z.B. „sier“ oder „sie/ihr“) oder einen <strong>Link</strong> (z.B. „https://de.pronouns.page/dey“)
oder <strong>vier benutzerdefinierten Formen</strong> (z.B. „xier/xies/xiem/xien“).
Du kannst auch den {/pronomen#generator=<strong>Generator</strong>} verwenden, um die Sätze mit benutzerdefinierten Formen auszufüllen.
pronounsNotFound: 'Nicht erkennbares Format. Bitte beachte die oben genannte Anweisung.'
@ -673,7 +676,7 @@ profile:
relationship: 'Verhältnis (z.B. „Partner*in“, „beste*r Freund*in“)'
mutual: 'Diese Verbindung besteht in beide Richtungen'
yourMentions:
header: 'Mein Umfeld Erwähnungen '
header: 'Erwähnungen aus deinem Umfeld'
description: >
Hier ist die Liste der Personen, die dich zu ihrem Umfeld hinzugefügt haben. Wenn du sie auch zu deinem
Umfeld hinzufügst, wird auf beiden Karten ein Häkchen [s:shield-check] angezeigt.
@ -739,6 +742,43 @@ profile:
success: 'Deine Visitenkarten wurden erfolgreich wiederhergestellt! Die Seite wird nun neugeladen.'
error:
signature: 'Ungültige Signatur, kann die Integrität der Sicherungsdatei nicht verifizieren'
markdown:
enable: 'Verwende Formattierung mit Markdown in deiner Visitenkarte'
features: 'Zeige unterstützte Markdownsyntax'
examples:
- 'ein **fettgedruckter** Text'
- 'ein _kursiver_ Text'
- 'ein `Code`-Fragment'
- 'ein ~~durchgestrichener~~ Text'
- 'ein ==markierter== Text'
- 'ein ^hochgestellter^ Text'
- 'ein ~tiefgestellter~ Text'
- >
benutze einen umgedrehten Schrägstrich `\`, um \_Formatierungszeichen\_ \*\*abzuschalten\*\*,
insbesondere bei Gendersternen (wie Autor\*in)
calendar:
header: 'Kalender'
info:
- >
In diesem Abschnitt kannst du deinen eigenen Kalender erstellen,
welcher in deiner Visitenkarte erscheint und den du zu deiner Kalenderapp hinzufügen kannst.
- >
Wähle aus der Liste von öffentlich gefeierten Ereignissen des Queeren Kalenders
oder füge deine eigenen persönlichen Ereignisse hinzu.
Beispiele sind Jahrestage des Coming-outs, der Beginn einer HRT, der Tag einer Namensänderung, Jahrestage in deinen Beziehungen etc.
customEvents:
header: 'Persönliche Ereignisse'
disclaimer: 'Dieses Ereignis ist ein persönliches Ereignis, welches von der Person dieser Visitenkarte erstellt wurde'
name: 'Ereignisname (z.B. „HRT begonnen“)'
month: 'Monat'
day: 'Tag'
comment: 'Kommentar (optional)'
validation:
missingName: 'Ereignisname ist erforderlich'
missingDate: 'Datum ist erforderlich'
invalidDate: 'Datum ist ungültig'
publicEvents:
header: 'Ereignisse aus dem Queeren Kalender'
share: 'Teilen'
@ -1160,6 +1200,8 @@ calendar:
list: 'Ereignisliste'
link: 'Link'
full: 'Ganzer Kalender'
onlyFirstDays: 'Version, bei der bei langen Ereignissen nur der jeweils erste Tag eingetragen ist'
start: 'Beginn'
supportBanners:
ukraine:

View File

@ -126,7 +126,7 @@ export default [
'footer.source',
'error.invalidImage',
'profile.banner',
'pronoun.example',
'profile.example',
'calendar.onlyFirstDays',
'calendar.start',
'calendar.events.nonmonogamy_visibility_day',

View File

@ -358,11 +358,10 @@ contact:
team:
name: 'The “Neutral Language Council” collective' # TODO
nameShort: 'Collective'
description: # TODO
- >
We are a queer collective dedicated to assembling, researching, shaping and promoting
gender neutral and nonbinary language.
We also support actions towards equality and social justice.
description: > # TODO
We are a queer collective dedicated to assembling, researching, shaping and promoting
gender neutral and nonbinary language.
We also support actions towards equality and social justice.
logo: 'Logo of the collective is a combination of the transgender symbol and a speech bubble that symbolises language.' # TODO
members: 'Current members' # TODO
member: 'Member of the collective' # TODO

View File

@ -84,7 +84,7 @@ export default ({ app, store }) => {
}
app.router.afterEach(() => {
if (typeof window !== 'undefined' && window.fusetag) {
if (typeof window !== 'undefined' && window.fusetag && window.fusetag.pageInit) {
window.fusetag.pageInit();
}
});

View File

@ -19,6 +19,16 @@ export const buildList = (fn, ...args) => {
return list;
}
export const deepGet = (obj, path) => {
let value = obj;
for (let part of path.split('.')) {
value = value[part];
if (value === undefined) { break; }
}
return value;
}
export const head = ({title, description, banner, noindex = false, keywords}) => {
const meta = { meta: [] };

View File

@ -4,19 +4,10 @@ import Plausible from 'plausible-api';
import fetch from 'node-fetch';
import { ulid } from 'ulid';
import expectedTranslations from '../locale/expectedTranslations.js';
import { deepGet } from './helpers.js';
import fs from 'fs';
import Suml from 'suml';
const deepGet = (obj, path) => {
let value = obj;
for (let part of path.split('.')) {
value = value[part];
if (value === undefined) { break; }
}
return value;
}
const formatDate = d => `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
export const buildChart = (rows, cumulative = true) => {

View File

@ -1,3 +1,4 @@
import { deepGet } from './helpers.js';
import translations from '../data/translations.suml';
import baseTranslations from '../locale/_base/translations.suml';
import expectedTranslations from '../locale/expectedTranslations.js';
@ -17,17 +18,14 @@ class Translator {
}
get(key, warn = false, base = false, useFallback = true) {
let value = base ? this.baseTranslations : this.translations;
for (let part of key.split('.')) {
value = value[part];
if (value === undefined) {
if (warn) {
console.error('Cannot find translation: ' + key);
}
if (!base && useFallback) {
return this.get(key, warn, true);
}
return undefined;
const translations = base ? this.baseTranslations : this.translations;
const value = deepGet(translations, key);
if (value === undefined) {
if (warn) {
console.error('Cannot find translation: ' + key);
}
if (!base && useFallback) {
return this.get(key, warn, true);
}
}
return value;

69
test/translations.test.js Normal file
View File

@ -0,0 +1,69 @@
import { expect, test } from '@jest/globals';
import { loadSumlFromBase } from '../server/loader.js';
import { deepGet } from '../src/helpers.js';
import expectedTranslations from '../locale/expectedTranslations.js';
import locales from '../locale/locales.js';
const baseTranslations = loadSumlFromBase('locale/_base/translations');
const typeFlexibleKeys = new Set(['home.generator.alt']);
function specificTypeOf(value) {
if (typeof(value) === 'object') {
if (value === null) {
return null;
} else if (Array.isArray(value)) {
return 'array';
} else {
return 'object';
}
} else {
return typeof(value);
}
}
function toMatchBaseTranslationsSchema(actual) {
const messages = recursivelyValidateSchema(actual, baseTranslations);
if (messages.length > 0) {
return {
message: () =>
`expected translations to match schema of base translations\n\n${messages.join('\n')}`,
pass: false,
}
} else {
return {
message: () =>
'expected translations to mismatch schema of base translations',
pass: true,
}
}
}
function recursivelyValidateSchema(actual, base, parentKey = '') {
const messages = [];
for (const [property, value] of Object.entries(actual)) {
const key = parentKey ? `${parentKey}.${property}` : property;
if (base[property] === undefined || base[property] === null) {
continue;
}
if (value !== null && !typeFlexibleKeys.has(key) && specificTypeOf(value) !== specificTypeOf(base[property])) {
messages.push(`${key} has type ${specificTypeOf(value)}, expected ${specificTypeOf(base[property])}`);
}
if (specificTypeOf(value) === 'object') {
messages.push(...recursivelyValidateSchema(value, base[property], key));
}
}
return messages;
}
test.each(locales)('translations of $code match schema of base translations', ({ code }) => {
const translations = loadSumlFromBase(`locale/${code}/translations`);
expect(translations).toMatchBaseTranslationSchema();
});
test('expected translations are defined in base translations', () => {
const undefinedTranslations = expectedTranslations.filter(key => deepGet(baseTranslations, key) === undefined);
expect(undefinedTranslations).toEqual([]);
});
expect.extend({ toMatchBaseTranslationSchema: toMatchBaseTranslationsSchema });