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
+   }
+}