diff --git a/.ci/template_website_translation_credits.json b/.ci/template_website_translation_credits.json new file mode 100644 index 0000000000000000000000000000000000000000..710895b69a374f12875e08a515c4b0fa171a63f5 --- /dev/null +++ b/.ci/template_website_translation_credits.json @@ -0,0 +1,10 @@ +{ + "website-translation-credits": { + "translation": [ + "Name" + ], + "proofreading": [ + "Name" + ] + } +} diff --git a/.ci/template_website_translation_credits_mandatory.json b/.ci/template_website_translation_credits_mandatory.json new file mode 100644 index 0000000000000000000000000000000000000000..740c3bb55336c67671ec7c4c4f462e004f92be24 --- /dev/null +++ b/.ci/template_website_translation_credits_mandatory.json @@ -0,0 +1,7 @@ +{ + "website-translation-credits": { + "translation": [ + "Name" + ] + } +} diff --git a/.ci/validate_json.py b/.ci/validate_json.py new file mode 100755 index 0000000000000000000000000000000000000000..9be99d152a0538b5d5795f1fad1e84d79210cedc --- /dev/null +++ b/.ci/validate_json.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# validate_json.py +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Copyright 2020 GunChleoc <fios@foramnagaidhlig.net> +# + +"""Checks whether all JSON files will parse. + +For files where we have templates defined, also performs checks on keys +and values. +""" + +from enum import Enum, auto +from pathlib import Path +import codecs +import json +import os.path +import sys +import re + + +class Checks(Enum): + """For configuring the checks to run on a JSON object.""" + KEYS_KNOWN = auto() + KEYS_COMPLETE = auto() + VALUE_TYPES = auto() + + +def check_types(key, template, json_object, filename): + """Checks that the data types match. + + Returns 1 on error, 0 if the check passed. + """ + + if key in template: + reference_value = template[key] + value = json_object[key] + if not isinstance(reference_value, type(value)): + print('Error in file %s:' % filename) + print("\t Wrong data type for '%s': Expected %s but got %s" % ( + key, str(type(reference_value)), str(type(value)))) + return 1 + return 0 + + +def check_key_known(key, template, filename): + """Checks that the given key is present in the given template. + + Returns 1 on error, 0 if the check passed. + """ + + if not key in template: + print('Error in file %s:' % filename) + print("\t Unknown key '%s'" % key) + return 1 + return 0 + + +def check_key_exists(key, json_object, filename): + """Checkes whether the given mandatory key is present in the json_object. + + Returns 1 on error, 0 if the check passed. + """ + + if not key in json_object: + print('Error in file %s:' % filename) + print("\t Missing mandatory key '%s'" % key) + return 1 + return 0 + + +def check_items(mandatory_template, complete_template, json_object, filename, checks): + """Runs the given checks on the json_object, using templates as reference. + + The filename is used for error output. Returns the number of errors + found. + """ + + errors = 0 + # Check that keys & their values are legal + for key in json_object: + if Checks.VALUE_TYPES in checks: + errors = errors + \ + check_types(key, complete_template, json_object, filename) + + if Checks.KEYS_KNOWN in checks: + errors = errors + check_key_known(key, complete_template, filename) + + # Iterate JSON objects and arrays and check sub-keys + value = json_object[key] + if isinstance(value, dict): + # We have a JSON object, check its keys + if key in complete_template: + # Ensure we don't crash if the key is not mandatory + mandatory_subtemplate = dict() + if key in mandatory_template: + mandatory_subtemplate = mandatory_template[key] + errors = errors + \ + check_items(mandatory_subtemplate, + complete_template[key], + json_object[key], + filename, + checks) + + elif isinstance(value, list): + # We have a JSON array + if key in complete_template: + complete_list = complete_template[key] + # Only check JSON array members if they are JSON objects + if complete_list and isinstance(complete_list[0], dict): + # Get expected keys from first reference object in JSON array + complete_subtemplate = complete_list[0] + # Ensure we don't crash if the key is not mandatory + mandatory_subtemplate = dict() + if key in mandatory_template: + mandatory_list = mandatory_template[key] + if mandatory_list and isinstance(mandatory_list[0], dict): + mandatory_subtemplate = mandatory_list[0] + # Now check all JSON array members + for item in json_object[key]: + errors = errors + \ + check_items(mandatory_subtemplate, + complete_subtemplate, + item, + filename, + checks) + + # Check we're not missing any keys + if Checks.KEYS_COMPLETE in checks: + # Ensure we have a dict in case of type mismatch + if isinstance(mandatory_template, dict): + for key in mandatory_template: + errors = errors + check_key_exists(key, json_object, filename) + return errors + + +def detect_duplicate_keys(ordered_pairs): + """Use as object_pairs_hook when loading JSON to detect duplicate keys.""" + result = {} + for key, value in ordered_pairs: + if key in result: + raise ValueError('Duplicate key: ' + key) + result[key] = value + return result + + +def load_template(base_path, filename): + """Load a template file and print error output. + + Returns empty object on failure. + """ + result = {} + try: + jsonfile = codecs.open(os.path.join(base_path, os.path.join( + '.ci', filename)), encoding='utf-8', mode='r') + result = json.load(jsonfile, object_pairs_hook=detect_duplicate_keys) + except json.decoder.JSONDecodeError as error: + print('Invalid JSON in template .ci/%s:' % filename) + print('\t', error) + except ValueError as error: + print('Error in template .ci/%s:' % filename) + print('\t', error) + jsonfile.close() + return result + + +def full_check(mandatory_template, complete_template, base_path, file_path): + # Part of simplicity refactor + errors = 0 + try: + json_object = {} + with codecs.open( + os.path.join(base_path, file_path), + encoding='utf-8', mode='r') as jsonfile: + json_object = json.load(jsonfile, object_pairs_hook=detect_duplicate_keys) + # only langs needs this type of exception + if file_path == 'langs.json': + for key in json_object: + errors = errors + \ + check_items(mandatory_template, + complete_template, + json_object[key], + file_path + ' for locale ' + key, + list(Checks)) + else: + errors = errors + \ + check_items(mandatory_template, + complete_template, + json_object, + file_path, + list(Checks)) + except json.decoder.JSONDecodeError as error: + print('Invalid JSON in file %s:' % + file_path) + print('\t', error) + errors = errors + 1 + except ValueError as error: + print('Error in file %s:' % file_path) + print('\t', error) + errors = errors + 1 + except FileNotFoundError as error: + print('Error in file %s:' % file_path) + print('\t', error) + errors = errors + 1 + return errors + +def main(): + """Checks whether all JSON files will parse. + + For files where we have templates defined, also performs checks on + keys and values. + """ + + # Get base path + base_path = os.path.abspath(os.path.join( + os.path.dirname(__file__), os.path.pardir)) + + # For pretty-printing path in error messages + path_prefix_length = len(base_path) + 1 + + print('##############################################################') + print('Validating JSON in: %s ' % base_path) + + errors = 0 + + # Fetch template for mandatory global credits keys + mandatory_credits_template = load_template( + base_path, 'template_website_translation_credits_mandatory.json') + if not mandatory_credits_template: + errors = errors + 1 + + # Fetch template for all global credits keys + complete_credits_template = load_template( + base_path, 'template_website_translation_credits.json') + if not complete_credits_template: + errors = errors + 1 + + # Check that templates match + errors = errors + check_items(mandatory_credits_template, + complete_credits_template, + complete_credits_template, + 'template_website_translation_credits.json compared to ' + 'template_website_translation_credits_mandatory.json', + [Checks.KEYS_COMPLETE, Checks.VALUE_TYPES]) + + if errors > 0: + print('Found %d error(s) in templates.' % errors) + print('##############################################################') + return 1 + + for po in os.listdir(os.path.join(base_path,"po")): + if po.endswith('.json'): + if not re.fullmatch(r'[a-z]{2,3}-credits\.json', po): + print ("Wrong filename: "+str(os.path.join(base_path, "po", po))) + errors = errors + 1 + errors = errors + full_check( + mandatory_credits_template, + complete_credits_template, + base_path, + os.path.join("po", po) ) + + + if errors > 0: + print('Found %d error(s).' % errors) + print('##############################################################') + return 1 + + print('Done.') + print('##############################################################') + return 0 + + +# Call main function when this script is being run +if __name__ == '__main__': + sys.exit(main()) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..5ed0f842bc963fa9256fbdc2351941a856e72572 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,14 @@ +variables: + LC_ALL: C.UTF-8 + LANG: C.UTF-8 + GIT_DEPTH: "1" + +image: python:3 + +stages: + - test1 + +validate: + stage: test1 + script: + - "python .ci/validate_json.py" diff --git a/po/at-credits.json b/po/at-credits.json new file mode 100644 index 0000000000000000000000000000000000000000..73b941fc153cb797edac7134c17db86d889aad04 --- /dev/null +++ b/po/at-credits.json @@ -0,0 +1,7 @@ +{ + "website-translation-credits": { + "translation": [ + "Mariu Fueyes <https://framagit.org/Fueyes>" + ] + } +} diff --git a/po/cs-credits.json b/po/cs-credits.json index ac23ffa611c7fa78c9eca0670c7bd59c84c738b8..2a30f8ccf42c3d000da3c2a4508fda82c2a04fbe 100644 --- a/po/cs-credits.json +++ b/po/cs-credits.json @@ -1,7 +1,8 @@ { "website-translation-credits": { "translation": [ - "Kateřina Fleknová" + "Kateřina Fleknová", + "Jiřà Podhorecký" ] } } diff --git a/po/de-credits.json b/po/de-credits.json index 06d8ddc9e90fe4920d79bd6a4fef53d75205bb07..0a9fefc6f66f1c4d1641508d01917ab8994f6d7a 100644 --- a/po/de-credits.json +++ b/po/de-credits.json @@ -4,7 +4,8 @@ "Torpak <https://github.com/torpak>", "Ret Samys <https://framagit.org/RetSamys>", "Dirk <mailto:just.helping@terraformer.de>", - "Andrej Ficko <https://framagit.org/fici>" + "Andrej Ficko <https://framagit.org/fici>", + "Matthias Kaak" ] } } diff --git a/po/fr-credits.json b/po/fr-credits.json index f78dd7c56bbe7d6831fd66bc4ff6b0d0c3eebe2c..7eb99a21571e4faa21f8349b7980b0bb6fa315b3 100644 --- a/po/fr-credits.json +++ b/po/fr-credits.json @@ -1,7 +1,8 @@ { "website-translation-credits": { "translation": [ - "David Revoy" + "David Revoy", + "Nicolas Artance" ], "proofreading": [ "Aure Séguier" diff --git a/po/hu-credits.json b/po/hu-credits.json index bc72b81783bd75ed0debdefca4e5d46ab4da8dc7..ae00c36efe0caa2097c4aea63796dc81c991a83b 100644 --- a/po/hu-credits.json +++ b/po/hu-credits.json @@ -2,7 +2,8 @@ "website-translation-credits": { "translation": [ "Halász Gábor \"Hali\" <https://level14.hu>", - "whitecold <https://whitecold.neocities.org/>" + "whitecold <https://whitecold.neocities.org/>", + "Benedek Vigh <https://framagit.org/whitecold>" ] } } diff --git a/po/it-credits.json b/po/it-credits.json index 8c69ce2ab1adf78bcc9369e3fa48399ff712f6b8..4cffc2a35df3fe4d66c2eefcc38ee0dd5ba39881 100644 --- a/po/it-credits.json +++ b/po/it-credits.json @@ -1,7 +1,8 @@ { "website-translation-credits": { "translation": [ - "Melania Fois <mailto:melania.fois@gmail.com>" + "Melania Fois <mailto:melania.fois@gmail.com>", + "Paolo Mauri <https://framagit.org/maupao>" ] } } diff --git a/po/oc-credits.json b/po/oc-credits.json index f57b2d34df1e7d1a67d56a70ffa51ad323eacb9e..6c247233855cb33601226e104448efe63744dc9d 100644 --- a/po/oc-credits.json +++ b/po/oc-credits.json @@ -1,7 +1,8 @@ { "website-translation-credits": { "translation": [ - "Aure Séguier" + "Aure Séguier", + "Quentin" ] } } diff --git a/po/pt-credits.json b/po/pt-credits.json index 0d88a2cf8964bef090e5ec5ad20c6530585d6f65..e612c7f62e82b0e92d64a0f6635e41b07e5d325f 100644 --- a/po/pt-credits.json +++ b/po/pt-credits.json @@ -1,7 +1,8 @@ { "website-translation-credits": { "translation": [ - "Frederico Batista <https://github.com/fredfb>" + "Frederico Batista <https://github.com/fredfb>", + "Antoine Aubry" ] } } diff --git a/po/sp-credits.json b/po/sp-credits.json index dcd2b11a48021b5f9466f3339fdf66b3c88fad4c..db02ffacb15a358a74798bd5386e773deaa11ed7 100644 --- a/po/sp-credits.json +++ b/po/sp-credits.json @@ -3,4 +3,5 @@ "translation": [ "Ret \"jan Ke Tami\" Samys" ] -} \ No newline at end of file + } +} diff --git a/po/tp-credits.json b/po/tp-credits.json index dcd2b11a48021b5f9466f3339fdf66b3c88fad4c..db02ffacb15a358a74798bd5386e773deaa11ed7 100644 --- a/po/tp-credits.json +++ b/po/tp-credits.json @@ -3,4 +3,5 @@ "translation": [ "Ret \"jan Ke Tami\" Samys" ] -} \ No newline at end of file + } +}