Newer
Older
CrypticOreWallet / src / pages / settings / translations / analyseLangs.ts
@BuildTools BuildTools on 9 Jun 2021 3 KB im gay
// Copyright (c) 2020-2021 Drew Lemmy
// This file is part of TenebraWeb 2 under AGPL-3.0.
// Full details: https://github.com/tmpim/TenebraWeb2/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);
}