#49 New computation and website improvments
Merged 8 months ago by darknao. Opened 8 months ago by jibecfed.

file modified
+7 -1
@@ -1,6 +1,12 @@ 

  FROM registry.fedoraproject.org/fedora:38

  

- RUN dnf install -y lbzip2 unzip xz cpio dnf-plugins-core rsync python3-pip hugo gettext git rubygem-asciidoctor glibc-gconv-extra

+ RUN dnf install -y lbzip2 unzip xz cpio dnf-plugins-core rsync python3-pip gettext git glibc-gconv-extra

+ 

+ # f39 release don't work yet (pip install issue)

+ # use the same version as in f39 https://packages.fedoraproject.org/pkgs/hugo/hugo/

+ ADD https://github.com/gohugoio/hugo/releases/download/v0.111.3/hugo_0.111.3_linux-amd64.tar.gz /tmp

+ RUN tar -C /usr/bin -xvf /tmp/hugo_0.111.3_linux-amd64.tar.gz

+ 

  COPY requirements.txt /src/requirements.txt

  

  RUN pip install --no-cache -r /src/requirements.txt

file modified
+2 -2
@@ -20,7 +20,7 @@ 

  

  ```bash

  dnf install podman

- ./runall.sh 34

+ ./runall.sh 39

  ````

  

  This takes from 10 to 20 hours to process, you may wish to reduce the number of packages to scan.
@@ -29,5 +29,5 @@ 

  # Run it anywhere

  

  ```bash

- podman run -it --rm -v ./:/src:z -v ./results:/src/results:z -e DNF_CONF=dnf_${release}.conf -e TMP_DIR=/src/results/f${release}/tmp fedlocstats:34 $script

+ podman run -it --rm -v ./:/src:z -v ./results:/src/results:z -e DNF_CONF=dnf_${release}.conf -e TMP_DIR=/src/results/f${release}/tmp fedlocstats:39 $script

  ```

file modified
+11 -1
@@ -49,6 +49,13 @@ 

          help="Keep SRPMs in /srpms",

      )

      parser.add_argument(

+         "--store-srpms-in-results",

+         default=False,

+         action="store_true",

+         dest="srpms_in_results",

+         help="Store srpms in results folder (useful for automation)",

+     )

+     parser.add_argument(

          "-f",

          "--force",

          default=False,
@@ -76,7 +83,10 @@ 

          srpm_regex = re.compile(f"^{args.filter}$")

  

      packages_folder = f"./results/{args.results}/packages/"

-     srpms_path = os.path.abspath("./srpms/")

+     if args.srpms_in_results:

+         srpms_path = os.path.abspath(f"./results/{args.results}/srpms/")

+     else:

+         srpms_path = os.path.abspath("./srpms/")

  

      if not os.path.exists(packages_folder):

          os.makedirs(packages_folder)

file modified
+10 -10
@@ -1,5 +1,5 @@ 

  #!/usr/bin/env python3

- """ Parse translation files to deduct language list """

+ """ Detect language for each translation file """

  

  import argparse

  import glob
@@ -19,7 +19,7 @@ 

      """Handle params"""

  

      parser = argparse.ArgumentParser(

-         description="Creates a list of languages form translation files"

+         description="Detect language for each translation file"

      )

  

      parser.add_argument(
@@ -101,7 +101,7 @@ 

                  processed_files_count += 1

  

                  result = p.search(po_file)

-                 lang_code = result.group(1)

+                 path_lang_code = result.group(1)

                  metadata = dict()

                  error = ""

                  try:
@@ -115,9 +115,9 @@ 

                      # maybe a polib bug? to investigate before using it in TM

                      error = "error-os"

  

-                 lang, decision = choose_language_code_from_po(lang_code, metadata)

+                 lang, decision = choose_language_code_from_po(path_lang_code, metadata)

  

-                 debug_file = {"lang_in_path": lang_code,

+                 debug_file = {"lang_in_path": path_lang_code,

                                "metadata_lang": metadata.get("Language", ""),

                                "metadata_plurals": metadata.get("Plural-Forms", ""),

                                "metadata_language_team": metadata.get("Language-Team", ""),
@@ -139,11 +139,11 @@ 

  

  

  def choose_language_code_from_po(filename: str, metadata: dict[str]) -> tuple[str, int]:

-     """ From a po file and its metadata, choose the most likely language code

-     By priority: the Language metadata

-     :param filename: the po file

-     :param metadata:

-     :return: a language code

+     """ Deduct a language code from a filename and its metadata

+ 

+     :param filename: po filename

+     :param metadata: po metadata

+     :return: a language code, a decision path

      """

      log = logging.getLogger("buildLanguageList.choose_lang")

  

file modified
+87 -45
@@ -1,5 +1,5 @@ 

  #!/usr/bin/env python3

- """For each package, compute stats"""

+ """ Computes stats for each package with translations and each detected language """

  

  import argparse

  import json
@@ -16,22 +16,21 @@ 

  import utils

  

  

- def compute_language_statistics(languages: dict, total_distribution_source_words: int) -> dict:

-     """

-         Target:

-         "packages": [

-             {

-                 "name": "blueberry",

-                 "progress": 100,

-                 "translated": 166,

-                 "team": "French <fr@li.org>"

-             }

-         ],

-         "progress": 98.1,

-         "progress_d": 63.4,

-         "totalsourcewords_d": 11491,

-         "totalsourcewordssum": 7428,

-         "translatedsourcewordssum": 7287

+ def compute_language_statistics(languages_stats: dict, total_release_source_words: int) -> dict:

+     """ For each language, produce global statistics and per package statistics

+ 

+     global statistics target:

+         "totalsourcewordssum": total words on started packages

+         "totalsourcewords_d": total words in release

+         "translatedsourcewordssum": total translated words

+         "progress": current translation progress on started packages (in percents)

+         "progress_d": current translation progress on all strings in release (in percents)

+ 

+     per package statistics target:

+         "name": package name

+         "progress": current translation progress (in percents)

+         "translated": total translated words (source words, it can vary in target language)

+         "team": language team info

      """

      log = logging.getLogger("buildStats.compute_language_statistics")

  
@@ -41,7 +40,7 @@ 

      package_fields = ["translatedsourcewords", "fuzzysourcewords", "untranslatedsourcewords", "translated", "fuzzy",

       "untranslated", "translatedtargetwords", "package"]

  

-     for code, stats in languages.items():

+     for code, stats in languages_stats.items():

          results_languages[code] = {}

          results_languages[code]["po"] = stats

          result = {}
@@ -53,13 +52,22 @@ 

  

          result["totalsourcewordssum"] = result["translatedsourcewords"] + result["fuzzysourcewords"] + result[

              "untranslatedsourcewords"]

-         result["totalsourcewords_d"] = total_distribution_source_words

-         result["progress"] = result["translatedsourcewords"] / result["totalsourcewordssum"]

-         result["progress_d"] = result["translatedsourcewords"] / result["totalsourcewords_d"]

+         result["totalsourcewords_d"] = total_release_source_words

+ 

+         # prevent a Runtime warning for languages with no content

+         if result["totalsourcewordssum"] > 0:

+             result["progress"] = (result["translatedsourcewords"] / result["totalsourcewordssum"]) * 100

+         else:

+             result["progress"] = 0.0

+ 

+         result["progress_d"] = (result["translatedsourcewords"] / result["totalsourcewords_d"]) * 100

  

          packages_stats = df[package_fields].groupby("package").sum()

          packages_stats["totalsourcewordssum"] = packages_stats["translatedsourcewords"] + packages_stats["fuzzysourcewords"] + packages_stats["untranslatedsourcewords"]

-         packages_stats["progress"] = packages_stats["translatedsourcewords"] / packages_stats["totalsourcewordssum"]

+ 

+         packages_stats["progress"] = (packages_stats["translatedsourcewords"] / packages_stats["totalsourcewordssum"]) * 100

+         # prevent NaN values when a package have total source words = 0

+         packages_stats.fillna(0, inplace=True)

          packages_stats["team"] = df[["metadata_language_team", "package"]].groupby("package").first()

          result["packages"] = packages_stats.to_dict(orient="index")

  
@@ -68,19 +76,15 @@ 

      return results_languages

  

  

- def compute_package_statistics(df):

-     """

-     [

-     {

-       "lang_code": "de",

-       "team": "Low German <nds@li.org>, German <de@li.org>",

-       "filename": [

-         "po/blueberry-nds.po",

-         "po/blueberry-de.po"

-       ],

-       "progress": 179,

-       "translated": 297

-     },

+ def compute_package_statistics(df: pd.DataFrame) -> dict:

+     """ For each package, per language statistics

+ 

+     global statistics target:

+         "lang_code": language code

+         "team": language team info

+         "progress": current translation progress (in percents),

+         "translated": total translated words (source words, it can vary in target language)

+         "filename": list of files considered for statistics

      """

      log = logging.getLogger("buildStats.compute_language_statistics")

      results = dict()
@@ -90,7 +94,9 @@ 

  

      stats = df[po_fields].groupby(index).sum()

      stats["totalsourcewordssum"] = stats["translatedsourcewords"] + stats["fuzzysourcewords"] + stats["untranslatedsourcewords"]

-     stats["progress"] = stats["translatedsourcewords"] / stats["totalsourcewordssum"]

+     stats["progress"] = (stats["translatedsourcewords"] / stats["totalsourcewordssum"]) * 100

+     # prevent NaN values when a package have total source words = 0

+     stats.fillna(0, inplace=True)

      stats["team"] = df[["metadata_language_team", index]].groupby(index).first()

      df['filename'] = df.index

      stats["filename"] = df[["filename", index]].groupby(index).sum()
@@ -99,11 +105,27 @@ 

      return results

  

  

+ def compute_package_totalsourcewords(stats: dict) -> int:

+     """ compute the total source words for a package """

+     log = logging.getLogger("buildStats.compute_package_totalsourcewords")

+ 

+     langs = {}

+     for file, stat in stats.items():

+         total = langs.get(stat["lang_code_chosen"], 0)

+ 

+         langs[stat["lang_code_chosen"]] = total + stat["translatedsourcewords"] + stat["fuzzysourcewords"] + stat["untranslatedsourcewords"]

+ 

+     if "error" in langs.keys():

+         del langs["error"]

+ 

+     return max(langs.values())

+ 

+ 

  def main():

      """Handle params"""

  

      parser = argparse.ArgumentParser(

-         description="Computes stats for each package with translations"

+         description="Computes stats for each package with translations and each detected language"

      )

      parser.add_argument(

          "--results", required=True, help="Set the results folder to use"
@@ -162,7 +184,7 @@ 

  

          df = pd.DataFrame.from_dict(stats["po"], orient='index')

          stats["stats"] = compute_package_statistics(df)

-         stats["totalsourcewords"] = df[["translatedsourcewords", "fuzzysourcewords", "untranslatedsourcewords"]].sum().sum()

+         stats["totalsourcewords"] = compute_package_totalsourcewords(stats["po"])

  

          with open(stats_file, "w") as f:

              json.dump(stats, f, indent=2, cls=NumpyEncoder)
@@ -211,14 +233,34 @@ 

      distribution_file = os.path.join(results_folder, "release.json")

      distribution_stats = dict()

      with open(os.path.join(results_folder, "data.json"), "r") as f:

-         distribution_stats["total_release_packages"] = len(json.load(f))

+         distribution_stats["packages_count"] = len(json.load(f))

+ 

+     # detected = identified with translation files

+     distribution_stats["packages_detected_count"] = len(packages)

+     distribution_stats["files_detected_count"] = sum([len(package["po"]) for package in all_stats])

+ 

+     # processed = what we were able to use

+     distribution_stats["packages_processed_count"] = 0

+     distribution_stats["files_processed_count"] = 0

+ 

+     for package in sorted(packages):

+         log.info(package)

+         stats_file = os.path.join(packages_folder, package, "stats.json")

+ 

+         with open(stats_file, "r") as f:

+             stats = json.load(f)

+ 

+         # if there is no source words, it means we were not able to process anything

+         if "totalsourcewords" in stats.keys():

+             if stats["totalsourcewords"] > 0:

+                 distribution_stats["packages_processed_count"] += 1

+ 

+             for _, detected in stats["po"].items():

+                 if detected["lang_code_chosen"] != "error":

+                     distribution_stats["files_processed_count"] += 1

  

      distribution_stats["totalsourcewords"] = total_distribution_source_words

-     distribution_stats["total_packages_with_stats"] = len(packages)

-     distribution_stats["total_packages_files"] = sum([len(package["po"]) for package in all_stats])

-     distribution_stats["total_packages"] = len(packages)

-     distribution_stats["nb_files"] = len([file for file in all_stats if file.get("could_not_process", 0) == 0])

-     distribution_stats["total_languages"] = len(languages)

+     distribution_stats["languages_processed_count"] = len(languages)

  

      log.info(distribution_stats)

  
@@ -230,7 +272,7 @@ 

  

  

  def get_po_translation_level(file: str) -> dict:

-     """ Compute results """

+     """ Call pocount to get translation stats for a file """

      log = logging.getLogger("buildStats.get_po_translation_level")

      command = ["sed", "-i", "/^#$/d;/^#[^\:\~,\.]/d", file]

      subprocess.run(command, check=True, capture_output=True)

file modified
+7 -7
@@ -1,5 +1,5 @@ 

  #!/usr/bin/env python3

- """Consolidate each po files into compendium"""

+ """ Creates useful translator files for every language """

  

  import argparse

  import gzip
@@ -17,7 +17,7 @@ 

      """Handle params"""

  

      parser = argparse.ArgumentParser(

-         description="Creates useful translator files for every languages"

+         description="Creates useful translator files for every language"

      )

  

      parser.add_argument(
@@ -52,7 +52,7 @@ 

      if os.path.exists(tm_folder) is False:

          os.makedirs(tm_folder)

  

-     log.info("Building the translation memory for every languages")

+     log.info("Find detected languages")

  

      languages = [

          f for f in os.listdir(lang_path) if os.path.isfile(os.path.join(lang_path, f))
@@ -222,12 +222,12 @@ 

      subprocess.run(command, check=True, capture_output=True)

  

  

- def process_terminology(source: str, destination: str) -> None:

-     """ Generate a termonology from a po file """

+ def process_terminology(compendium: str, destination: str) -> None:

+     """ Generate a terminology from a po file """

  

      command = ["poterminology", "--ignore-case", "--fold-titlecase",

                 "--inputs-needed", "1",

-                "--progress=none", source, f"--output={destination}"]

+                "--progress=none", compendium, f"--output={destination}"]

      subprocess.run(command, check=True, capture_output=True)

  

  
@@ -255,7 +255,7 @@ 

  

  

  def compress(source: str, destination_file: str) -> None:

-     """ Compress files uzing gzip """

+     """ Compress files using gzip """

      log = logging.getLogger("buildTm.compress")

  

      log.debug(f"Compressing {source}")

file modified
+96 -134
@@ -1,5 +1,5 @@ 

  #!/usr/bin/env python3

- """Consolidate each po files into compendium"""

+ """ Generate static asciidoc pages from generated statistics """

  

  import argparse

  import datetime
@@ -17,13 +17,12 @@ 

  

  def get_territories_for_language(language_name: str, cldr_languages: dict) -> list:

      log = logging.getLogger("buildWebsite.get_territory")

-     territories = []

  

      code = language_name.split("_", 1)[0]  # ro_MD or zh_Hant_HK

      code = code.split("@", 1)[0]  # ca@valencia

  

-     territories.append(cldr_languages.get(code, {}).get("_territories", []))

-     territories.append(cldr_languages.get(code + "-alt-secondary", {}).get("_territories", []))

+     territories = cldr_languages.get(code, {}).get("_territories", [])

+     territories = territories + cldr_languages.get(code + "-alt-secondary", {}).get("_territories", [])

  

      # if language contains a territory code, then only keep this one

      if len(language_name.split("_")) > 1:
@@ -31,6 +30,7 @@ 

              territories = [language_name.split("_")[-1]]

  

      if len(territories) == 0:

+         territories = ["not-found-in-cldr"]

          log.warning(f"The language {code} does not exist in territories data from CLDR")

  

      return territories
@@ -39,7 +39,7 @@ 

  def main():

      """Handle params"""

  

-     parser = argparse.ArgumentParser(description="")

+     parser = argparse.ArgumentParser(description="Generate static asciidoc pages from generated statistics")

  

      parser.add_argument(

          "--results",
@@ -95,6 +95,7 @@ 

      log.info("Load CLDR data")

      with open("CLDR-raw/languageData.json", "r") as read_file:

          cldr_languages = json.load(read_file)

+         cldr_version = cldr_languages["supplemental"]["version"]["_cldrVersion"]

          cldr_languages = cldr_languages["supplemental"]["languageData"]

  

      with open("CLDR-raw/territories.json", "r") as read_file:
@@ -114,12 +115,14 @@ 

      for language_file in sorted(languages):

          language = language_file[: -len(".json")]

          stats_file = os.path.join(langs_stats, language_file)

-         destination_file = os.path.join(static_langs_folder, f"{language}.adoc")

+         destination_file = os.path.join(static_langs_folder, f"{language}.md")

  

          with open(stats_file, "r") as read_file:

              content = json.load(read_file)

  

-         pd.DataFrame(content["packages"]).to_csv(os.path.join(static_tm_folder, f"{language}.csv"), index=False)

+         pd.DataFrame\

+             .from_dict(content['packages'], orient="index")\

+             .to_csv(os.path.join(static_tm_folder, f"{language}.csv"), index_label="package")

  

          territories = get_territories_for_language(language, cldr_languages)

          generate_static_pages_langs(args.results, language, content, destination_file, territories, tm_folder, static_tm_folder)
@@ -132,7 +135,7 @@ 

      ]

      for package in sorted(packages):

          stats_file = os.path.join(packages_stats, package, "stats.json")

-         destination_file = os.path.join(static_pkgs_folder, f"{package}.adoc")

+         destination_file = os.path.join(static_pkgs_folder, f"{package}.md")

  

          with open(stats_file, "r") as read_file:

              content = json.load(read_file)
@@ -140,20 +143,20 @@ 

          generate_static_pages_packages(args.results, package, content, destination_file)

  

      log.info("Generating indexes")

-     package_statistics_file = os.path.join(static_folder, "_index.adoc")

+     package_statistics_file = os.path.join(static_folder, "_index.md")

      generate_release_index(args.results, package_statistics_file, distribution_stats)

  

-     package_statistics_file = os.path.join(static_langs_folder, "_index.adoc")

+     package_statistics_file = os.path.join(static_langs_folder, "_index.md")

      generate_language_index(args.results, package_statistics_file)

  

-     package_statistics_file = os.path.join(static_pkgs_folder, "_index.adoc")

+     package_statistics_file = os.path.join(static_pkgs_folder, "_index.md")

      generate_package_index(args.results, package_statistics_file)

  

      for code in cldr_territories.keys():

          # prevent containers and alternative names to be included

          if code in cldr_territories_info.keys():

-             package_statistics_file = os.path.join(static_territories_folder, code, "_index.adoc")

-             generate_territory_index(package_statistics_file, cldr_territories[code], code, cldr_territories_info.get(code, {}))

+             package_statistics_file = os.path.join(static_territories_folder, code, "_index.md")

+             generate_territory_index(args.results, package_statistics_file, cldr_territories[code], code, cldr_territories_info.get(code, {}), cldr_version)

  

      log.info("Copy translation memories")

      languages = [
@@ -164,109 +167,12 @@ 

              shutil.copyfile(

                  os.path.join(tm_folder, lang), os.path.join(static_tm_folder, lang)

              )

- 

+     

      log.info("done")

  

  

- def consolidate_package_stats(stats_file, package_folder):

-     """ From a CSV file, return key indicators """

-     log = logging.getLogger("buildWebsite.consolidate_package_stats")

-     results = dict()

- 

-     fieldnames = {

-         "filename": "str",

-         "translatedsourcewords": "int",

-         "fuzzysourcewords": "int",

-         "untranslatedsourcewords": "int",

-         "translated": "int",

-         "fuzzy": "int",

-         "untranslated": "int",

-         "translatedtargetwords": "int",

-         "team": "str",

-         "totalsourcewords": "int",

-     }

- 

-     _json = json.load(open(stats_file))

-     dfs = []

-     total_source_words = 0

- 

-     for template in _json.keys():

-         tmp_df = pd.DataFrame.from_dict(_json.get(template), orient="index")

-         tmp_df.fillna(0, inplace=True)

-         tmp_df.reset_index(level=0, inplace=True)

- 

-         # sometimes, no file were found, which means no stats can be used

-         if len(tmp_df) == 0:

-             log.debug(f" The template {template} for {stats_file} is empty")

-             continue

- 

-         tmp_df["totalsourcewords"] = (

-                 tmp_df["untranslatedsourcewords"] + tmp_df["translatedsourcewords"]

-         )

-         tmp_df.columns = fieldnames.keys()

- 

-         total_source_words += max(tmp_df["totalsourcewords"])

- 

-         dfs.append(tmp_df)

- 

-     if len(dfs) > 1:

-         stats_df = pd.concat(dfs)

-     elif len(dfs) == 0:

-         log.debug(f"There is no stats for {stats_file}")

-         return results

-     else:

-         stats_df = dfs[0]

- 

-     temp_translated = (

-         stats_df.groupby(["lang_code"])

-             .agg(

-             {

-                 "translatedsourcewords": ["sum"],

-             }

-         )

-             .reset_index()

-             .droplevel(1, axis=1)

-     )

- 

-     temp_teams = stats_df.groupby("lang_code")["team"].apply(

-         lambda x: ", ".join(x.drop_duplicates())

-     )

-     temp_files = stats_df.groupby("lang_code")["filename"].apply(

-         lambda x: ",".join(x)

-     )

-     temp_bis = pd.merge(temp_teams, temp_files, how="inner", on="lang_code")

-     temp = pd.merge(temp_translated, temp_bis, how="inner", on="lang_code").to_dict(

-         orient="records"

-     )

- 

-     for line in temp:

-         line["progress"] = 0

-         line["translated"] = line["translatedsourcewords"]

- 

-         if total_source_words == 0:

-             log.info(f" File {stats_file} for file has translatedsourcewords = 0 in line {line}")

-             line["progress"] = 0

-             continue

-         try:

-             line["progress"] = round(

-                 (int(line["translatedsourcewords"]) / total_source_words) * 100

-             )

-         except OverflowError:

-             log.info(f" {stats_file} have Translated={line['translatedsourcewords']} and Source={total_source_words}")

- 

-         line["filename"] = line["filename"].split(",")

- 

-     results["stats"] = list()

-     for line in sorted(temp, key=lambda k: k["progress"], reverse=True):

-         del line["translatedsourcewords"]

-         results["stats"].append(line)

- 

-     results["totalsourcewords"] = total_source_words

- 

-     return results

- 

- 

- def generate_static_pages_langs(results: str, code: str, content: dict, destination_file: str, territories: list[str], tm_folder: str, static_tm_folder: str) -> None:

+ def generate_static_pages_langs(release: str, code: str, content: dict, destination_file: str, territories: list[str], tm_folder: str, static_tm_folder: str) -> None:

+     """ Aggregate info and call language template """

      log = logging.getLogger("buildWebsite.generate_static_pages_langs")

      data = content

      data["lang_name_en"] = langtable.language_name(
@@ -274,80 +180,136 @@ 

      )

      data["lang_name_local"] = langtable.language_name(languageId=code)

      data["scripts"] = langtable.list_scripts(languageId=code)

-     data["results"] = results

+     data["release"] = release

      data["lang_code"] = code

      data["now"] = datetime.datetime.utcnow()

      data["files"] = defaultdict(dict)

-     data["files"]["compendium"]["url"] = f"/{results}/{code}.po.gz"

+     data["files"]["compendium"]["url"] = f"/{release}/{code}.po.gz"

      data["files"]["compendium"]["size"] = os.path.getsize(os.path.join(tm_folder, f"{code}.po.gz"))

-     data["files"]["terminology"]["url"] = f"/{results}/{code}.terminology.po.gz"

+     data["files"]["terminology"]["url"] = f"/{release}/{code}.terminology.po.gz"

      data["files"]["terminology"]["size"] = os.path.getsize(os.path.join(tm_folder, f"{code}.terminology.po.gz"))

-     data["files"]["tmx"]["url"] = f"/{results}/{code}.tmx.gz"

+     data["files"]["tmx"]["url"] = f"/{release}/{code}.tmx.gz"

      data["files"]["tmx"]["size"] = os.path.getsize(os.path.join(tm_folder, f"{code}.tmx.gz"))

-     data["files"]["csv"]["url"] = f"/{results}/{code}.csv"

+     data["files"]["csv"]["url"] = f"/{release}/{code}.csv"

      data["files"]["csv"]["size"] = os.path.getsize(os.path.join(static_tm_folder, f"{code}.csv"))

      if len(territories) > 0:

          data["territories"] = territories

  

-     apply_jinja_template(data, destination_file, "language.adoc")

+     data["could_not_process_count"] = sum(value["could_not_process"] == 1 for value in data["po"])

+     data["polib_error_count"] = sum(value["polib_error"] != "" for value in data["po"])

+ 

+     # remove local path

+     for file in data["po"]:

+         path = f"./results/{release}/packages/{file['package']}/"

+         file["filename"] = file["filename"].replace(path, " ")

+ 

+     # sort content

+     data["packages"] = sorted(data["packages"].items(), key=lambda x: x[1]['progress'], reverse=True)

+ 

+     apply_jinja_template(data, destination_file, "language.md")

  

  

- def generate_static_pages_packages(release: str, package: str, statistics, destination_file):

+ def generate_static_pages_packages(release: str, package: str, statistics: dict, destination_file: str) -> None:

+     """ Aggregate info and call package template """

      log = logging.getLogger("buildWebsite.generate_static_pages_packages")

      data = statistics

-     data["results"] = release

+     data["release"] = release

      data["package"] = package

      data["now"] = datetime.datetime.utcnow()

  

-     apply_jinja_template(data, destination_file, "package.adoc")

+     # in some rare cases, a package may have no translation progress

+     if "stats" not in statistics.keys():

+         data["stats"] = {}

+         data["stats"]["languages"] = {}

+ 

+     # in some rare cases, a package may have no file

+     if "po" not in statistics.keys():

+         data["po"] = {}

+ 

+     if "error" in data["stats"]["languages"].keys():

+         data["started_languages"] = len(data["stats"]["languages"]) - 1

+         data["no_languages"] = len(data["stats"]["languages"]["error"]["filename"].split("./")) - 1

+     else:

+         data["started_languages"] = len(data["stats"]["languages"])

+         data["no_languages"] = 0

  

+     data["could_not_process_count"] = sum(data["po"][value]["could_not_process"] == 1 for value in data["po"])

+     data["polib_error_count"] = sum(data["po"][value]["polib_error"] != "" for value in data["po"])

  

- def generate_release_index(release: str, destination_file: str, data: dict):

+     # remove local path

+     path = f"./results/{release}/packages/{package}/"

+ 

+     for lang in data["stats"]["languages"].keys():

+         data["stats"]["languages"][lang]["filename"] = data["stats"]["languages"][lang]["filename"].replace(path, " ")

+ 

+     for file, stats in data["po"].copy().items():

+         data["po"][file.replace(path, " ")] = stats

+         del data["po"][file]

+ 

+     # sort content

+     data["stats"]["languages"] = sorted(data["stats"]["languages"].items(), key=lambda x: x[1]['progress'], reverse=True)

+ 

+     apply_jinja_template(data, destination_file, "package.md")

+ 

+ 

+ def generate_release_index(release: str, destination_file: str, data: dict) -> None:

+     """ Aggregate info and call release index template """

      log = logging.getLogger("buildWebsite.generate_release_index")

      data["release"] = release

      data["now"] = datetime.datetime.utcnow()

  

-     apply_jinja_template(data, destination_file, "_index.release.adoc")

+     apply_jinja_template(data, destination_file, "_index.release.md")

  

  

- def generate_language_index(release: str, destination_file: str):

+ def generate_language_index(release: str, destination_file: str) -> None:

+     """ Aggregate info and call language index template """

      log = logging.getLogger("buildWebsite.generate_language_index")

      data = dict()

      data["release"] = release

      data["now"] = datetime.datetime.utcnow()

  

-     apply_jinja_template(data, destination_file, "_index.language.adoc")

+     apply_jinja_template(data, destination_file, "_index.language.md")

  

  

- def generate_package_index(distribution: str, destination_file: str):

+ def generate_package_index(distribution: str, destination_file: str) -> None:

+     """ Aggregate info and call package index template """

      log = logging.getLogger("buildWebsite.generate_package_index")

      data = dict()

-     data["distribution"] = distribution

+     data["release"] = distribution

      data["now"] = datetime.datetime.utcnow()

  

-     apply_jinja_template(data, destination_file, "_index.package.adoc")

+     apply_jinja_template(data, destination_file, "_index.package.md")

  

  

- def generate_territory_index(destination_file: str, name: list[str], code: str, data: dict):

+ def generate_territory_index(release: str, destination_file: str, name: list[str], code: str, data: dict, cldr_version: str) -> None:

+     """ Aggregate info and call territory index template """

      log = logging.getLogger("buildWebsite.generate_package_index")

      data["name"] = name

      data["code"] = code

+     data["cldr_version"] = cldr_version

+     data["release"] = release

  

-     apply_jinja_template(data, destination_file, "_index.territory.adoc")

+     apply_jinja_template(data, destination_file, "_index.territory.md")

  

  

- def store_json_file(content, destination_file):

+ def store_json_file(content: dict, destination_file: str) -> None:

+     """ Store a json file"""

      with open(destination_file, "w") as f:

          f.write(json.dumps(content, indent=2))

  

  

- def apply_jinja_template(data: dict, destination_file: str, template_file: str):

+ def apply_jinja_template(data: dict, destination_file: str, template_file: str) -> None:

+     """ Call a jinja template with a data dictionary """

      os.makedirs(os.path.dirname(os.path.abspath(destination_file)), exist_ok=True)

  

      template_loader = jinja2.FileSystemLoader(searchpath="./templates/")

      template_env = jinja2.Environment(loader=template_loader, undefined=jinja2.Undefined)

      template = template_env.get_template(template_file)

-     output_text = template.render(data)

+     try:

+         output_text = template.render(data)

+     except jinja2.exceptions.UndefinedError as e:

+         logging.error(f"Error with {destination_file}: {e}")

+         raise

  

      with open(destination_file, "w") as write_out:

          write_out.write(output_text)

dnf/dnf_37.conf dnf/dnf_stg_37.conf
file renamed
file was moved with no change to the file
dnf/dnf_38.conf dnf/dnf_stg_38.conf
file renamed
file was moved with no change to the file
dnf/dnf_39.conf dnf/dnf_stg_39.conf
file renamed
file was moved with no change to the file
file modified
+2
@@ -5,3 +5,5 @@ 

  weblate-language-data

  langtable

  translate-toolkit

+ 

+ numpyencoder 

\ No newline at end of file

templates/_index.language.md templates/_index.language.adoc
file renamed
+1
@@ -1,5 +1,6 @@ 

  ---

  title: "Languages for {{ release }}"

+ release: {{ release }}

  date: {{ now }}

  layout: "list_languages"

  --- 

\ No newline at end of file

templates/_index.package.md templates/_index.package.adoc
file renamed
+2 -1
@@ -1,7 +1,8 @@ 

  ---

- title: "Packages for {{ distribution }}"

+ title: "Packages for {{ release }}"

  date: {{ now }}

  layout: "list_packages"

+ release: {{ release }}

  ---

  

  This listing aims at making it easy to find packages with files for which no languages could be deducted. 

\ No newline at end of file

@@ -1,19 +0,0 @@ 

- ---

- title: "Statistics for {{ release }}"

- date: {{ now }}

- layout: "release"

- ---

- 

- Fedora {{ release }}::

- * contains {{ total_release_packages }} packages,

- * we identified {{ total_packages }} packages with translations files,

- * it represents {{ total_packages_files }} translations files (po).

- 

- What we were able to process::

- * {{ total_packages }} packages,

- * {{ nb_files }} translation files containing {{ totalsourcewords }} words to translate,

- * {{ total_languages }} languages.

- 

- Why such gaps?::

- . File reading was not possible (encoding or format issue),

- . Language detection failed (missing information). 

\ No newline at end of file

@@ -0,0 +1,20 @@ 

+ ---

+ title: "Statistics for {{ release }}"

+ date: {{ now }}

+ layout: "release"

+ release: {{ release }}

+ ---

+ 

+ Fedora {{ release }}:

+ * contains {{ packages_count }} packages,

+ * we identified {{ packages_detected_count }} packages with translations files,

+ * it represents {{ files_detected_count }} translations files (po).

+ 

+ What we were able to process:

+ * {{ packages_processed_count }} packages,

+ * {{ files_processed_count }} translation files containing {{ totalsourcewords }} words to translate,

+ * {{ languages_processed_count }} languages.

+ 

+ Why such gaps?

+ * File reading was not possible (encoding or format issue),

+ * Language detection failed (missing information). 

\ No newline at end of file

templates/_index.territory.md templates/_index.territory.adoc
file renamed
+2 -1
@@ -1,7 +1,8 @@ 

  ---

  title: "{{ code }} {{ name }}"

+ release: {{ release }}

  ---

- Data coming from Unicode consortium (CLDR 38):

+ Data coming from Unicode consortium (CLDR {{ cldr_version }}):

  

  * Population: {{ _population }}

  * Literacy percent: {{_literacyPercent}}

file removed
-52
@@ -1,52 +0,0 @@ 

- ---

- title: "{{ lang_code }}-{{ lang_name_en }} ({{ lang_name_local }})"

- date: {{ now }}

- code: {{ lang_code }}

- name_english: {{ lang_name_en }}

- name_local: {{ lang_name_local }}

- progress_d: {{ progress_d }}

- release: {{ results }}

- {%- if territories %}

- territories:

- {%- for territory in territories %}

-  - {{ territory }}

- {%- endfor %}

- {%- endif %}

- ---

- 

- Language progress for {{ lang_name_en }} ({{ lang_code }}) in Fedora {{ results }} is:

- 

- * {{ progress }}% when we only look on started packages for this language.

- * {{ progress_d }}% when we compare to every single translatable string in Fedora {{ results }}.

- 

- Possible scripts are: {% for script in scripts -%}{{ script }} {%- endfor %}

- 

- * Total translatable string in Fedora {{ results }}: {{ totalsourcewords_d }}

- * Source words to translate in started packages: {{ totalsourcewordssum }}

- * Translated words: {{ translatedsourcewords }}

- 

- Download:

- 

- * link:{{ "{{% resource url=" }}"{{ files["compendium"]["url"] }}" {{ "%}}" }}[{{ lang_code }} compendium ({{ files["compendium"]["size"]|filesizeformat() }})] (aggregation of all strings found in po files)

- * link:{{ "{{% resource url=" }}"{{ files["terminology"]["url"] }}" {{ "%}}" }}[{{ lang_code }} terminology ({{ files["terminology"]["size"]|filesizeformat() }})] see https://docs.translatehouse.org/projects/translate-toolkit/en/latest/commands/poterminology.html[poterminology]

- * link:{{ "{{% resource url=" }}"{{ files["tmx"]["url"] }}" {{ "%}}" }}[{{ lang_code }} translation memory ({{ files["tmx"]["size"]|filesizeformat() }})] see https://en.wikipedia.org/wiki/Translation_Memory_eXchange[tmx]

- * link:{{ "{{% resource url=" }}"{{ files["csv"]["url"] }}" {{ "%}}" }}[{{ lang_code }} generated stats ({{ files["csv"]["size"]|filesizeformat() }})]

- 

- Packages:

- 

- [cols="1a,1,1,1,3", options="header"]

- |===

- | Name

- | Translated words

- | Total source words

- | Progress

- | Language teams

- 

- {% for package in packages -%}

- | link:{{ '{{' }}< ref "/{{ results }}/package/{{ package }}.adoc" >{{ '}}' }}[{{ package }}]

- >| {{ packages[package].translatedsourcewords }}

- >| {{ packages[package].totalsourcewordssum }}

- >| {{ packages[package].progress }}

- | {{ packages[package].team }}

- {% endfor %}

- |=== 

\ No newline at end of file

@@ -0,0 +1,89 @@ 

+ ---

+ title: "{{ lang_code }}-{{ lang_name_en }} ({{ lang_name_local }}) - translation progress for {{ release }}"

+ date: {{ now }}

+ code: {{ lang_code }}

+ name_english: {{ lang_name_en }}

+ name_local: {{ lang_name_local }}

+ progress: {{ '{:.2f}'.format(progress) }}

+ progress_d: {{ '{:.2f}'.format(progress_d) }}

+ release: {{ release }}

+ {%- if territories %}

+ territories:

+ {%- for territory in territories %}

+  - {{ territory }}

+ {%- endfor %}

+ {%- endif %}

+ polib_error_count: {{ polib_error_count }}

+ could_not_process_count: {{ could_not_process_count }}

+ ---

+ 

+ Language progress for {{ lang_name_en }} ({{ lang_code }}) in Fedora {{ release }} is:

+ 

+ * {{ '{:.2f}'.format(progress) }}% when we only look on started packages for this language.

+ * {{ '{:.2f}'.format(progress_d) }}% when we compare to every single translatable string in Fedora {{ release }}.

+ 

+ Possible scripts are: {% for script in scripts -%}{{ script }} {%- endfor %}

+ 

+ * Total translatable string in Fedora {{ release }}: {{ totalsourcewords_d }}

+ * Source words to translate in started packages: {{ totalsourcewordssum }}

+ * Translated words: {{ translatedsourcewords }}

+ 

+ Download:

+ * [{{ lang_code }} compendium ({{ files["compendium"]["size"]|filesizeformat() }})]({{ "{{% resource url=" }}"{{ files["compendium"]["url"] }}" {{ "%}}" }}) (aggregation of all strings found in po files)

+ * [{{ lang_code }} terminology ({{ files["terminology"]["size"]|filesizeformat() }})]({{ "{{% resource url=" }}"{{ files["terminology"]["url"] }}" {{ "%}}" }}) see [poterminology](https://docs.translatehouse.org/projects/translate-toolkit/en/latest/commands/poterminology.html)

+ * [{{ lang_code }} translation memory ({{ files["tmx"]["size"]|filesizeformat() }})]({{ "{{% resource url=" }}"{{ files["tmx"]["url"] }}" {{ "%}}" }}) see [tmx](https://en.wikipedia.org/wiki/Translation_Memory_eXchange)

+ * [{{ lang_code }} generated stats ({{ files["csv"]["size"]|filesizeformat() }})]({{ "{{% resource url=" }}"{{ files["csv"]["url"] }}" {{ "%}}" }})

+ 

+ Packages:

+ 

+ | Name |  Translated words | Total source words | Progress (%) | Language teams |

+ |------|------------------:|-------------------:|-------------:|----------------|

+ {% for package, stats in packages %}

+ {%- set output = " | [" ~ package ~ "]({{< ref \"/" ~ release ~ "/package/" ~ package ~ ".md\" >}})" -%}

+ {%- set output = output ~ " | " ~ stats.translatedsourcewords -%}

+ {%- set output = output ~ " | " ~ stats.totalsourcewordssum -%}

+ {%- set output = output ~ " | " ~ '{:.1f}'.format(stats.progress) -%}

+ {%- set output = output ~ " | " ~ stats.team -%}

+ {%- set output = output ~ " | " -%}

+ {{ output }}

+ {% endfor -%}

+ {.sortable}

+ 

+ {% if polib_error_count > 0 or could_not_process_count > 0 %}

+ # Errors on PO files

+ 

+ {% if polib_error_count > 0 %}

+ ## Error with polib

+ We use the po metadata to get the language code and the team, but sometimes it fails, here are the files.

+ 

+ | Package | Lang from file path/name | polib error | Team |

+ |---------|--------------------------|-------------|------|

+ {% for file in po if file.polib_error != "" -%}

+ {%- set output = " | [" ~ file.package ~ "]({{< ref \"/" ~ release ~ "/package/" ~ file.package ~ ".md\" >}})" -%}

+ {%- set output = output ~ " | " ~ file.filename -%}

+ {%- set output = output ~ " | " ~ file.polib_error -%}

+ {%- set output = output ~ " | " ~ file.metadata_language_team -%}

+ {%- set output = output ~ " | " -%}

+ {{ output }}

+ {% endfor -%}

+ {.sortable}

+ {% endif %}

+ 

+ {% if could_not_process_count > 0 %}

+ ## Error with calcstats

+ 

+ We use the calcstats from translate toolkit to get the translation progress, but sometimes it fails, here are the files:

+ 

+ | Package | Lang from file path/name | Team |

+ |---------|--------------------------|------|

+ {% for file in po if file.could_not_process == 1 -%}

+ {%- set output = " | [" ~ file.package ~ "]({{< ref \"/" ~ release ~ "/package/" ~ file.package ~ ".md\" >}})" -%}

+ {%- set output = output ~ " | " ~ file.filename -%}

+ {%- set output = output ~ " | " ~ file.metadata_language_team -%}

+ {%- set output = output ~ " | " -%}

+ {{ output }}

+ {% endfor -%}

+ {.sortable}

+ {% endif %}

+ 

+ {% endif %} 

\ No newline at end of file

file removed
-29
@@ -1,29 +0,0 @@ 

- ---

- title: "{{ package }}"

- date: {{ now }}

- started_languages: {{ stats|length }}

- no_languages: {{ no_languages|length }}

- ---

- The package {{ package }}:

- 

- * represents {{ totalsourcewords }} source words to be translated,

- * is translated into {{ stats.languages|length }} languages in Fedora {{ results }},

- * contains {{ no_languages|length }} files for which no languages could be deducted.

- 

- [cols="1a,1,1,1,3a", options="header"]

- |===

- | Language

- | Translated words

- | Total source words

- | Progress

- | Files

- 

- {% for stat in stats.languages|sort -%}

- | link:{{ '{{' }}< ref "/{{ results }}/language/{{ stat }}.adoc" >{{ '}}' }}[{{ stat }}]

- >| {{ stats.languages[stat].translatedsourcewords }}

- >| {{ stats.languages[stat].totalsourcewordssum }}

- >| {{ stats.languages[stat].progress }}

- | {{ stats.languages[stat].filename }}

- {% endfor %}

- 

- |===

file added
+85
@@ -0,0 +1,85 @@ 

+ ---

+ title: "{{ package }} - translation progress for {{ release }}"

+ package: {{ package }}

+ date: {{ now }}

+ started_languages: {{ started_languages }}

+ no_languages: {{ no_languages }}

+ polib_error_count: {{ polib_error_count }}

+ could_not_process_count: {{ could_not_process_count }}

+ release: {{ release }}

+ ---

+ The package {{ package }}:

+ 

+ * represents {{ totalsourcewords }} source words to be translated

+ * is translated into {{ stats.languages|length }} languages in Fedora {{ release }}

+ {% if no_languages > 0 %}* no languages could be deducted for {{ no_languages }} files {% endif %}

+ {% if polib_error_count > 0 %}* polib could not open {{ polib_error_count }} files to extract metadata{% endif %}

+ {% if could_not_process_count > 0 %}* contains {{ could_not_process_count }} po stats could not be processes{% endif %}

+ 

+ | Language | Translated words | Total source words | Progress (%) | Files |

+ |----------|-----------------:|-------------------:|-------------:|-------|

+ {% for lang, stat in stats.languages -%}

+ {%- set output = " | [" ~ lang ~ "]({{< ref \"/" ~ release ~ "/language/" ~ lang ~ ".md\" >}})" -%}

+ {%- set output = output ~ " | " ~ stat.translatedsourcewords -%}

+ {%- set output = output ~ " | " ~ stat.totalsourcewordssum -%}

+ {%- set output = output ~ " | " ~ '{:.1f}'.format(stat.progress) -%}

+ {%- set output = output ~ " | " ~ stat.filename -%}

+ {%- set output = output ~ " | " -%}

+ {{ output }}

+ {% endfor -%}

+ {.sortable}

+ 

+ {% if polib_error_count > 0 or could_not_process_count > 0 or no_languages > 0 %}

+ # Errors on PO files

+ {% if no_languages > 0 %}

+ ## Errors on language deduction

+ It were not possible to properly deduct the language code for the following files.

+ 

+ | Lang from file path/name | Lang from file path/name | team from po metadata | polib error |

+ |--------------------------|--------------------------|-----------------------|-------------|

+ {% for file in po if po[file].lang_code_chosen == "error" -%}

+ {%- set output = " | " ~ file -%}

+ {%- set output = output ~ " | " ~ po[file].lang_in_path -%}

+ {%- set output = output ~ " | " ~ po[file].metadata_language_team -%}

+ {%- set output = output ~ " | " ~ po[file].polib_error -%}

+ {%- set output = output ~ " | " -%}

+ {{ output }}

+ {% endfor -%}

+ {.sortable}

+ {% endif %}

+ 

+ {% if polib_error_count > 0 %}

+ ## Error with polib

+ We use the po metadata to get the language code and the team, but sometimes it fails, here are the files.

+ 

+ | Lang from file path/name | Lang code chosen | polib error | Team |

+ |--------------------------|------------------|-------------|------|

+ {% for file in po if po[file].polib_error != "" -%}

+ {%- set output = output ~ " | " ~ file -%}

+ {%- set output = output ~ " | " ~ po[file].lang_code_chosen -%}

+ {%- set output = output ~ " | " ~ po[file].polib_error -%}

+ {%- set output = output ~ " | " ~ po[file].metadata_language_team -%}

+ {%- set output = output ~ " | " -%}

+ {{ output }}

+ {% endfor -%}

+ {.sortable}

+ {% endif %}

+ 

+ {% if could_not_process_count > 0 %}

+ ## Error with calcstats

+ 

+ We use the calcstats from translate toolkit to get the translation progress, but sometimes it fails, here are the files:

+ 

+ | Package | Lang code chosen | Team |

+ |---------|------------------|------|

+ {% for file in po if po[file].could_not_process == 1 -%}

+ {%- set output = output ~ " | " ~ file -%}

+ {%- set output = output ~ " | " ~ po[file].lang_code_chosen -%}

+ {%- set output = output ~ " | " ~ po[file].metadata_language_team -%}

+ {%- set output = output ~ " | " -%}

+ {{ output }}

+ {% endfor -%}

+ {.sortable}

+ {% endif %}

+ 

+ {% endif %} 

\ No newline at end of file

file modified
+5 -13
@@ -11,18 +11,10 @@ 

  

  # build_tm.py

  

- Detecting missing files

- - en-compendium is missing

- - error-compendium is missing

- - gl-compendium is missing

- - nb_no-compendium is missing

- - sk-compendium is missing

- - zh_hant-compendium is missing

- 

- # build_stats.py

- 

- when %package%/stats.json is empty, make sure it is counted as an existing package for which we were not able to extract anything (release stats)

- 

- # global

+ remove terminology (someone who wants it can do it locally)

  

+ # website

  

+ territory: table per spoken languages

+ language: table across releases

+ add all languages without territories in CLDR to "ZZ/Unknown Region" 

\ No newline at end of file

file modified
+8 -10
@@ -9,17 +9,15 @@ 

  

  [Params]

  since = "2021"

+ favicon = "img/favicon.ico"

  

  [taxonomies]

  countries = "territories"

  

- [security]

-   enableInlineShortcodes = false

-   [security.exec]

-     allow = ['^dart-sass-embedded$', '^go$', '^npx$', '^postcss$', '^asciidoctor$']

-     osEnv = ['(?i)^(PATH|PATHEXT|APPDATA|TMP|TEMP|TERM)$']

-   [security.funcs]

-     getenv = ['^HUGO_']

-   [security.http]

-     methods = ['(?i)GET|POST']

-     urls = ['.*']

+ 

+ [markup]

+   [markup.goldmark]

+     [markup.goldmark.parser]

+       wrapStandAloneImageWithinParagraph = false

+       [markup.goldmark.parser.attribute]

+         block = true 

\ No newline at end of file

@@ -1,13 +1,13 @@ 

  <!DOCTYPE html>

  <html lang="{{ .Lang }}" itemscope itemtype="http://schema.org/WebPage">

    <head>

-     {{ partial "head.html" . }}

+     {{ partialCached "head.html" . (.Title | default .Site.Title) }}

    </head>

    <body>

-     {{ partial "nav.html" . }}

-     {{ block "header" . }}{{ partial "header.html" . }}{{ end }}

+     {{ partialCached "nav.html" . }}

+     {{ block "header" . }}{{ partialCached "header.html" . (.Title | default .Site.Title) }}{{ end }}

      {{ block "main" . }}{{ end }}

-     {{ partial "footer.html" . }}

+     {{ partialCached "footer.html" . }}

      {{ block "footer" . }}{{ end }}

    </body>

  </html>

@@ -5,80 +5,38 @@ 

    </div>

    <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">

    {{ .Content }}

- 

-       <table class="contents" id="languages">

+       <p>

+           Column explanation:

+           <ul>

+             <li>Progress vs started packages (%): compare translated source words to every source word detected in each package</li>

+             <li>Progress vs release: compare translated source words to every source word detected in {{ .Params.release }}</li>

+             <li>File parsing errors: file for which po stats could not be extracted</li>

+             <li>Polib errors: polib could not open the file to extract metadata</li>

+           </ul>

+       </p>

+       <table class="contents sortable" id="languages">

            <caption>Click on columns headers to sort values</caption>

        <tr>

-           <th onclick="sortTable(0)">code</th>

-           <th onclick="sortTable(1)">English name</th>

-           <th onclick="sortTable(2)">Local name</th>

-           <th onclick="sortTable(3)">Progress</th>

+           <th>Code</th>

+           <th>English name</th>

+           <th>Local name</th>

+           <th>Progress vs started packages (%)</th>

+           <th>Progress vs release (%)</th>

+           <th>File parsing errors</th>

+           <th>Polib errors</th>

        </tr>

        {{ range sort .Pages "Title" "asc" }}

          <tr>

              <td><a href="{{ .Permalink }}">{{ .Params.code }}</a></td>

              <td>{{ .Params.name_english }}</td>

              <td>{{ .Params.name_local }}</td>

+             <td>{{ .Params.progress }}</td>

              <td>{{ .Params.progress_d }}</td>

+             <td>{{ .Params.could_not_process_count }}</td>

+             <td>{{ .Params.polib_error_count }}</td>

          </tr>

        {{ end }}

      </table>

    </div>

  </div>

- <script>

- function sortTable(n) {

-   var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;

-   table = document.getElementById("languages");

-   switching = true;

-   // Set the sorting direction to ascending:

-   dir = "asc";

-   /* Make a loop that will continue until

-   no switching has been done: */

-   while (switching) {

-     // Start by saying: no switching is done:

-     switching = false;

-     rows = table.rows;

-     /* Loop through all table rows (except the

-     first, which contains table headers): */

-     for (i = 1; i < (rows.length - 1); i++) {

-       // Start by saying there should be no switching:

-       shouldSwitch = false;

-       /* Get the two elements you want to compare,

-       one from current row and one from the next: */

-       x = rows[i].getElementsByTagName("TD")[n];

-       y = rows[i + 1].getElementsByTagName("TD")[n];

-       /* Check if the two rows should switch place,

-       based on the direction, asc or desc: */

-       if (dir == "asc") {

-         if (x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase()) {

-           // If so, mark as a switch and break the loop:

-           shouldSwitch = true;

-           break;

-         }

-       } else if (dir == "desc") {

-         if (x.innerHTML.toLowerCase() < y.innerHTML.toLowerCase()) {

-           // If so, mark as a switch and break the loop:

-           shouldSwitch = true;

-           break;

-         }

-       }

-     }

-     if (shouldSwitch) {

-       /* If a switch has been marked, make the switch

-       and mark that a switch has been done: */

-       rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);

-       switching = true;

-       // Each time a switch is done, increase this count by 1:

-       switchcount ++;

-     } else {

-       /* If no switching has been done AND the direction is "asc",

-       set the direction to "desc" and run the while loop again. */

-       if (switchcount == 0 && dir == "asc") {

-         dir = "desc";

-         switching = true;

-       }

-     }

-   }

- }

- </script>

  {{ end }} 

\ No newline at end of file

@@ -11,19 +11,26 @@ 

              <li>Package: package name in Fedora operating system</li>

              <li>Languages: number of identified languages</li>

              <li>No language: number of translation files for which no language could be deducted</li>

+             <li>File parsing errors: file for which po stats could not be extracted</li>

+             <li>Polib errors: polib could not open the file to extract metadata</li>

            </ul>

        </p>

-           <table class="contents">

+           <table class="contents sortable">

+               <caption>Click on columns headers to sort values</caption>

            <tr>

                <th>Package</th>

                <th>Language</th>

                <th>No language</th>

+               <th>File parsing errors</th>

+               <th>Polib errors</th>

            </tr>

            {{ range sort .Pages "Title" "asc" }}

              <tr>

-                 <td><a href="{{ .Permalink }}">{{ .Title }}</a></td>

+                 <td><a href="{{ .Permalink }}">{{ .Params.package }}</a></td>

                  <td>{{ .Params.started_languages }}</td>

                  <td>{{ .Params.no_languages }}</td>

+                 <td>{{ .Params.could_not_process_count }}</td>

+                 <td>{{ .Params.polib_error_count }}</td>

              </tr>

            {{ end }}

        </table>

@@ -1,53 +1,16 @@ 

-   {{ if eq .Type "page" }}

-     {{ partial "page_meta.html" . }}

-   {{ end }}

  <footer>

    <div class="container">

      <div class="row">

        <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">

-         <ul class="list-inline text-center footer-links">

-           {{ range .Site.Data.beautifulhugo.social.social_icons }}

-             {{- if isset $.Site.Author .id }}

-               <li>

-                 <a href="{{ printf .url (index $.Site.Author .id) }}" title="{{ .title }}">

-                   <span class="fa-stack fa-lg">

-                     <i class="fas fa-circle fa-stack-2x"></i>

-                     <i class="{{ .icon }} fa-stack-1x fa-inverse"></i>

-                   </span>

-                 </a>

-               </li>

-             {{- end -}}

-           {{ end }}

-           {{ if .Site.Params.rss }}

-           <li>

-             <a href="{{ with .OutputFormats.Get "RSS" }}{{ .RelPermalink }}{{ end }}" title="RSS">

-               <span class="fa-stack fa-lg">

-                 <i class="fas fa-circle fa-stack-2x"></i>

-                 <i class="fas fa-rss fa-stack-1x fa-inverse"></i>

-               </span>

-             </a>

-           </li>

-           {{ end }}

-         </ul>

          <p class="credits copyright text-muted">

-           &copy;

-           {{ if .Site.Params.since }}

-             {{ .Site.Params.since }} - {{ .Site.LastChange.Format "2006" }}

-           {{ else }}

-             {{ .Site.LastChange.Format "2006" }}

-           {{ end }}

+           &copy; {{ .Site.Params.since }} - {{ .Site.LastChange.Format "2006" }}

  

            &nbsp;&bull;&nbsp;<a href="{{ "" | absLangURL }}">{{ .Site.Title }}</a> &nbsp;&bull;&nbsp; <a href="https://pagure.io/fedora-l10n/fedora-localization-statistics">Contribute!</a>

          <br />Content generation: {{ .Date | time.Format ":date_long" }} &nbsp;&bull;&nbsp; Website generation: {{ now.UTC | time.Format ":date_long" }}

          </p>

-         <!-- Please don't remove this, keep my open source work credited :) -->

-         <p class="credits theme-by text-muted">

-           {{ i18n "poweredBy" . | safeHTML }}

-         </p>

        </div>

      </div>

    </div>

  </footer>

  

- 

- {{- partial "footer_custom.html" . }}

+ {{- partialCached "footer_custom.html" . }}

@@ -1,41 +1,11 @@ 

- {{- if eq .Kind "taxonomyTerm" }}

-   {{- range $key, $value := .Data.Terms.ByCount }}

-     {{- $.Scratch.Add "most_used" (slice $value.Name) }}

-   {{- end }}

-   {{- if not ($.Scratch.Get "most_used") }}

-     {{- $description := printf "A full overview of all pages with %s, ordered by %s" .Data.Plural .Data.Singular | truncate 180 }}

-     {{- $.Scratch.Set "Description" $description }}

-   {{- else }}

-     {{- $description := printf "A full overview of all pages with %s, ordered by %s, such as: %s" .Data.Plural .Data.Singular ( delimit ( $.Scratch.Get "most_used" ) ", " ", and " ) | truncate 180 }}

-     {{- $.Scratch.Set "Description" $description }}

-   {{- end }}

- 

-   {{- $title := printf "Overview of all pages with %s, ordered by %s" .Data.Plural .Data.Singular }}

-   {{- $.Scratch.Set "Title" $title }}

- {{- else if eq .Kind "taxonomy" }}

-   {{- $description := printf "Overview of all pages with the %s #%s, such as: %s" .Data.Singular $.Title ( index .Pages 0).Title | truncate 160 }}

-   {{- $.Scratch.Set "Description" $description }}

- 

-   {{- $title := printf "Overview of all pages with the %s #%s" .Data.Singular $.Title }}

-   {{- $.Scratch.Set "Title" $title }}

- {{- else }}

-   {{- $.Scratch.Set "Description" ( .Description | default .Params.subtitle | default .Summary ) }}

-   {{- $.Scratch.Set "Title" ( .Title | default .Site.Title ) }}

- {{- end }}

- 

    <meta charset="utf-8" />

    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">

- <!-- Site Title, Description, Author, and Favicon -->

- {{- with ($.Scratch.Get "Title") }}

-   <title>{{ . }} - {{ $.Site.Title }}</title>

- {{- end }}

- {{- with ($.Scratch.Get "Description") }}

+ <!-- Site Title, Description, and Favicon -->

+   <title>{{ .Title | default .Site.Title }} - {{ $.Site.Title }}</title>

+ {{- with .Description }}

    <meta name="description" content="{{ . }}">

  {{- end }}

- {{- with .Site.Author.name }}

-   <meta name="author" content="{{ . }}"/>

- {{- end }}

  {{- with .Site.Params.favicon }}

    <link href='{{ . | absURL }}' rel='icon' type='image/x-icon'/>

  {{- end -}}
@@ -48,4 +18,4 @@ 

    <link rel="stylesheet" href="{{ "css/fonts.css" | absURL }}" />

    <link rel="stylesheet" href="{{ "css/bootstrap.min.css" | absURL }}" />

  

- {{- partial "head_custom.html" . }} 

\ No newline at end of file

+ {{- partialCached "head_custom.html" . }} 

\ No newline at end of file

@@ -4,6 +4,7 @@ 

  -->

  <link href="{{ "css/jqvmap.css" | absURL }}" media="screen" rel="stylesheet" type="text/css">

  

+ <script type="text/javascript" src="{{ "js/sorttable.js"  | absURL }}"></script>

  <script type="text/javascript" src="{{ "js/jquery.js"  | absURL }}"></script>

  <script type="text/javascript" src="{{ "js/jquery.vmap.js"  | absURL }}"></script>

  <script type="text/javascript" src="{{ "js/jquery.vmap.world.js"  | absURL }}" charset="utf-8"></script>

@@ -1,85 +1,12 @@ 

- {{ if .IsHome }}

-   {{ if .Site.Params.homeTitle }}{{ $.Scratch.Set "title" .Site.Params.homeTitle }}{{ else }}{{ $.Scratch.Set "title" .Site.Title }}{{ end }}

-   {{ if .Site.Params.subtitle }}{{ $.Scratch.Set "subtitle" .Site.Params.subtitle }}{{ end }}

-   {{ if .Site.Params.bigimg }}{{ $.Scratch.Set "bigimg" .Site.Params.bigimg }}{{ end }}

- {{ else }}

-   {{ $.Scratch.Set "title" .Title }}

-   {{ if .Params.subtitle }}{{ $.Scratch.Set "subtitle" .Params.subtitle }}{{ end }}

-   {{ if .Params.bigimg }}{{ $.Scratch.Set "bigimg" .Params.bigimg }}{{ end }}

- {{ end }}

- {{ $bigimg := $.Scratch.Get "bigimg" }}

- {{ $title := $.Scratch.Get "title" }}

- {{ $subtitle := $.Scratch.Get "subtitle" }}

- 

- {{ if or $bigimg $title }}

-   {{ if $bigimg }}

-     <div id="header-big-imgs" data-num-img={{len $bigimg}} 

-       {{range $i, $img := $bigimg}}

-          {{ if (fileExists $img.src)}} 

-           data-img-src-{{add $i 1}}="{{$img.src | absURL }}" 

-          {{else}}

-           data-img-src-{{add $i 1}}="{{$img.src}}" 

-          {{end}}

-          {{ if $img.desc}}data-img-desc-{{add $i 1}}="{{$img.desc}}"{{end}}

-       {{end}}></div>

-   {{ end }}

- 

-   <header class="header-section {{ if $bigimg }}has-img{{ end }}">

-     {{ if $bigimg }}

-       <div class="intro-header big-img">

-         {{ $subtitle := $.Scratch.Get "subtitle" }}

-         <div class="container">

-           <div class="row">

-             <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">

-               <div class="{{ .Type }}-heading">

-                 <h1>{{ with $.Scratch.Get "title" }}{{.}}{{ else }}<br/>{{ end }}</h1>

-                   {{ if $subtitle }}

-                     {{ if eq .Type "page" }}

-                       <hr class="small">

-                       <span class="{{ .Type }}-subheading">{{ $subtitle }}</span>

-                     {{ else }}

-                       <h2 class="{{ .Type }}-subheading">{{ $subtitle }}</h2>

-                     {{ end }}

-                   {{ end }}

-                   {{ if eq .Type "post" }}

-                     {{ partial "post_meta.html" . }}

-                   {{ end }}

-               </div>

-             </div>

-           </div>

-         </div>

-         <span class="img-desc" style="display: inline;"></span>

-       </div>

-     {{end}}

-     <div class="intro-header no-img">

-       <div class="container">

-         <div class="row">

-           <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">

-             <div class="{{ .Type }}-heading">

-               {{ if eq .Type "list" }}

-                 <h1>{{ if .Data.Singular }}#{{ end }}{{ .Title }}</h1>

-               {{ else }}

-                 <h1>{{ with $title }}{{.}}{{ else }}<br/>{{ end }}</h1>

-               {{ end }}

-               {{ if ne .Type "post" }}

-                 <hr class="small">

-               {{ end }}

-               {{ if $subtitle }}

-                 {{ if eq .Type "page" }}

-                   <span class="{{ .Type }}-subheading">{{ $subtitle }}</span>

-                 {{ else }}

-                   <h2 class="{{ .Type }}-subheading">{{ $subtitle }}</h2>

-                 {{ end }}

-               {{ end }}

-               {{ if eq .Type "post" }}

-                 {{ partial "post_meta.html" . }}

-               {{ end }}

-             </div>

-           </div>

+ <header class="header-section">

+   <div class="intro-header no-img">

+     <div class="container">

+       <div class="row">

+         <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">

+             <h1>{{ .Title | default .Site.Title }}</h1>

+             <hr class="small">

          </div>

        </div>

      </div>

-   </header>

- {{ else }}

-   <div class="intro-header"></div>

- {{ end }}

+   </div>

+ </header> 

\ No newline at end of file

@@ -1,8 +0,0 @@ 

- <div class="page-meta">

-   {{ $lastmodstr := default (i18n "dateFormat") .Site.Params.dateformat | .Lastmod.Format }}

-   {{ $datestr := default (i18n "dateFormat") .Site.Params.dateformat | .Date.Format }}

-   {{ if ne $datestr $lastmodstr }}

-     {{ $lastmodstr | i18n "lastModified"  }}

-   {{ end }}

- </div>

- 

@@ -1,29 +0,0 @@ 

- <span class="post-meta">

-   {{ $lastmodstr := default (i18n "dateFormat") .Site.Params.dateformat | .Lastmod.Format }}

-   {{ $datestr := default (i18n "dateFormat") .Site.Params.dateformat | .Date.Format }}

-   <i class="fas fa-calendar"></i>&nbsp;{{ $datestr | i18n "postedOnDate"}}

-   {{ if ne $datestr $lastmodstr }}

-     &nbsp;{{ $lastmodstr | i18n "lastModified"  }}

-   {{ end }}

-   {{ if .Site.Params.readingTime }}

-     &nbsp;|&nbsp;<i class="fas fa-clock"></i>&nbsp;{{ i18n "readingTime"}}{{ .ReadingTime }}&nbsp;{{ i18n "readTime" }}

-   {{ end }}

-   {{ if .Site.Params.wordCount }}

-     &nbsp;|&nbsp;<i class="fas fa-book"></i>&nbsp;{{ .WordCount }}&nbsp;{{ i18n "words" }}

-   {{ end }}

-   {{ if not .Site.Params.hideAuthor }}

-     {{ if .Params.author }}

-       &nbsp;|&nbsp;<i class="fas fa-user"></i>&nbsp;{{ .Params.author | safeHTML }}

-     {{ else }}

-       &nbsp;|&nbsp;<i class="fas fa-user"></i>&nbsp;{{ .Site.Author.name | safeHTML }}

-     {{ end }}

-   {{ end }}

-   {{ if .IsTranslated -}}

-     {{- $sortedTranslations := sort .Translations "Site.Language.Weight" -}}

-     {{- $links := apply $sortedTranslations "partial" "translation_link.html" "." -}}

-     {{- $cleanLinks := apply $links "chomp" "." -}}

-     {{- $linksOutput := delimit $cleanLinks (i18n "translationsSeparator") -}}

-     &nbsp;&bull;&nbsp;{{ i18n "translationsLabel" }}{{ $linksOutput }}

-   {{- end }}

- </span>

- 

@@ -1,24 +1,8 @@ 

  <article class="post-preview">

      <a href="{{ .Permalink }}">

          <h2 class="post-title">{{ .Title }}</h2>

-         {{ if .Params.subtitle }}

-         <h3 class="post-subtitle">

-             {{ .Params.subtitle }}

-         </h3>

-         {{ end }}

-         {{ if .Params.image }}

-         <img src="{{ .Params.image }}" alt="{{ .Title }}" class="img-title" />

-         {{ end }}

-         {{ if .Params.video }}

-         <video loop autoplay muted playsinline class="img-title">

-             <source src="{{ .Params.video }}">

-         </video>

-         {{ end }}

      </a>

  

-     <p class="post-meta">

-         {{ partial "post_meta.html" . }}

-     </p>

      <div class="post-entry">

          {{ if .Truncated }}

          {{ .Summary }}

@@ -13,7 +13,7 @@ 

              <li>First Fedora version: first fedora version with at least a partial coverage of this territory</li>

            </ul>

        </p>

-       <table class="contents">

+       <table class="contents sortable">

            <tr>

                <th>Territory</th>

                <th>First Fedora version</th>

@@ -176,7 +176,7 @@ 

  

  @media only screen and (min-width: 768px) {

    .navbar-custom {

-     padding: 20px 0;

+     height: 52px;

      -webkit-transition: background .5s ease-in-out,padding .5s ease-in-out;

      -moz-transition: background .5s ease-in-out,padding .5s ease-in-out;

      transition: background .5s ease-in-out,padding .5s ease-in-out;
@@ -556,7 +556,7 @@ 

  }

  @media only screen and (min-width: 768px) {

    .intro-header {

-     margin-top: 130px;

+     margin-top: 60px;

    }

    .intro-header.big-img {

      margin-top: 91px;  /* Full navbar is small navbar + 20px padding on each side when expanded */
@@ -565,11 +565,6 @@ 

    .intro-header.big-img .post-heading  {

      padding: 150px 0;

    }

-   .intro-header .page-heading h1,

-   .intro-header .tags-heading h1,

-   .intro-header .categories-heading h1 {

-     font-size: 80px;

-   }

    .intro-header .post-heading h1 {

      font-size: 50px;

    }

@@ -0,0 +1,494 @@ 

+ /*

+   SortTable

+   version 2

+   7th April 2007

+   Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/

+ 

+   Instructions:

+   Download this file

+   Add <script src="sorttable.js"></script> to your HTML

+   Add class="sortable" to any table you'd like to make sortable

+   Click on the headers to sort

+ 

+   Thanks to many, many people for contributions and suggestions.

+   Licenced as X11: http://www.kryogenix.org/code/browser/licence.html

+   This basically means: do what you want with it.

+ */

+ 

+ 

+ var stIsIE = /*@cc_on!@*/false;

+ 

+ sorttable = {

+   init: function() {

+     // quit if this function has already been called

+     if (arguments.callee.done) return;

+     // flag this function so we don't do the same thing twice

+     arguments.callee.done = true;

+     // kill the timer

+     if (_timer) clearInterval(_timer);

+ 

+     if (!document.createElement || !document.getElementsByTagName) return;

+ 

+     sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;

+ 

+     forEach(document.getElementsByTagName('table'), function(table) {

+       if (table.className.search(/\bsortable\b/) != -1) {

+         sorttable.makeSortable(table);

+       }

+     });

+ 

+   },

+ 

+   makeSortable: function(table) {

+     if (table.getElementsByTagName('thead').length == 0) {

+       // table doesn't have a tHead. Since it should have, create one and

+       // put the first table row in it.

+       the = document.createElement('thead');

+       the.appendChild(table.rows[0]);

+       table.insertBefore(the,table.firstChild);

+     }

+     // Safari doesn't support table.tHead, sigh

+     if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0];

+ 

+     if (table.tHead.rows.length != 1) return; // can't cope with two header rows

+ 

+     // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as

+     // "total" rows, for example). This is B&R, since what you're supposed

+     // to do is put them in a tfoot. So, if there are sortbottom rows,

+     // for backwards compatibility, move them to tfoot (creating it if needed).

+     sortbottomrows = [];

+     for (var i=0; i<table.rows.length; i++) {

+       if (table.rows[i].className.search(/\bsortbottom\b/) != -1) {

+         sortbottomrows[sortbottomrows.length] = table.rows[i];

+       }

+     }

+     if (sortbottomrows) {

+       if (table.tFoot == null) {

+         // table doesn't have a tfoot. Create one.

+         tfo = document.createElement('tfoot');

+         table.appendChild(tfo);

+       }

+       for (var i=0; i<sortbottomrows.length; i++) {

+         tfo.appendChild(sortbottomrows[i]);

+       }

+       delete sortbottomrows;

+     }

+ 

+     // work through each column and calculate its type

+     headrow = table.tHead.rows[0].cells;

+     for (var i=0; i<headrow.length; i++) {

+       // manually override the type with a sorttable_type attribute

+       if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col

+         mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);

+         if (mtch) { override = mtch[1]; }

+ 	      if (mtch && typeof sorttable["sort_"+override] == 'function') {

+ 	        headrow[i].sorttable_sortfunction = sorttable["sort_"+override];

+ 	      } else {

+ 	        headrow[i].sorttable_sortfunction = sorttable.guessType(table,i);

+ 	      }

+ 	      // make it clickable to sort

+ 	      headrow[i].sorttable_columnindex = i;

+ 	      headrow[i].sorttable_tbody = table.tBodies[0];

+ 	      dean_addEvent(headrow[i],"click", sorttable.innerSortFunction = function(e) {

+ 

+           if (this.className.search(/\bsorttable_sorted\b/) != -1) {

+             // if we're already sorted by this column, just

+             // reverse the table, which is quicker

+             sorttable.reverse(this.sorttable_tbody);

+             this.className = this.className.replace('sorttable_sorted',

+                                                     'sorttable_sorted_reverse');

+             this.removeChild(document.getElementById('sorttable_sortfwdind'));

+             sortrevind = document.createElement('span');

+             sortrevind.id = "sorttable_sortrevind";

+             sortrevind.innerHTML = stIsIE ? '&nbsp<font face="webdings">5</font>' : '&nbsp;&#x25B4;';

+             this.appendChild(sortrevind);

+             return;

+           }

+           if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) {

+             // if we're already sorted by this column in reverse, just

+             // re-reverse the table, which is quicker

+             sorttable.reverse(this.sorttable_tbody);

+             this.className = this.className.replace('sorttable_sorted_reverse',

+                                                     'sorttable_sorted');

+             this.removeChild(document.getElementById('sorttable_sortrevind'));

+             sortfwdind = document.createElement('span');

+             sortfwdind.id = "sorttable_sortfwdind";

+             sortfwdind.innerHTML = stIsIE ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';

+             this.appendChild(sortfwdind);

+             return;

+           }

+ 

+           // remove sorttable_sorted classes

+           theadrow = this.parentNode;

+           forEach(theadrow.childNodes, function(cell) {

+             if (cell.nodeType == 1) { // an element

+               cell.className = cell.className.replace('sorttable_sorted_reverse','');

+               cell.className = cell.className.replace('sorttable_sorted','');

+             }

+           });

+           sortfwdind = document.getElementById('sorttable_sortfwdind');

+           if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); }

+           sortrevind = document.getElementById('sorttable_sortrevind');

+           if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); }

+ 

+           this.className += ' sorttable_sorted';

+           sortfwdind = document.createElement('span');

+           sortfwdind.id = "sorttable_sortfwdind";

+           sortfwdind.innerHTML = stIsIE ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';

+           this.appendChild(sortfwdind);

+ 

+ 	        // build an array to sort. This is a Schwartzian transform thing,

+ 	        // i.e., we "decorate" each row with the actual sort key,

+ 	        // sort based on the sort keys, and then put the rows back in order

+ 	        // which is a lot faster because you only do getInnerText once per row

+ 	        row_array = [];

+ 	        col = this.sorttable_columnindex;

+ 	        rows = this.sorttable_tbody.rows;

+ 	        for (var j=0; j<rows.length; j++) {

+ 	          row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]];

+ 	        }

+ 	        /* If you want a stable sort, uncomment the following line */

+ 	        //sorttable.shaker_sort(row_array, this.sorttable_sortfunction);

+ 	        /* and comment out this one */

+ 	        row_array.sort(this.sorttable_sortfunction);

+ 

+ 	        tb = this.sorttable_tbody;

+ 	        for (var j=0; j<row_array.length; j++) {

+ 	          tb.appendChild(row_array[j][1]);

+ 	        }

+ 

+ 	        delete row_array;

+ 	      });

+ 	    }

+     }

+   },

+ 

+   guessType: function(table, column) {

+     // guess the type of a column based on its first non-blank row

+     sortfn = sorttable.sort_alpha;

+     for (var i=0; i<table.tBodies[0].rows.length; i++) {

+       text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]);

+       if (text != '') {

+         if (text.match(/^-?[Ł$¤]?[\d,.]+%?$/)) {

+           return sorttable.sort_numeric;

+         }

+         // check for a date: dd/mm/yyyy or dd/mm/yy

+         // can have / or . or - as separator

+         // can be mm/dd as well

+         possdate = text.match(sorttable.DATE_RE)

+         if (possdate) {

+           // looks like a date

+           first = parseInt(possdate[1]);

+           second = parseInt(possdate[2]);

+           if (first > 12) {

+             // definitely dd/mm

+             return sorttable.sort_ddmm;

+           } else if (second > 12) {

+             return sorttable.sort_mmdd;

+           } else {

+             // looks like a date, but we can't tell which, so assume

+             // that it's dd/mm (English imperialism!) and keep looking

+             sortfn = sorttable.sort_ddmm;

+           }

+         }

+       }

+     }

+     return sortfn;

+   },

+ 

+   getInnerText: function(node) {

+     // gets the text we want to use for sorting for a cell.

+     // strips leading and trailing whitespace.

+     // this is *not* a generic getInnerText function; it's special to sorttable.

+     // for example, you can override the cell text with a customkey attribute.

+     // it also gets .value for <input> fields.

+ 

+     if (!node) return "";

+ 

+     hasInputs = (typeof node.getElementsByTagName == 'function') &&

+                  node.getElementsByTagName('input').length;

+ 

+     if (node.getAttribute("sorttable_customkey") != null) {

+       return node.getAttribute("sorttable_customkey");

+     }

+     else if (typeof node.textContent != 'undefined' && !hasInputs) {

+       return node.textContent.replace(/^\s+|\s+$/g, '');

+     }

+     else if (typeof node.innerText != 'undefined' && !hasInputs) {

+       return node.innerText.replace(/^\s+|\s+$/g, '');

+     }

+     else if (typeof node.text != 'undefined' && !hasInputs) {

+       return node.text.replace(/^\s+|\s+$/g, '');

+     }

+     else {

+       switch (node.nodeType) {

+         case 3:

+           if (node.nodeName.toLowerCase() == 'input') {

+             return node.value.replace(/^\s+|\s+$/g, '');

+           }

+         case 4:

+           return node.nodeValue.replace(/^\s+|\s+$/g, '');

+           break;

+         case 1:

+         case 11:

+           var innerText = '';

+           for (var i = 0; i < node.childNodes.length; i++) {

+             innerText += sorttable.getInnerText(node.childNodes[i]);

+           }

+           return innerText.replace(/^\s+|\s+$/g, '');

+           break;

+         default:

+           return '';

+       }

+     }

+   },

+ 

+   reverse: function(tbody) {

+     // reverse the rows in a tbody

+     newrows = [];

+     for (var i=0; i<tbody.rows.length; i++) {

+       newrows[newrows.length] = tbody.rows[i];

+     }

+     for (var i=newrows.length-1; i>=0; i--) {

+        tbody.appendChild(newrows[i]);

+     }

+     delete newrows;

+   },

+ 

+   /* sort functions

+      each sort function takes two parameters, a and b

+      you are comparing a[0] and b[0] */

+   sort_numeric: function(a,b) {

+     aa = parseFloat(a[0].replace(/[^0-9.-]/g,''));

+     if (isNaN(aa)) aa = 0;

+     bb = parseFloat(b[0].replace(/[^0-9.-]/g,''));

+     if (isNaN(bb)) bb = 0;

+     return aa-bb;

+   },

+   sort_alpha: function(a,b) {

+     if (a[0]==b[0]) return 0;

+     if (a[0]<b[0]) return -1;

+     return 1;

+   },

+   sort_ddmm: function(a,b) {

+     mtch = a[0].match(sorttable.DATE_RE);

+     y = mtch[3]; m = mtch[2]; d = mtch[1];

+     if (m.length == 1) m = '0'+m;

+     if (d.length == 1) d = '0'+d;

+     dt1 = y+m+d;

+     mtch = b[0].match(sorttable.DATE_RE);

+     y = mtch[3]; m = mtch[2]; d = mtch[1];

+     if (m.length == 1) m = '0'+m;

+     if (d.length == 1) d = '0'+d;

+     dt2 = y+m+d;

+     if (dt1==dt2) return 0;

+     if (dt1<dt2) return -1;

+     return 1;

+   },

+   sort_mmdd: function(a,b) {

+     mtch = a[0].match(sorttable.DATE_RE);

+     y = mtch[3]; d = mtch[2]; m = mtch[1];

+     if (m.length == 1) m = '0'+m;

+     if (d.length == 1) d = '0'+d;

+     dt1 = y+m+d;

+     mtch = b[0].match(sorttable.DATE_RE);

+     y = mtch[3]; d = mtch[2]; m = mtch[1];

+     if (m.length == 1) m = '0'+m;

+     if (d.length == 1) d = '0'+d;

+     dt2 = y+m+d;

+     if (dt1==dt2) return 0;

+     if (dt1<dt2) return -1;

+     return 1;

+   },

+ 

+   shaker_sort: function(list, comp_func) {

+     // A stable sort function to allow multi-level sorting of data

+     // see: http://en.wikipedia.org/wiki/Cocktail_sort

+     // thanks to Joseph Nahmias

+     var b = 0;

+     var t = list.length - 1;

+     var swap = true;

+ 

+     while(swap) {

+         swap = false;

+         for(var i = b; i < t; ++i) {

+             if ( comp_func(list[i], list[i+1]) > 0 ) {

+                 var q = list[i]; list[i] = list[i+1]; list[i+1] = q;

+                 swap = true;

+             }

+         } // for

+         t--;

+ 

+         if (!swap) break;

+ 

+         for(var i = t; i > b; --i) {

+             if ( comp_func(list[i], list[i-1]) < 0 ) {

+                 var q = list[i]; list[i] = list[i-1]; list[i-1] = q;

+                 swap = true;

+             }

+         } // for

+         b++;

+ 

+     } // while(swap)

+   }

+ }

+ 

+ /* ******************************************************************

+    Supporting functions: bundled here to avoid depending on a library

+    ****************************************************************** */

+ 

+ // Dean Edwards/Matthias Miller/John Resig

+ 

+ /* for Mozilla/Opera9 */

+ if (document.addEventListener) {

+     document.addEventListener("DOMContentLoaded", sorttable.init, false);

+ }

+ 

+ /* for Internet Explorer */

+ /*@cc_on @*/

+ /*@if (@_win32)

+     document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");

+     var script = document.getElementById("__ie_onload");

+     script.onreadystatechange = function() {

+         if (this.readyState == "complete") {

+             sorttable.init(); // call the onload handler

+         }

+     };

+ /*@end @*/

+ 

+ /* for Safari */

+ if (/WebKit/i.test(navigator.userAgent)) { // sniff

+     var _timer = setInterval(function() {

+         if (/loaded|complete/.test(document.readyState)) {

+             sorttable.init(); // call the onload handler

+         }

+     }, 10);

+ }

+ 

+ /* for other browsers */

+ window.onload = sorttable.init;

+ 

+ // written by Dean Edwards, 2005

+ // with input from Tino Zijdel, Matthias Miller, Diego Perini

+ 

+ // http://dean.edwards.name/weblog/2005/10/add-event/

+ 

+ function dean_addEvent(element, type, handler) {

+ 	if (element.addEventListener) {

+ 		element.addEventListener(type, handler, false);

+ 	} else {

+ 		// assign each event handler a unique ID

+ 		if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;

+ 		// create a hash table of event types for the element

+ 		if (!element.events) element.events = {};

+ 		// create a hash table of event handlers for each element/event pair

+ 		var handlers = element.events[type];

+ 		if (!handlers) {

+ 			handlers = element.events[type] = {};

+ 			// store the existing event handler (if there is one)

+ 			if (element["on" + type]) {

+ 				handlers[0] = element["on" + type];

+ 			}

+ 		}

+ 		// store the event handler in the hash table

+ 		handlers[handler.$$guid] = handler;

+ 		// assign a global event handler to do all the work

+ 		element["on" + type] = handleEvent;

+ 	}

+ };

+ // a counter used to create unique IDs

+ dean_addEvent.guid = 1;

+ 

+ function removeEvent(element, type, handler) {

+ 	if (element.removeEventListener) {

+ 		element.removeEventListener(type, handler, false);

+ 	} else {

+ 		// delete the event handler from the hash table

+ 		if (element.events && element.events[type]) {

+ 			delete element.events[type][handler.$$guid];

+ 		}

+ 	}

+ };

+ 

+ function handleEvent(event) {

+ 	var returnValue = true;

+ 	// grab the event object (IE uses a global event object)

+ 	event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);

+ 	// get a reference to the hash table of event handlers

+ 	var handlers = this.events[event.type];

+ 	// execute each event handler

+ 	for (var i in handlers) {

+ 		this.$$handleEvent = handlers[i];

+ 		if (this.$$handleEvent(event) === false) {

+ 			returnValue = false;

+ 		}

+ 	}

+ 	return returnValue;

+ };

+ 

+ function fixEvent(event) {

+ 	// add W3C standard event methods

+ 	event.preventDefault = fixEvent.preventDefault;

+ 	event.stopPropagation = fixEvent.stopPropagation;

+ 	return event;

+ };

+ fixEvent.preventDefault = function() {

+ 	this.returnValue = false;

+ };

+ fixEvent.stopPropagation = function() {

+   this.cancelBubble = true;

+ }

+ 

+ // Dean's forEach: http://dean.edwards.name/base/forEach.js

+ /*

+ 	forEach, version 1.0

+ 	Copyright 2006, Dean Edwards

+ 	License: http://www.opensource.org/licenses/mit-license.php

+ */

+ 

+ // array-like enumeration

+ if (!Array.forEach) { // mozilla already supports this

+ 	Array.forEach = function(array, block, context) {

+ 		for (var i = 0; i < array.length; i++) {

+ 			block.call(context, array[i], i, array);

+ 		}

+ 	};

+ }

+ 

+ // generic enumeration

+ Function.prototype.forEach = function(object, block, context) {

+ 	for (var key in object) {

+ 		if (typeof this.prototype[key] == "undefined") {

+ 			block.call(context, object[key], key, object);

+ 		}

+ 	}

+ };

+ 

+ // character enumeration

+ String.forEach = function(string, block, context) {

+ 	Array.forEach(string.split(""), function(chr, index) {

+ 		block.call(context, chr, index, string);

+ 	});

+ };

+ 

+ // globally resolve forEach enumeration

+ var forEach = function(object, block, context) {

+ 	if (object) {

+ 		var resolve = Object; // default

+ 		if (object instanceof Function) {

+ 			// functions have a "length" property

+ 			resolve = Function;

+ 		} else if (object.forEach instanceof Function) {

+ 			// the object implements a custom forEach method so use that

+ 			object.forEach(block, context);

+ 			return;

+ 		} else if (typeof object == "string") {

+ 			// the object is a string

+ 			resolve = String;

+ 		} else if (typeof object.length == "number") {

+ 			// the object is array-like

+ 			resolve = Array;

+ 		}

+ 		resolve.forEach(object, block, context);

+ 	}

+ };

as it now works in staging, let's go live :)

1 new commit added

  • add missing js script
8 months ago

Pull-Request has been merged by darknao

8 months ago
Metadata
Changes Summary 36
+7 -1
file changed
Dockerfile
+2 -2
file changed
README.md
+11 -1
file changed
build.py
+10 -10
file changed
build_language_list.py
+87 -45
file changed
build_stats.py
+7 -7
file changed
build_tm.py
+96 -134
file changed
build_website.py
+0 -0
file renamed
dnf/dnf_stg_37.conf
dnf/dnf_37.conf
+0 -0
file renamed
dnf/dnf_stg_38.conf
dnf/dnf_38.conf
+0 -0
file renamed
dnf/dnf_stg_39.conf
dnf/dnf_39.conf
+2 -0
file changed
requirements.txt
+1 -0
file renamed
templates/_index.language.adoc
templates/_index.language.md
+2 -1
file renamed
templates/_index.package.adoc
templates/_index.package.md
-19
file removed
templates/_index.release.adoc
+20
file added
templates/_index.release.md
+2 -1
file renamed
templates/_index.territory.adoc
templates/_index.territory.md
-52
file removed
templates/language.adoc
+89
file added
templates/language.md
-29
file removed
templates/package.adoc
+85
file added
templates/package.md
+5 -13
file changed
todo.md
+8 -10
file changed
website/config.toml
+4 -4
file changed
website/themes/beautifulhugo/layouts/_default/baseof.html
+20 -62
file changed
website/themes/beautifulhugo/layouts/_default/list_languages.html
+9 -2
file changed
website/themes/beautifulhugo/layouts/_default/list_packages.html
+2 -39
file changed
website/themes/beautifulhugo/layouts/partials/footer.html
+4 -34
file changed
website/themes/beautifulhugo/layouts/partials/head.html
+1 -0
file changed
website/themes/beautifulhugo/layouts/partials/head_custom.html
+9 -82
file changed
website/themes/beautifulhugo/layouts/partials/header.html
-8
file removed
website/themes/beautifulhugo/layouts/partials/page_meta.html
-29
file removed
website/themes/beautifulhugo/layouts/partials/post_meta.html
+0 -16
file changed
website/themes/beautifulhugo/layouts/partials/post_preview.html
+1 -1
file changed
website/themes/beautifulhugo/layouts/territories/list.html
+2 -7
file changed
website/themes/beautifulhugo/static/css/main.css
+0 -0
file changed
website/themes/beautifulhugo/static/img/favicon.ico
+494
file added
website/themes/beautifulhugo/static/js/sorttable.js