// Copyright (c) 2020-2021 Drew Lemmy
// This file is part of KristWeb 2 under GPL-3.0.
// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
import JSON5 from "json5";
import { Language, getLanguages } from "@utils/i18n";
export interface LangKeys { [key: string]: string }
export interface AnalysedLanguage {
code: string;
language?: Language;
error?: string;
keys?: LangKeys;
keyCount: number;
missingKeys?: { k: string; v: string }[];
}
const IGNORE_KEYS = /_(?:plural|interval|male|female|\d+)$/;
export function analyseLanguage(
code: string,
language: Language | undefined,
enKeys: Record<string, string> | undefined,
translation: any
): AnalysedLanguage {
const isObject = (val: any) => typeof val === "object" && !Array.isArray(val);
const addDelimiter = (a: string, b: string) => a ? `${a}.${b}` : b;
// Find all translation keys recursively
const keys = (obj: any = {}, head = ""): LangKeys =>
Object.entries(obj)
.reduce((out, [key, value]) => {
// Remove plural suffixes etc. These will be de-duplicated afterwards.
const bareKey = key.replace(IGNORE_KEYS, "");
const fullPath = addDelimiter(head, bareKey);
return isObject(value as any)
? { ...out, ...keys(value, fullPath) }
: {...out, [fullPath]: value };
}, {});
const langKeys = keys(translation);
return {
code,
language,
keys: langKeys,
keyCount: Object.keys(langKeys).length,
missingKeys: enKeys
? Object.entries(enKeys)
.filter(([k]) => !langKeys[k])
.map(([k, v]) => ({ k, v }))
: []
};
}
export async function getEnglishData(): Promise<AnalysedLanguage> {
const languages = getLanguages();
// Fetch and analyse the data for English first
const enLang = languages!["en"];
const enData = await getLanguageData("en");
const en = analyseLanguage("en", enLang, undefined, enData);
return en;
}
export interface AnalysedLanguages {
enKeyCount: number;
languages: AnalysedLanguage[];
}
export async function analyseLanguages(): Promise<AnalysedLanguages | false> {
const languages = getLanguages();
if (!languages) return false;
// Fetch and analyse the data for English first
const en = await getEnglishData();
// Fetch the locale file for each language code
const langEntries = Object.entries(languages);
const languageData = await Promise.allSettled(
langEntries.map(e => getLanguageData(e[0]))
);
return {
enKeyCount: en.keyCount,
// If a language couldn't be fetched, show an error for it instead
languages: languageData.map((result, i) => result.status === "fulfilled"
? analyseLanguage(
langEntries[i][0],
langEntries[i][1],
en.keys,
result.value
)
: {
code: langEntries[i][0],
language: langEntries[i][1],
keyCount: 0,
error: result.reason.toString()
})
};
}
// Replaced by webpack DefinePlugin and git-revision-webpack-plugin
declare const __GIT_VERSION__: string;
const gitVersion: string = __GIT_VERSION__;
async function getLanguageData(code: string): Promise<any> {
const res = await fetch(`/locales/${code}.json?v=${encodeURIComponent(gitVersion)}`);
if (!res.ok) throw new Error(res.statusText);
// Translations now use JSON5 to allow for comments, newlines, and basic
// syntax errors like trailing commas
const data = await res.text();
return JSON5.parse(data);
}