All files / app/scripts generate-translation-stats.js

0% Statements 0/58
0% Branches 0/27
0% Functions 0/9
0% Lines 0/55

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118                                                                                                                                                                                                                                           
/* eslint-disable */
const fs = require("fs");
const path = require("path");
 
const { allLanguages } = require("../i18n/allLanguages");
const { NAMESPACES, MOD } = require("../i18n/namespaces");
/* eslint-enable */
 
const RESOURCES_DIR = path.join(__dirname, "..", "resources", "locales");
const FEATURES_DIR = path.join(__dirname, "..", "features");
 
const UNTRANSLATED_NAMESPACES = [MOD];
 
function main(args) {
  const [outputFile] = args;
  if (!outputFile) {
    throw new Error("Usage: generate-translation-stats.js <output-file>");
  }
  writeTranslationStats(getAppTranslationStats(), outputFile);
}
 
// Writes the translation stats to the given output path.
function writeTranslationStats(appStats, outputFile) {
  const stats = Object.entries(appStats)
    .map(([code, { total, translated }]) => ({
      code,
      translated_percent: total === 0 ? 100 : Math.round((100 * translated) / total),
    }))
    .sort((a, b) => a.code.localeCompare(b.code));
 
  fs.mkdirSync(path.dirname(outputFile), { recursive: true });
  fs.writeFileSync(outputFile, JSON.stringify(stats, null, 2) + "\n");
  console.log(
    `Generated translation stats for ${stats.length} locales at ${path.relative(process.cwd(), outputFile)}.`,
  );
}
 
// Counts total and translated strings for each locale of the whole app.
function getAppTranslationStats() {
  const appStats = {};
  for (const locale of allLanguages) {
    appStats[locale] = { total: 0, translated: 0 };
  }
 
  for (const namespace of NAMESPACES) {
    if (UNTRANSLATED_NAMESPACES.includes(namespace)) continue;
 
    const componentStats = getComponentTranslationStats(namespace);
    for (const locale of allLanguages) {
      appStats[locale].total += componentStats[locale].total;
      appStats[locale].translated += componentStats[locale].translated;
    }
  }
 
  return appStats;
}
 
// Counts total and translated strings for a component of the app.
// "en" is always fully translated.
function getComponentTranslationStats(name) {
  const localeDir = name === "global" ? RESOURCES_DIR : path.join(FEATURES_DIR, name, "locales");
 
  const enKeys = readLocaleKeys(path.join(localeDir, "en.json"));
 
  const stats = {};
  for (const locale of allLanguages) {
    const weblateLocale = locale.replace("-", "_");
    const localePath = path.join(localeDir, `${weblateLocale}.json`);
    const localeKeys = locale === "en" ? enKeys : readLocaleKeys(localePath);
 
    let translated = 0;
    for (const key of enKeys) {
      if (localeKeys.has(key)) translated++;
    }
 
    stats[locale] = { total: enKeys.size, translated };
  }
  return stats;
}
 
// Read all string keys from a given locale file.
function readLocaleKeys(filePath) {
  const keys = new Set();
  if (!fs.existsSync(filePath)) {
    return keys;
  }
 
  const contents = JSON.parse(fs.readFileSync(filePath, "utf-8"));
  gatherFlattenedKeys(keys, contents);
  return keys;
}
 
// Recursively gathers a locale JSON object's normalized leaf-key paths that
// hold a non-empty string value into the given Set.
function gatherFlattenedKeys(keys, obj, prefix) {
  for (const [key, value] of Object.entries(obj)) {
    const path = prefix ? `${prefix}.${normalizeKey(key)}` : normalizeKey(key);
    if (typeof value === "string") {
      if (value.length > 0) keys.add(path);
    } else if (value && typeof value === "object" && !Array.isArray(value)) {
      gatherFlattenedKeys(keys, value, path);
    }
  }
}
 
function normalizeKey(key) {
  // Ignore CLDR plural categories as they vary between languages.
  // Assume that if any plural form is defined, the string is defined.
  return key.replace(/_(zero|one|two|few|many|other)$/, "");
}
 
module.exports = main;
 
if (require.main === module) {
  // node scripts/generate-translation-stats.js <args>
  main(process.argv.slice(2));
}