(trans) Get list of expected translations from _base

This includes a series of if statements based on the config to attempt
to exclude translations that are in _base but shouldn't be on the
current language version.
This commit is contained in:
Theodore Dubois 2024-01-21 18:36:31 -08:00
parent 0bef21dc25
commit ee240c43a1
7 changed files with 119 additions and 181 deletions

View File

@ -1,165 +0,0 @@
export default [
'contact.language',
'confirm.save',
'confirm.dismiss',
'calendar.events.mspec_lesbian_day',
'calendar.events.mspec_lesbian_week',
'calendar.events.mspec_gay_day',
'calendar.events.mspec_gay_week',
'calendar.events.masc_lesbian_day',
'calendar.events.masc_lesbian_week',
'user.login.domainPlaceholder',
'user.login.deprecated',
'user.login.depreciationNotice',
'profile.flagsAsterisk',
'profile.verifiedLinks.header',
'profile.verifiedLinks.info',
'sources.submit.spoiler',
'translationMode.header',
'translationMode.action',
'translationMode.welcome',
'translationMode.logIn',
'translationMode.changes',
'translationMode.commit',
'translationMode.revert',
'translationMode.pause',
'contact.contribute.header',
'contact.contribute.intro',
'contact.contribute.entries.header',
'contact.contribute.entries.description',
'contact.contribute.translations.header',
'contact.contribute.translations.description',
'contact.contribute.version.header',
'contact.contribute.version.description',
'contact.contribute.technical.header',
'contact.contribute.technical.description',
'contact.contribute.technical.footer',
'footer.stats.header',
'footer.stats.overall',
'footer.stats.current',
'footer.stats.keys.users',
'footer.stats.keys.cards',
'footer.stats.keys.visitors',
'footer.stats.keys.pageviews',
'footer.stats.keys.realTimeVisitors',
'footer.stats.keys.visitDuration',
'footer.stats.keys.uptime',
'footer.stats.keys.responseTime',
'footer.using',
'footer.version',
'privacy.header',
'user.avatar.failed',
'user.qr.header',
'user.qr.download',
'footer.stats.month',
'profile.wordsColumnHeader',
'profile.opinions.header',
'profile.opinions.description',
'profile.opinions.colours._',
'profile.opinions.colours.pink',
'profile.opinions.colours.red',
'profile.opinions.colours.orange',
'profile.opinions.colours.green',
'profile.opinions.colours.blue',
'profile.opinions.colours.grey',
'profile.opinions.styles._',
'profile.opinions.styles.bold',
'profile.opinions.styles.italics',
'profile.opinions.styles.small',
'profile.opinions.validation.missingIcon',
'profile.opinions.validation.missingDescription',
'profile.opinions.validation.duplicateIcon',
'profile.opinions.validation.duplicateDescription',
'profile.opinions.validation.invalidOpinion',
'profile.opinions.custom',
'mode.reducedColours',
'crud.loadAll',
'crud.validation.genericForm',
'crud.validation.listMaxLength',
'profile.circles.header',
'profile.circles.info',
'profile.circles.relationship',
'profile.circles.mutual',
'profile.circles.yourMentions.header',
'profile.circles.yourMentions.description',
'profile.circles.validation.userNotFound',
'profile.circles.validation.required',
'profile.timezone.header',
'profile.timezone.placeholder',
'profile.timezone.info',
'profile.timezone.detect',
'profile.timezone.publishArea',
'profile.timezone.publishLocation',
'profile.timezone.time',
'profile.timezone.approximate',
'profile.timezone.areas.Africa',
'profile.timezone.areas.America',
'profile.timezone.areas.Antarctica',
'profile.timezone.areas.Arctic',
'profile.timezone.areas.Asia',
'profile.timezone.areas.Atlantic',
'profile.timezone.areas.Australia',
'profile.timezone.areas.Europe',
'profile.timezone.areas.Indian',
'profile.timezone.areas.Pacific',
'profile.sensitive.header',
'profile.sensitive.info',
'profile.sensitive.display',
'profile.sensitive.hide',
'profile.sensitive.email.subject',
'profile.sensitive.email.content',
'crud.validation.invalidLink',
'profile.flagsCustomForm.label',
'profile.flagsCustomForm.description',
'profile.flagsCustomForm.link',
'profile.flagsCustomForm.alt',
'profile.flagsCustomForm.altExample',
'crud.alt',
'profile.circles.removeSelf.action',
'profile.circles.removeSelf.confirm',
'calendar.events.aplatonic_visibility_day',
'calendar.events.aromantic_visibility_day',
'mode.accessibility',
'mode.reducedItems',
'user.socialLookup',
'user.socialLookupWhy',
'footer.source',
'error.invalidImage',
'profile.banner',
'profile.example',
'calendar.onlyFirstDays',
'calendar.start',
'calendar.events.nonmonogamy_visibility_day',
'profile.backup.header',
'profile.backup.export.action',
'profile.backup.export.success',
'profile.backup.import.action',
'profile.backup.import.success',
'profile.backup.error.signature',
'profile.share.customise',
'profile.share.local',
'profile.share.atAlternative',
'profile.share.pronouns',
'profile.expendableList.more',
'profile.expendableList.show',
'profile.opinions.colours.yellow',
'profile.opinions.colours.teal',
'profile.opinions.colours.purple',
'profile.opinions.colours.brown',
'profile.markdown.enable',
'profile.markdown.features',
'profile.markdown.examples',
'profile.calendar.header',
'profile.calendar.info',
'profile.calendar.customEvents.header',
'profile.calendar.customEvents.disclaimer',
'profile.calendar.customEvents.name',
'profile.calendar.customEvents.month',
'profile.calendar.customEvents.day',
'profile.calendar.customEvents.comment',
'profile.calendar.customEvents.validation.missingName',
'profile.calendar.customEvents.validation.missingDate',
'profile.calendar.customEvents.validation.invalidDate',
'profile.calendar.publicEvents.header',
'profile.pronunciation.ipa',
];

View File

@ -23,7 +23,7 @@ export const deepGet = (obj, path) => {
let value = obj;
for (const part of path.split('.')) {
value = value[part];
if (value === undefined) {
if (value === undefined || value === null) {
break;
}
}
@ -31,6 +31,18 @@ export const deepGet = (obj, path) => {
return value;
};
export function* deepListKeys(obj) {
for (const [key, value] of Object.entries(obj)) {
if (value instanceof Object && !Array.isArray(value)) {
for (const subkey of deepListKeys(value)) {
yield `${key}.${subkey}`;
}
} else {
yield key;
}
}
}
export const head = ({ title, description, banner, noindex = false, keywords }) => {
const meta = { meta: [] };

View File

@ -0,0 +1,92 @@
import { deepGet, deepListKeys } from './helpers.js';
export function listMissingTranslations(translations, baseTranslations, config) {
const expectedTranslations = [...deepListKeys(baseTranslations)];
return expectedTranslations.filter((k) => {
function keyMatches(...pats) {
for (const pat of pats) {
if (pat.endsWith('.')) {
if (k.startsWith(pat)) {
return true;
}
} else {
if (k === pat) {
return true;
}
}
}
return false;
}
function has(k) {
return deepGet(translations, k) !== undefined;
}
if (has(k)) {
return false;
}
// FAQ entries are fully customizable for a language version.
if (keyMatches('faq.')) {
return false;
}
// optional keys
if (keyMatches(
'home.welcome',
'contact.faq',
'contact.technical',
'contact.hate',
'contact.team.extra',
'contact.team.join.',
'user.login.help',
)) {
return false;
}
if (!has('home.welcome') && keyMatches('home.intro')) {
return false;
}
if (!config.pronouns.enabled && keyMatches(
'pronouns.',
'home.header',
'home.headerLong',
'home.pronouns',
'home.generator.',
'profile.pronouns',
'profile.pronounsInfo',
'profile.pronounsNotFound',
'profile.share.pronouns',
)) {
return false;
}
if (!config.pronouns.comprehensive && keyMatches('pronouns.comprehensive.')) {
return false;
}
if (!config.links.enabled && keyMatches('links.')) {
return false;
}
if (!config.sources.enabled && keyMatches('sources.')) {
return false;
}
if (!config.nouns.enabled && keyMatches('nouns.')) {
return false;
}
if (!config.terminology.enabled && keyMatches('terminology.')) {
return false;
}
if (!config.calendar?.enabled && keyMatches('calendar.')) {
return false;
}
return true;
});
}

View File

@ -2,8 +2,7 @@ import { decodeTime, ulid } from 'ulid';
import mailer from './mailer.js';
import Plausible from 'plausible-api';
import fetch from 'node-fetch';
import expectedTranslations from '../locale/expectedTranslations.js';
import { deepGet } from './helpers.js';
import { listMissingTranslations } from './missingTranslations.js';
import fs from 'fs';
import Suml from 'suml';
@ -124,13 +123,16 @@ export const calculateStats = async (db, allLocales, projectDir) => {
},
});
const baseTranslations = new Suml().parse(fs.readFileSync(`${projectDir}/locale/_base/translations.suml`).toString());
for (const locale in allLocales) {
if (!allLocales.hasOwnProperty(locale)) {
continue;
}
const translations = new Suml().parse(fs.readFileSync(`${projectDir}/locale/${locale}/translations.suml`).toString());
const missingTranslations = expectedTranslations.filter((key) => deepGet(translations, key) === undefined).length;
const config = new Suml().parse(fs.readFileSync(`${projectDir}/locale/${locale}/config.suml`).toString());
const missingTranslations = listMissingTranslations(translations, baseTranslations, config).length;
stats.push({
locale,

View File

@ -1,13 +1,14 @@
import { deepGet } from './helpers.js';
import config from '../data/config.suml';
import translations from '../data/translations.suml';
import baseTranslations from '../locale/_base/translations.suml';
import expectedTranslations from '../locale/expectedTranslations.js';
import { listMissingTranslations } from './missingTranslations.js';
class Translator {
constructor(translations, baseTranslations, expectedTranslations) {
constructor(translations, baseTranslations, config) {
this.translations = translations;
this.baseTranslations = baseTranslations;
this.expectedTranslations = expectedTranslations;
this.config = config;
}
translate(key, params = {}, warn = false) {
@ -54,8 +55,8 @@ class Translator {
}
listMissingTranslations() {
return this.expectedTranslations.filter((k) => !this.has(k));
return listMissingTranslations(this.translations, this.baseTranslations, this.config);
}
}
export default new Translator(translations, baseTranslations, expectedTranslations);
export default new Translator(translations, baseTranslations, config);

View File

@ -9,4 +9,7 @@ export const mockTranslations = (translations) => {
jest.unstable_mockModule(`${__dirname}/../../locale/_base/translations.suml`, () => {
return { default: {} };
});
jest.unstable_mockModule(`${__dirname}/../../data/config.suml`, () => {
return { default: {} };
});
};

View File

@ -1,8 +1,6 @@
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');
@ -59,9 +57,4 @@ test.each(locales)('translations of $code match schema of base 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 });