From a941a7290432cb5ac82b8e2c3ec9c2ae31f37ff8 Mon Sep 17 00:00:00 2001 From: Paul Fitzpatrick Date: Tue, 28 Mar 2023 10:59:17 -0400 Subject: [PATCH] only offer languages that have been marked as substantially complete (#466) This checks for languages that have a special key translated. Any that don't have the key translated, are not offered to the user (unless GRIST_OFFER_ALL_LANGUAGES is set). Co-authored-by: jarek --- README.md | 1 + app/server/localization.ts | 29 ++++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 584ca938..75248e56 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,7 @@ GRIST_LIST_PUBLIC_SITES | if set to true, sites shared with the public will be l GRIST_MANAGED_WORKERS | if set, Grist can assume that if a url targeted at a doc worker returns a 404, that worker is gone GRIST_MAX_UPLOAD_ATTACHMENT_MB | max allowed size for attachments (0 or empty for unlimited). GRIST_MAX_UPLOAD_IMPORT_MB | max allowed size for imports (except .grist files) (0 or empty for unlimited). +GRIST_OFFER_ALL_LANGUAGES | if set, all translated langauages are offered to the user (by default, only languages with a special 'good enough' key set are offered to user). GRIST_ORG_IN_PATH | if true, encode org in path rather than domain GRIST_PAGE_TITLE_SUFFIX | a string to append to the end of the `` in HTML documents. Defaults to `" - Grist"`. Set to `_blank` for no suffix at all. GRIST_PROXY_AUTH_HEADER | header which will be set by a (reverse) proxy webserver with an authorized users' email. This can be used as an alternative to a SAML service. See also GRIST_FORWARD_AUTH_HEADER. diff --git a/app/server/localization.ts b/app/server/localization.ts index c97252ab..38b7c37c 100644 --- a/app/server/localization.ts +++ b/app/server/localization.ts @@ -1,3 +1,5 @@ +import {appSettings} from 'app/server/lib/AppSettings'; +import log from 'app/server/lib/log'; import {lstatSync, readdirSync, readFileSync} from 'fs'; import {createInstance, i18n} from 'i18next'; import {LanguageDetector} from 'i18next-http-middleware'; @@ -26,7 +28,7 @@ export function setupLocale(appRoot: string): i18n { throw new Error("Unrecognized resource file " + fileName); } supportedNamespaces.add(namespace); - preload.push([lang, namespace, fullPath]); + preload.push([namespace, lang, fullPath]); supportedLngs.add(lang); } @@ -51,15 +53,32 @@ export function setupLocale(appRoot: string): i18n { }).catch((err: any) => { // This should not happen, the promise should be resolved synchronously, without // any errors reported. - console.error("i18next failed unexpectedly", err); + log.error("i18next failed unexpectedly", err); }); if (errorDuringLoad) { - console.error('i18next failed to load', errorDuringLoad); + log.error('i18next failed to load', errorDuringLoad); throw errorDuringLoad; } // Load all files synchronously. - for(const [lng, ns, fullPath] of preload) { - instance.addResourceBundle(lng, ns, JSON.parse(readFileSync(fullPath, 'utf8'))); + // First sort by ns, which will put "client" first. That lets us check for a + // client key which, if absent, means the language should be ignored. + preload.sort((a, b) => a[0].localeCompare(b[0])); + const offerAll = appSettings.section('locale').flag('offerAllLanguages').readBool({ + envVar: 'GRIST_OFFER_ALL_LANGUAGES', + }); + const shouldIgnoreLng = new Set<string>(); + for(const [ns, lng, fullPath] of preload) { + const data = JSON.parse(readFileSync(fullPath, 'utf8')); + // If the "Translators: please ..." key in "App" has not been translated, + // ignore this language for this and later namespaces. + if (!offerAll && ns === 'client' && + !Object.keys(data.App || {}).some(key => key.includes('Translators: please'))) { + shouldIgnoreLng.add(lng); + log.debug(`skipping incomplete language ${lng} (set GRIST_OFFER_ALL_LANGUAGES if you want it)`); + } + if (!shouldIgnoreLng.has(lng)) { + instance.addResourceBundle(lng, ns, data); + } } return instance; }