summaryrefslogtreecommitdiffstats
path: root/third_party/libwebrtc/build/locale_tool.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /third_party/libwebrtc/build/locale_tool.py
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/libwebrtc/build/locale_tool.py')
-rwxr-xr-xthird_party/libwebrtc/build/locale_tool.py1512
1 files changed, 1512 insertions, 0 deletions
diff --git a/third_party/libwebrtc/build/locale_tool.py b/third_party/libwebrtc/build/locale_tool.py
new file mode 100755
index 0000000000..b5729d8af0
--- /dev/null
+++ b/third_party/libwebrtc/build/locale_tool.py
@@ -0,0 +1,1512 @@
+#!/usr/bin/env vpython
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Helper script used to manage locale-related files in Chromium.
+
+This script is used to check, and potentially fix, many locale-related files
+in your Chromium workspace, such as:
+
+ - GRIT input files (.grd) and the corresponding translations (.xtb).
+
+ - BUILD.gn files listing Android localized resource string resource .xml
+ generated by GRIT for all supported Chrome locales. These correspond to
+ <output> elements that use the type="android" attribute.
+
+The --scan-dir <dir> option can be used to check for all files under a specific
+directory, and the --fix-inplace option can be used to try fixing any file
+that doesn't pass the check.
+
+This can be very handy to avoid tedious and repetitive work when adding new
+translations / locales to the Chrome code base, since this script can update
+said input files for you.
+
+Important note: checks and fix may fail on some input files. For example
+remoting/resources/remoting_strings.grd contains an in-line comment element
+inside its <outputs> section that breaks the script. The check will fail, and
+trying to fix it too, but at least the file will not be modified.
+"""
+
+from __future__ import print_function
+
+import argparse
+import json
+import os
+import re
+import shutil
+import subprocess
+import sys
+import unittest
+
+# Assume this script is under build/
+_SCRIPT_DIR = os.path.dirname(__file__)
+_SCRIPT_NAME = os.path.join(_SCRIPT_DIR, os.path.basename(__file__))
+_TOP_SRC_DIR = os.path.join(_SCRIPT_DIR, '..')
+
+# Need to import android/gyp/util/resource_utils.py here.
+sys.path.insert(0, os.path.join(_SCRIPT_DIR, 'android/gyp'))
+
+from util import build_utils
+from util import resource_utils
+
+
+# This locale is the default and doesn't have translations.
+_DEFAULT_LOCALE = 'en-US'
+
+# Misc terminal codes to provide human friendly progress output.
+_CONSOLE_CODE_MOVE_CURSOR_TO_COLUMN_0 = '\x1b[0G'
+_CONSOLE_CODE_ERASE_LINE = '\x1b[K'
+_CONSOLE_START_LINE = (
+ _CONSOLE_CODE_MOVE_CURSOR_TO_COLUMN_0 + _CONSOLE_CODE_ERASE_LINE)
+
+##########################################################################
+##########################################################################
+#####
+##### G E N E R I C H E L P E R F U N C T I O N S
+#####
+##########################################################################
+##########################################################################
+
+def _FixChromiumLangAttribute(lang):
+ """Map XML "lang" attribute values to Chromium locale names."""
+ _CHROMIUM_LANG_FIXES = {
+ 'en': 'en-US', # For now, Chromium doesn't have an 'en' locale.
+ 'iw': 'he', # 'iw' is the obsolete form of ISO 639-1 for Hebrew
+ 'no': 'nb', # 'no' is used by the Translation Console for Norwegian (nb).
+ }
+ return _CHROMIUM_LANG_FIXES.get(lang, lang)
+
+
+def _FixTranslationConsoleLocaleName(locale):
+ _FIXES = {
+ 'nb': 'no', # Norwegian.
+ 'he': 'iw', # Hebrew
+ }
+ return _FIXES.get(locale, locale)
+
+
+def _CompareLocaleLists(list_a, list_expected, list_name):
+ """Compare two lists of locale names. Print errors if they differ.
+
+ Args:
+ list_a: First list of locales.
+ list_expected: Second list of locales, as expected.
+ list_name: Name of list printed in error messages.
+ Returns:
+ On success, return False. On error, print error messages and return True.
+ """
+ errors = []
+ missing_locales = sorted(set(list_a) - set(list_expected))
+ if missing_locales:
+ errors.append('Missing locales: %s' % missing_locales)
+
+ extra_locales = sorted(set(list_expected) - set(list_a))
+ if extra_locales:
+ errors.append('Unexpected locales: %s' % extra_locales)
+
+ if errors:
+ print('Errors in %s definition:' % list_name)
+ for error in errors:
+ print(' %s\n' % error)
+ return True
+
+ return False
+
+
+def _BuildIntervalList(input_list, predicate):
+ """Find ranges of contiguous list items that pass a given predicate.
+
+ Args:
+ input_list: An input list of items of any type.
+ predicate: A function that takes a list item and return True if it
+ passes a given test.
+ Returns:
+ A list of (start_pos, end_pos) tuples, where all items in
+ [start_pos, end_pos) pass the predicate.
+ """
+ result = []
+ size = len(input_list)
+ start = 0
+ while True:
+ # Find first item in list that passes the predicate.
+ while start < size and not predicate(input_list[start]):
+ start += 1
+
+ if start >= size:
+ return result
+
+ # Find first item in the rest of the list that does not pass the
+ # predicate.
+ end = start + 1
+ while end < size and predicate(input_list[end]):
+ end += 1
+
+ result.append((start, end))
+ start = end + 1
+
+
+def _SortListSubRange(input_list, start, end, key_func):
+ """Sort an input list's sub-range according to a specific key function.
+
+ Args:
+ input_list: An input list.
+ start: Sub-range starting position in list.
+ end: Sub-range limit position in list.
+ key_func: A function that extracts a sort key from a line.
+ Returns:
+ A copy of |input_list|, with all items in [|start|, |end|) sorted
+ according to |key_func|.
+ """
+ result = input_list[:start]
+ inputs = []
+ for pos in xrange(start, end):
+ line = input_list[pos]
+ key = key_func(line)
+ inputs.append((key, line))
+
+ for _, line in sorted(inputs):
+ result.append(line)
+
+ result += input_list[end:]
+ return result
+
+
+def _SortElementsRanges(lines, element_predicate, element_key):
+ """Sort all elements of a given type in a list of lines by a given key.
+
+ Args:
+ lines: input lines.
+ element_predicate: predicate function to select elements to sort.
+ element_key: lambda returning a comparison key for each element that
+ passes the predicate.
+ Returns:
+ A new list of input lines, with lines [start..end) sorted.
+ """
+ intervals = _BuildIntervalList(lines, element_predicate)
+ for start, end in intervals:
+ lines = _SortListSubRange(lines, start, end, element_key)
+
+ return lines
+
+
+def _ProcessFile(input_file, locales, check_func, fix_func):
+ """Process a given input file, potentially fixing it.
+
+ Args:
+ input_file: Input file path.
+ locales: List of Chrome locales to consider / expect.
+ check_func: A lambda called to check the input file lines with
+ (input_lines, locales) argument. It must return an list of error
+ messages, or None on success.
+ fix_func: None, or a lambda called to fix the input file lines with
+ (input_lines, locales). It must return the new list of lines for
+ the input file, and may raise an Exception in case of error.
+ Returns:
+ True at the moment.
+ """
+ print('%sProcessing %s...' % (_CONSOLE_START_LINE, input_file), end=' ')
+ sys.stdout.flush()
+ with open(input_file) as f:
+ input_lines = f.readlines()
+ errors = check_func(input_file, input_lines, locales)
+ if errors:
+ print('\n%s%s' % (_CONSOLE_START_LINE, '\n'.join(errors)))
+ if fix_func:
+ try:
+ input_lines = fix_func(input_file, input_lines, locales)
+ output = ''.join(input_lines)
+ with open(input_file, 'wt') as f:
+ f.write(output)
+ print('Fixed %s.' % input_file)
+ except Exception as e: # pylint: disable=broad-except
+ print('Skipped %s: %s' % (input_file, e))
+
+ return True
+
+
+def _ScanDirectoriesForFiles(scan_dirs, file_predicate):
+ """Scan a directory for files that match a given predicate.
+
+ Args:
+ scan_dir: A list of top-level directories to start scan in.
+ file_predicate: lambda function which is passed the file's base name
+ and returns True if its full path, relative to |scan_dir|, should be
+ passed in the result.
+ Returns:
+ A list of file full paths.
+ """
+ result = []
+ for src_dir in scan_dirs:
+ for root, _, files in os.walk(src_dir):
+ result.extend(os.path.join(root, f) for f in files if file_predicate(f))
+ return result
+
+
+def _WriteFile(file_path, file_data):
+ """Write |file_data| to |file_path|."""
+ with open(file_path, 'w') as f:
+ f.write(file_data)
+
+
+def _FindGnExecutable():
+ """Locate the real GN executable used by this Chromium checkout.
+
+ This is needed because the depot_tools 'gn' wrapper script will look
+ for .gclient and other things we really don't need here.
+
+ Returns:
+ Path of real host GN executable from current Chromium src/ checkout.
+ """
+ # Simply scan buildtools/*/gn and return the first one found so we don't
+ # have to guess the platform-specific sub-directory name (e.g. 'linux64'
+ # for 64-bit Linux machines).
+ buildtools_dir = os.path.join(_TOP_SRC_DIR, 'buildtools')
+ for subdir in os.listdir(buildtools_dir):
+ subdir_path = os.path.join(buildtools_dir, subdir)
+ if not os.path.isdir(subdir_path):
+ continue
+ gn_path = os.path.join(subdir_path, 'gn')
+ if os.path.exists(gn_path):
+ return gn_path
+ return None
+
+
+def _PrettyPrintListAsLines(input_list, available_width, trailing_comma=False):
+ result = []
+ input_str = ', '.join(input_list)
+ while len(input_str) > available_width:
+ pos = input_str.rfind(',', 0, available_width)
+ result.append(input_str[:pos + 1])
+ input_str = input_str[pos + 1:].lstrip()
+ if trailing_comma and input_str:
+ input_str += ','
+ result.append(input_str)
+ return result
+
+
+class _PrettyPrintListAsLinesTest(unittest.TestCase):
+
+ def test_empty_list(self):
+ self.assertListEqual([''], _PrettyPrintListAsLines([], 10))
+
+ def test_wrapping(self):
+ input_list = ['foo', 'bar', 'zoo', 'tool']
+ self.assertListEqual(
+ _PrettyPrintListAsLines(input_list, 8),
+ ['foo,', 'bar,', 'zoo,', 'tool'])
+ self.assertListEqual(
+ _PrettyPrintListAsLines(input_list, 12), ['foo, bar,', 'zoo, tool'])
+ self.assertListEqual(
+ _PrettyPrintListAsLines(input_list, 79), ['foo, bar, zoo, tool'])
+
+ def test_trailing_comma(self):
+ input_list = ['foo', 'bar', 'zoo', 'tool']
+ self.assertListEqual(
+ _PrettyPrintListAsLines(input_list, 8, trailing_comma=True),
+ ['foo,', 'bar,', 'zoo,', 'tool,'])
+ self.assertListEqual(
+ _PrettyPrintListAsLines(input_list, 12, trailing_comma=True),
+ ['foo, bar,', 'zoo, tool,'])
+ self.assertListEqual(
+ _PrettyPrintListAsLines(input_list, 79, trailing_comma=True),
+ ['foo, bar, zoo, tool,'])
+
+
+##########################################################################
+##########################################################################
+#####
+##### L O C A L E S L I S T S
+#####
+##########################################################################
+##########################################################################
+
+# Various list of locales that will be extracted from build/config/locales.gni
+# Do not use these directly, use ChromeLocales(), and IosUnsupportedLocales()
+# instead to access these lists.
+_INTERNAL_CHROME_LOCALES = []
+_INTERNAL_IOS_UNSUPPORTED_LOCALES = []
+
+
+def ChromeLocales():
+ """Return the list of all locales supported by Chrome."""
+ if not _INTERNAL_CHROME_LOCALES:
+ _ExtractAllChromeLocalesLists()
+ return _INTERNAL_CHROME_LOCALES
+
+
+def IosUnsupportedLocales():
+ """Return the list of locales that are unsupported on iOS."""
+ if not _INTERNAL_IOS_UNSUPPORTED_LOCALES:
+ _ExtractAllChromeLocalesLists()
+ return _INTERNAL_IOS_UNSUPPORTED_LOCALES
+
+
+def _PrepareTinyGnWorkspace(work_dir, out_subdir_name='out'):
+ """Populate an empty directory with a tiny set of working GN config files.
+
+ This allows us to run 'gn gen <out> --root <work_dir>' as fast as possible
+ to generate files containing the locales list. This takes about 300ms on
+ a decent machine, instead of more than 5 seconds when running the equivalent
+ commands from a real Chromium workspace, which requires regenerating more
+ than 23k targets.
+
+ Args:
+ work_dir: target working directory.
+ out_subdir_name: Name of output sub-directory.
+ Returns:
+ Full path of output directory created inside |work_dir|.
+ """
+ # Create top-level .gn file that must point to the BUILDCONFIG.gn.
+ _WriteFile(os.path.join(work_dir, '.gn'),
+ 'buildconfig = "//BUILDCONFIG.gn"\n')
+ # Create BUILDCONFIG.gn which must set a default toolchain. Also add
+ # all variables that may be used in locales.gni in a declare_args() block.
+ _WriteFile(
+ os.path.join(work_dir, 'BUILDCONFIG.gn'),
+ r'''set_default_toolchain("toolchain")
+declare_args () {
+ is_ios = false
+ is_android = true
+}
+''')
+
+ # Create fake toolchain required by BUILDCONFIG.gn.
+ os.mkdir(os.path.join(work_dir, 'toolchain'))
+ _WriteFile(os.path.join(work_dir, 'toolchain', 'BUILD.gn'),
+ r'''toolchain("toolchain") {
+ tool("stamp") {
+ command = "touch {{output}}" # Required by action()
+ }
+}
+''')
+
+ # Create top-level BUILD.gn, GN requires at least one target to build so do
+ # that with a fake action which will never be invoked. Also write the locales
+ # to misc files in the output directory.
+ _WriteFile(
+ os.path.join(work_dir, 'BUILD.gn'), r'''import("//locales.gni")
+
+action("create_foo") { # fake action to avoid GN complaints.
+ script = "//build/create_foo.py"
+ inputs = []
+ outputs = [ "$target_out_dir/$target_name" ]
+}
+
+# Write the locales lists to files in the output directory.
+_filename = root_build_dir + "/foo"
+write_file(_filename + ".locales", locales, "json")
+write_file(_filename + ".ios_unsupported_locales",
+ ios_unsupported_locales,
+ "json")
+''')
+
+ # Copy build/config/locales.gni to the workspace, as required by BUILD.gn.
+ shutil.copyfile(os.path.join(_TOP_SRC_DIR, 'build', 'config', 'locales.gni'),
+ os.path.join(work_dir, 'locales.gni'))
+
+ # Create output directory.
+ out_path = os.path.join(work_dir, out_subdir_name)
+ os.mkdir(out_path)
+
+ # And ... we're good.
+ return out_path
+
+
+# Set this global variable to the path of a given temporary directory
+# before calling _ExtractAllChromeLocalesLists() if you want to debug
+# the locales list extraction process.
+_DEBUG_LOCALES_WORK_DIR = None
+
+
+def _ReadJsonList(file_path):
+ """Read a JSON file that must contain a list, and return it."""
+ with open(file_path) as f:
+ data = json.load(f)
+ assert isinstance(data, list), "JSON file %s is not a list!" % file_path
+ return [item.encode('utf8') for item in data]
+
+
+def _ExtractAllChromeLocalesLists():
+ with build_utils.TempDir() as tmp_path:
+ if _DEBUG_LOCALES_WORK_DIR:
+ tmp_path = _DEBUG_LOCALES_WORK_DIR
+ build_utils.DeleteDirectory(tmp_path)
+ build_utils.MakeDirectory(tmp_path)
+
+ out_path = _PrepareTinyGnWorkspace(tmp_path, 'out')
+
+ # NOTE: The file suffixes used here should be kept in sync with
+ # build/config/locales.gni
+ gn_executable = _FindGnExecutable()
+ try:
+ subprocess.check_output(
+ [gn_executable, 'gen', out_path, '--root=' + tmp_path])
+ except subprocess.CalledProcessError as e:
+ print(e.output)
+ raise e
+
+ global _INTERNAL_CHROME_LOCALES
+ _INTERNAL_CHROME_LOCALES = _ReadJsonList(
+ os.path.join(out_path, 'foo.locales'))
+
+ global _INTERNAL_IOS_UNSUPPORTED_LOCALES
+ _INTERNAL_IOS_UNSUPPORTED_LOCALES = _ReadJsonList(
+ os.path.join(out_path, 'foo.ios_unsupported_locales'))
+
+
+##########################################################################
+##########################################################################
+#####
+##### G R D H E L P E R F U N C T I O N S
+#####
+##########################################################################
+##########################################################################
+
+# Technical note:
+#
+# Even though .grd files are XML, an xml parser library is not used in order
+# to preserve the original file's structure after modification. ElementTree
+# tends to re-order attributes in each element when re-writing an XML
+# document tree, which is undesirable here.
+#
+# Thus simple line-based regular expression matching is used instead.
+#
+
+# Misc regular expressions used to match elements and their attributes.
+_RE_OUTPUT_ELEMENT = re.compile(r'<output (.*)\s*/>')
+_RE_TRANSLATION_ELEMENT = re.compile(r'<file( | .* )path="(.*\.xtb)".*/>')
+_RE_FILENAME_ATTRIBUTE = re.compile(r'filename="([^"]*)"')
+_RE_LANG_ATTRIBUTE = re.compile(r'lang="([^"]*)"')
+_RE_PATH_ATTRIBUTE = re.compile(r'path="([^"]*)"')
+_RE_TYPE_ANDROID_ATTRIBUTE = re.compile(r'type="android"')
+
+
+
+def _IsGritInputFile(input_file):
+ """Returns True iff this is a GRIT input file."""
+ return input_file.endswith('.grd')
+
+
+def _GetXmlLangAttribute(xml_line):
+ """Extract the lang attribute value from an XML input line."""
+ m = _RE_LANG_ATTRIBUTE.search(xml_line)
+ if not m:
+ return None
+ return m.group(1)
+
+
+class _GetXmlLangAttributeTest(unittest.TestCase):
+ TEST_DATA = {
+ '': None,
+ 'foo': None,
+ 'lang=foo': None,
+ 'lang="foo"': 'foo',
+ '<something lang="foo bar" />': 'foo bar',
+ '<file lang="fr-CA" path="path/to/strings_fr-CA.xtb" />': 'fr-CA',
+ }
+
+ def test_GetXmlLangAttribute(self):
+ for test_line, expected in self.TEST_DATA.iteritems():
+ self.assertEquals(_GetXmlLangAttribute(test_line), expected)
+
+
+def _SortGrdElementsRanges(grd_lines, element_predicate):
+ """Sort all .grd elements of a given type by their lang attribute."""
+ return _SortElementsRanges(grd_lines, element_predicate, _GetXmlLangAttribute)
+
+
+def _CheckGrdElementRangeLang(grd_lines, start, end, wanted_locales):
+ """Check the element 'lang' attributes in specific .grd lines range.
+
+ This really checks the following:
+ - Each item has a correct 'lang' attribute.
+ - There are no duplicated lines for the same 'lang' attribute.
+ - That there are no extra locales that Chromium doesn't want.
+ - That no wanted locale is missing.
+
+ Args:
+ grd_lines: Input .grd lines.
+ start: Sub-range start position in input line list.
+ end: Sub-range limit position in input line list.
+ wanted_locales: Set of wanted Chromium locale names.
+ Returns:
+ List of error message strings for this input. Empty on success.
+ """
+ errors = []
+ locales = set()
+ for pos in xrange(start, end):
+ line = grd_lines[pos]
+ lang = _GetXmlLangAttribute(line)
+ if not lang:
+ errors.append('%d: Missing "lang" attribute in <output> element' % pos +
+ 1)
+ continue
+ cr_locale = _FixChromiumLangAttribute(lang)
+ if cr_locale in locales:
+ errors.append(
+ '%d: Redefinition of <output> for "%s" locale' % (pos + 1, lang))
+ locales.add(cr_locale)
+
+ extra_locales = locales.difference(wanted_locales)
+ if extra_locales:
+ errors.append('%d-%d: Extra locales found: %s' % (start + 1, end + 1,
+ sorted(extra_locales)))
+
+ missing_locales = wanted_locales.difference(locales)
+ if missing_locales:
+ errors.append('%d-%d: Missing locales: %s' % (start + 1, end + 1,
+ sorted(missing_locales)))
+
+ return errors
+
+
+##########################################################################
+##########################################################################
+#####
+##### G R D A N D R O I D O U T P U T S
+#####
+##########################################################################
+##########################################################################
+
+def _IsGrdAndroidOutputLine(line):
+ """Returns True iff this is an Android-specific <output> line."""
+ m = _RE_OUTPUT_ELEMENT.search(line)
+ if m:
+ return 'type="android"' in m.group(1)
+ return False
+
+assert _IsGrdAndroidOutputLine(' <output type="android"/>')
+
+# Many of the functions below have unused arguments due to genericity.
+# pylint: disable=unused-argument
+
+def _CheckGrdElementRangeAndroidOutputFilename(grd_lines, start, end,
+ wanted_locales):
+ """Check all <output> elements in specific input .grd lines range.
+
+ This really checks the following:
+ - Filenames exist for each listed locale.
+ - Filenames are well-formed.
+
+ Args:
+ grd_lines: Input .grd lines.
+ start: Sub-range start position in input line list.
+ end: Sub-range limit position in input line list.
+ wanted_locales: Set of wanted Chromium locale names.
+ Returns:
+ List of error message strings for this input. Empty on success.
+ """
+ errors = []
+ for pos in xrange(start, end):
+ line = grd_lines[pos]
+ lang = _GetXmlLangAttribute(line)
+ if not lang:
+ continue
+ cr_locale = _FixChromiumLangAttribute(lang)
+
+ m = _RE_FILENAME_ATTRIBUTE.search(line)
+ if not m:
+ errors.append('%d: Missing filename attribute in <output> element' % pos +
+ 1)
+ else:
+ filename = m.group(1)
+ if not filename.endswith('.xml'):
+ errors.append(
+ '%d: Filename should end with ".xml": %s' % (pos + 1, filename))
+
+ dirname = os.path.basename(os.path.dirname(filename))
+ prefix = ('values-%s' % resource_utils.ToAndroidLocaleName(cr_locale)
+ if cr_locale != _DEFAULT_LOCALE else 'values')
+ if dirname != prefix:
+ errors.append(
+ '%s: Directory name should be %s: %s' % (pos + 1, prefix, filename))
+
+ return errors
+
+
+def _CheckGrdAndroidOutputElements(grd_file, grd_lines, wanted_locales):
+ """Check all <output> elements related to Android.
+
+ Args:
+ grd_file: Input .grd file path.
+ grd_lines: List of input .grd lines.
+ wanted_locales: set of wanted Chromium locale names.
+ Returns:
+ List of error message strings. Empty on success.
+ """
+ intervals = _BuildIntervalList(grd_lines, _IsGrdAndroidOutputLine)
+ errors = []
+ for start, end in intervals:
+ errors += _CheckGrdElementRangeLang(grd_lines, start, end, wanted_locales)
+ errors += _CheckGrdElementRangeAndroidOutputFilename(grd_lines, start, end,
+ wanted_locales)
+ return errors
+
+
+def _AddMissingLocalesInGrdAndroidOutputs(grd_file, grd_lines, wanted_locales):
+ """Fix an input .grd line by adding missing Android outputs.
+
+ Args:
+ grd_file: Input .grd file path.
+ grd_lines: Input .grd line list.
+ wanted_locales: set of Chromium locale names.
+ Returns:
+ A new list of .grd lines, containing new <output> elements when needed
+ for locales from |wanted_locales| that were not part of the input.
+ """
+ intervals = _BuildIntervalList(grd_lines, _IsGrdAndroidOutputLine)
+ for start, end in reversed(intervals):
+ locales = set()
+ for pos in xrange(start, end):
+ lang = _GetXmlLangAttribute(grd_lines[pos])
+ locale = _FixChromiumLangAttribute(lang)
+ locales.add(locale)
+
+ missing_locales = wanted_locales.difference(locales)
+ if not missing_locales:
+ continue
+
+ src_locale = 'bg'
+ src_lang_attribute = 'lang="%s"' % src_locale
+ src_line = None
+ for pos in xrange(start, end):
+ if src_lang_attribute in grd_lines[pos]:
+ src_line = grd_lines[pos]
+ break
+
+ if not src_line:
+ raise Exception(
+ 'Cannot find <output> element with "%s" lang attribute' % src_locale)
+
+ line_count = end - 1
+ for locale in missing_locales:
+ android_locale = resource_utils.ToAndroidLocaleName(locale)
+ dst_line = src_line.replace(
+ 'lang="%s"' % src_locale, 'lang="%s"' % locale).replace(
+ 'values-%s/' % src_locale, 'values-%s/' % android_locale)
+ grd_lines.insert(line_count, dst_line)
+ line_count += 1
+
+ # Sort the new <output> elements.
+ return _SortGrdElementsRanges(grd_lines, _IsGrdAndroidOutputLine)
+
+
+##########################################################################
+##########################################################################
+#####
+##### G R D T R A N S L A T I O N S
+#####
+##########################################################################
+##########################################################################
+
+
+def _IsTranslationGrdOutputLine(line):
+ """Returns True iff this is an output .xtb <file> element."""
+ m = _RE_TRANSLATION_ELEMENT.search(line)
+ return m is not None
+
+
+class _IsTranslationGrdOutputLineTest(unittest.TestCase):
+
+ def test_GrdTranslationOutputLines(self):
+ _VALID_INPUT_LINES = [
+ '<file path="foo/bar.xtb" />',
+ '<file path="foo/bar.xtb"/>',
+ '<file lang="fr-CA" path="translations/aw_strings_fr-CA.xtb"/>',
+ '<file lang="fr-CA" path="translations/aw_strings_fr-CA.xtb" />',
+ ' <file path="translations/aw_strings_ar.xtb" lang="ar" />',
+ ]
+ _INVALID_INPUT_LINES = ['<file path="foo/bar.xml" />']
+
+ for line in _VALID_INPUT_LINES:
+ self.assertTrue(
+ _IsTranslationGrdOutputLine(line),
+ '_IsTranslationGrdOutputLine() returned False for [%s]' % line)
+
+ for line in _INVALID_INPUT_LINES:
+ self.assertFalse(
+ _IsTranslationGrdOutputLine(line),
+ '_IsTranslationGrdOutputLine() returned True for [%s]' % line)
+
+
+def _CheckGrdTranslationElementRange(grd_lines, start, end,
+ wanted_locales):
+ """Check all <translations> sub-elements in specific input .grd lines range.
+
+ This really checks the following:
+ - Each item has a 'path' attribute.
+ - Each such path value ends up with '.xtb'.
+
+ Args:
+ grd_lines: Input .grd lines.
+ start: Sub-range start position in input line list.
+ end: Sub-range limit position in input line list.
+ wanted_locales: Set of wanted Chromium locale names.
+ Returns:
+ List of error message strings for this input. Empty on success.
+ """
+ errors = []
+ for pos in xrange(start, end):
+ line = grd_lines[pos]
+ lang = _GetXmlLangAttribute(line)
+ if not lang:
+ continue
+ m = _RE_PATH_ATTRIBUTE.search(line)
+ if not m:
+ errors.append('%d: Missing path attribute in <file> element' % pos +
+ 1)
+ else:
+ filename = m.group(1)
+ if not filename.endswith('.xtb'):
+ errors.append(
+ '%d: Path should end with ".xtb": %s' % (pos + 1, filename))
+
+ return errors
+
+
+def _CheckGrdTranslations(grd_file, grd_lines, wanted_locales):
+ """Check all <file> elements that correspond to an .xtb output file.
+
+ Args:
+ grd_file: Input .grd file path.
+ grd_lines: List of input .grd lines.
+ wanted_locales: set of wanted Chromium locale names.
+ Returns:
+ List of error message strings. Empty on success.
+ """
+ wanted_locales = wanted_locales - set([_DEFAULT_LOCALE])
+ intervals = _BuildIntervalList(grd_lines, _IsTranslationGrdOutputLine)
+ errors = []
+ for start, end in intervals:
+ errors += _CheckGrdElementRangeLang(grd_lines, start, end, wanted_locales)
+ errors += _CheckGrdTranslationElementRange(grd_lines, start, end,
+ wanted_locales)
+ return errors
+
+
+# Regular expression used to replace the lang attribute inside .xtb files.
+_RE_TRANSLATIONBUNDLE = re.compile('<translationbundle lang="(.*)">')
+
+
+def _CreateFakeXtbFileFrom(src_xtb_path, dst_xtb_path, dst_locale):
+ """Create a fake .xtb file.
+
+ Args:
+ src_xtb_path: Path to source .xtb file to copy from.
+ dst_xtb_path: Path to destination .xtb file to write to.
+ dst_locale: Destination locale, the lang attribute in the source file
+ will be substituted with this value before its lines are written
+ to the destination file.
+ """
+ with open(src_xtb_path) as f:
+ src_xtb_lines = f.readlines()
+
+ def replace_xtb_lang_attribute(line):
+ m = _RE_TRANSLATIONBUNDLE.search(line)
+ if not m:
+ return line
+ return line[:m.start(1)] + dst_locale + line[m.end(1):]
+
+ dst_xtb_lines = [replace_xtb_lang_attribute(line) for line in src_xtb_lines]
+ with build_utils.AtomicOutput(dst_xtb_path) as tmp:
+ tmp.writelines(dst_xtb_lines)
+
+
+def _AddMissingLocalesInGrdTranslations(grd_file, grd_lines, wanted_locales):
+ """Fix an input .grd line by adding missing Android outputs.
+
+ This also creates fake .xtb files from the one provided for 'en-GB'.
+
+ Args:
+ grd_file: Input .grd file path.
+ grd_lines: Input .grd line list.
+ wanted_locales: set of Chromium locale names.
+ Returns:
+ A new list of .grd lines, containing new <output> elements when needed
+ for locales from |wanted_locales| that were not part of the input.
+ """
+ wanted_locales = wanted_locales - set([_DEFAULT_LOCALE])
+ intervals = _BuildIntervalList(grd_lines, _IsTranslationGrdOutputLine)
+ for start, end in reversed(intervals):
+ locales = set()
+ for pos in xrange(start, end):
+ lang = _GetXmlLangAttribute(grd_lines[pos])
+ locale = _FixChromiumLangAttribute(lang)
+ locales.add(locale)
+
+ missing_locales = wanted_locales.difference(locales)
+ if not missing_locales:
+ continue
+
+ src_locale = 'en-GB'
+ src_lang_attribute = 'lang="%s"' % src_locale
+ src_line = None
+ for pos in xrange(start, end):
+ if src_lang_attribute in grd_lines[pos]:
+ src_line = grd_lines[pos]
+ break
+
+ if not src_line:
+ raise Exception(
+ 'Cannot find <file> element with "%s" lang attribute' % src_locale)
+
+ src_path = os.path.join(
+ os.path.dirname(grd_file),
+ _RE_PATH_ATTRIBUTE.search(src_line).group(1))
+
+ line_count = end - 1
+ for locale in missing_locales:
+ dst_line = src_line.replace(
+ 'lang="%s"' % src_locale, 'lang="%s"' % locale).replace(
+ '_%s.xtb' % src_locale, '_%s.xtb' % locale)
+ grd_lines.insert(line_count, dst_line)
+ line_count += 1
+
+ dst_path = src_path.replace('_%s.xtb' % src_locale, '_%s.xtb' % locale)
+ _CreateFakeXtbFileFrom(src_path, dst_path, locale)
+
+
+ # Sort the new <output> elements.
+ return _SortGrdElementsRanges(grd_lines, _IsTranslationGrdOutputLine)
+
+
+##########################################################################
+##########################################################################
+#####
+##### G N A N D R O I D O U T P U T S
+#####
+##########################################################################
+##########################################################################
+
+_RE_GN_VALUES_LIST_LINE = re.compile(
+ r'^\s*".*values(\-([A-Za-z0-9-]+))?/.*\.xml",\s*$')
+
+def _IsBuildGnInputFile(input_file):
+ """Returns True iff this is a BUILD.gn file."""
+ return os.path.basename(input_file) == 'BUILD.gn'
+
+
+def _GetAndroidGnOutputLocale(line):
+ """Check a GN list, and return its Android locale if it is an output .xml"""
+ m = _RE_GN_VALUES_LIST_LINE.match(line)
+ if not m:
+ return None
+
+ if m.group(1): # First group is optional and contains group 2.
+ return m.group(2)
+
+ return resource_utils.ToAndroidLocaleName(_DEFAULT_LOCALE)
+
+
+def _IsAndroidGnOutputLine(line):
+ """Returns True iff this is an Android-specific localized .xml output."""
+ return _GetAndroidGnOutputLocale(line) != None
+
+
+def _CheckGnOutputsRangeForLocalizedStrings(gn_lines, start, end):
+ """Check that a range of GN lines corresponds to localized strings.
+
+ Special case: Some BUILD.gn files list several non-localized .xml files
+ that should be ignored by this function, e.g. in
+ components/cronet/android/BUILD.gn, the following appears:
+
+ inputs = [
+ ...
+ "sample/res/layout/activity_main.xml",
+ "sample/res/layout/dialog_url.xml",
+ "sample/res/values/dimens.xml",
+ "sample/res/values/strings.xml",
+ ...
+ ]
+
+ These are non-localized strings, and should be ignored. This function is
+ used to detect them quickly.
+ """
+ for pos in xrange(start, end):
+ if not 'values/' in gn_lines[pos]:
+ return True
+ return False
+
+
+def _CheckGnOutputsRange(gn_lines, start, end, wanted_locales):
+ if not _CheckGnOutputsRangeForLocalizedStrings(gn_lines, start, end):
+ return []
+
+ errors = []
+ locales = set()
+ for pos in xrange(start, end):
+ line = gn_lines[pos]
+ android_locale = _GetAndroidGnOutputLocale(line)
+ assert android_locale != None
+ cr_locale = resource_utils.ToChromiumLocaleName(android_locale)
+ if cr_locale in locales:
+ errors.append('%s: Redefinition of output for "%s" locale' %
+ (pos + 1, android_locale))
+ locales.add(cr_locale)
+
+ extra_locales = locales.difference(wanted_locales)
+ if extra_locales:
+ errors.append('%d-%d: Extra locales: %s' % (start + 1, end + 1,
+ sorted(extra_locales)))
+
+ missing_locales = wanted_locales.difference(locales)
+ if missing_locales:
+ errors.append('%d-%d: Missing locales: %s' % (start + 1, end + 1,
+ sorted(missing_locales)))
+
+ return errors
+
+
+def _CheckGnAndroidOutputs(gn_file, gn_lines, wanted_locales):
+ intervals = _BuildIntervalList(gn_lines, _IsAndroidGnOutputLine)
+ errors = []
+ for start, end in intervals:
+ errors += _CheckGnOutputsRange(gn_lines, start, end, wanted_locales)
+ return errors
+
+
+def _AddMissingLocalesInGnAndroidOutputs(gn_file, gn_lines, wanted_locales):
+ intervals = _BuildIntervalList(gn_lines, _IsAndroidGnOutputLine)
+ # NOTE: Since this may insert new lines to each interval, process the
+ # list in reverse order to maintain valid (start,end) positions during
+ # the iteration.
+ for start, end in reversed(intervals):
+ if not _CheckGnOutputsRangeForLocalizedStrings(gn_lines, start, end):
+ continue
+
+ locales = set()
+ for pos in xrange(start, end):
+ lang = _GetAndroidGnOutputLocale(gn_lines[pos])
+ locale = resource_utils.ToChromiumLocaleName(lang)
+ locales.add(locale)
+
+ missing_locales = wanted_locales.difference(locales)
+ if not missing_locales:
+ continue
+
+ src_locale = 'bg'
+ src_values = 'values-%s/' % resource_utils.ToAndroidLocaleName(src_locale)
+ src_line = None
+ for pos in xrange(start, end):
+ if src_values in gn_lines[pos]:
+ src_line = gn_lines[pos]
+ break
+
+ if not src_line:
+ raise Exception(
+ 'Cannot find output list item with "%s" locale' % src_locale)
+
+ line_count = end - 1
+ for locale in missing_locales:
+ if locale == _DEFAULT_LOCALE:
+ dst_line = src_line.replace('values-%s/' % src_locale, 'values/')
+ else:
+ dst_line = src_line.replace(
+ 'values-%s/' % src_locale,
+ 'values-%s/' % resource_utils.ToAndroidLocaleName(locale))
+ gn_lines.insert(line_count, dst_line)
+ line_count += 1
+
+ gn_lines = _SortListSubRange(
+ gn_lines, start, line_count,
+ lambda line: _RE_GN_VALUES_LIST_LINE.match(line).group(1))
+
+ return gn_lines
+
+
+##########################################################################
+##########################################################################
+#####
+##### T R A N S L A T I O N E X P E C T A T I O N S
+#####
+##########################################################################
+##########################################################################
+
+_EXPECTATIONS_FILENAME = 'translation_expectations.pyl'
+
+# Technical note: the format of translation_expectations.pyl
+# is a 'Python literal', which defines a python dictionary, so should
+# be easy to parse. However, when modifying it, care should be taken
+# to respect the line comments and the order of keys within the text
+# file.
+
+
+def _ReadPythonLiteralFile(pyl_path):
+ """Read a .pyl file into a Python data structure."""
+ with open(pyl_path) as f:
+ pyl_content = f.read()
+ # Evaluate as a Python data structure, use an empty global
+ # and local dictionary.
+ return eval(pyl_content, dict(), dict())
+
+
+def _UpdateLocalesInExpectationLines(pyl_lines,
+ wanted_locales,
+ available_width=79):
+ """Update the locales list(s) found in an expectations file.
+
+ Args:
+ pyl_lines: Iterable of input lines from the file.
+ wanted_locales: Set or list of new locale names.
+ available_width: Optional, number of character colums used
+ to word-wrap the new list items.
+ Returns:
+ New list of updated lines.
+ """
+ locales_list = ['"%s"' % loc for loc in sorted(wanted_locales)]
+ result = []
+ line_count = len(pyl_lines)
+ line_num = 0
+ DICT_START = '"languages": ['
+ while line_num < line_count:
+ line = pyl_lines[line_num]
+ line_num += 1
+ result.append(line)
+ # Look for start of "languages" dictionary.
+ pos = line.find(DICT_START)
+ if pos < 0:
+ continue
+
+ start_margin = pos
+ start_line = line_num
+ # Skip over all lines from the list.
+ while (line_num < line_count and
+ not pyl_lines[line_num].rstrip().endswith('],')):
+ line_num += 1
+ continue
+
+ if line_num == line_count:
+ raise Exception('%d: Missing list termination!' % start_line)
+
+ # Format the new list according to the new margin.
+ locale_width = available_width - (start_margin + 2)
+ locale_lines = _PrettyPrintListAsLines(
+ locales_list, locale_width, trailing_comma=True)
+ for locale_line in locale_lines:
+ result.append(' ' * (start_margin + 2) + locale_line)
+ result.append(' ' * start_margin + '],')
+ line_num += 1
+
+ return result
+
+
+class _UpdateLocalesInExpectationLinesTest(unittest.TestCase):
+
+ def test_simple(self):
+ self.maxDiff = 1000
+ input_text = r'''
+# This comment should be preserved
+# 23456789012345678901234567890123456789
+{
+ "android_grd": {
+ "languages": [
+ "aa", "bb", "cc", "dd", "ee",
+ "ff", "gg", "hh", "ii", "jj",
+ "kk"],
+ },
+ # Example with bad indentation in input.
+ "another_grd": {
+ "languages": [
+ "aa", "bb", "cc", "dd", "ee", "ff", "gg", "hh", "ii", "jj", "kk",
+ ],
+ },
+}
+'''
+ expected_text = r'''
+# This comment should be preserved
+# 23456789012345678901234567890123456789
+{
+ "android_grd": {
+ "languages": [
+ "A2", "AA", "BB", "CC", "DD",
+ "E2", "EE", "FF", "GG", "HH",
+ "I2", "II", "JJ", "KK",
+ ],
+ },
+ # Example with bad indentation in input.
+ "another_grd": {
+ "languages": [
+ "A2", "AA", "BB", "CC", "DD",
+ "E2", "EE", "FF", "GG", "HH",
+ "I2", "II", "JJ", "KK",
+ ],
+ },
+}
+'''
+ input_lines = input_text.splitlines()
+ test_locales = ([
+ 'AA', 'BB', 'CC', 'DD', 'EE', 'FF', 'GG', 'HH', 'II', 'JJ', 'KK', 'A2',
+ 'E2', 'I2'
+ ])
+ expected_lines = expected_text.splitlines()
+ self.assertListEqual(
+ _UpdateLocalesInExpectationLines(input_lines, test_locales, 40),
+ expected_lines)
+
+ def test_missing_list_termination(self):
+ input_lines = r'''
+ "languages": ['
+ "aa", "bb", "cc", "dd"
+'''.splitlines()
+ with self.assertRaises(Exception) as cm:
+ _UpdateLocalesInExpectationLines(input_lines, ['a', 'b'], 40)
+
+ self.assertEqual(str(cm.exception), '2: Missing list termination!')
+
+
+def _UpdateLocalesInExpectationFile(pyl_path, wanted_locales):
+ """Update all locales listed in a given expectations file.
+
+ Args:
+ pyl_path: Path to .pyl file to update.
+ wanted_locales: List of locales that need to be written to
+ the file.
+ """
+ tc_locales = {
+ _FixTranslationConsoleLocaleName(locale)
+ for locale in set(wanted_locales) - set([_DEFAULT_LOCALE])
+ }
+
+ with open(pyl_path) as f:
+ input_lines = [l.rstrip() for l in f.readlines()]
+
+ updated_lines = _UpdateLocalesInExpectationLines(input_lines, tc_locales)
+ with build_utils.AtomicOutput(pyl_path) as f:
+ f.writelines('\n'.join(updated_lines) + '\n')
+
+
+##########################################################################
+##########################################################################
+#####
+##### C H E C K E V E R Y T H I N G
+#####
+##########################################################################
+##########################################################################
+
+# pylint: enable=unused-argument
+
+
+def _IsAllInputFile(input_file):
+ return _IsGritInputFile(input_file) or _IsBuildGnInputFile(input_file)
+
+
+def _CheckAllFiles(input_file, input_lines, wanted_locales):
+ errors = []
+ if _IsGritInputFile(input_file):
+ errors += _CheckGrdTranslations(input_file, input_lines, wanted_locales)
+ errors += _CheckGrdAndroidOutputElements(
+ input_file, input_lines, wanted_locales)
+ elif _IsBuildGnInputFile(input_file):
+ errors += _CheckGnAndroidOutputs(input_file, input_lines, wanted_locales)
+ return errors
+
+
+def _AddMissingLocalesInAllFiles(input_file, input_lines, wanted_locales):
+ if _IsGritInputFile(input_file):
+ lines = _AddMissingLocalesInGrdTranslations(
+ input_file, input_lines, wanted_locales)
+ lines = _AddMissingLocalesInGrdAndroidOutputs(
+ input_file, lines, wanted_locales)
+ elif _IsBuildGnInputFile(input_file):
+ lines = _AddMissingLocalesInGnAndroidOutputs(
+ input_file, input_lines, wanted_locales)
+ return lines
+
+
+##########################################################################
+##########################################################################
+#####
+##### C O M M A N D H A N D L I N G
+#####
+##########################################################################
+##########################################################################
+
+class _Command(object):
+ """A base class for all commands recognized by this script.
+
+ Usage is the following:
+ 1) Derived classes must re-define the following class-based fields:
+ - name: Command name (e.g. 'list-locales')
+ - description: Command short description.
+ - long_description: Optional. Command long description.
+ NOTE: As a convenience, if the first character is a newline,
+ it will be omitted in the help output.
+
+ 2) Derived classes for commands that take arguments should override
+ RegisterExtraArgs(), which receives a corresponding argparse
+ sub-parser as argument.
+
+ 3) Derived classes should implement a Run() command, which can read
+ the current arguments from self.args.
+ """
+ name = None
+ description = None
+ long_description = None
+
+ def __init__(self):
+ self._parser = None
+ self.args = None
+
+ def RegisterExtraArgs(self, subparser):
+ pass
+
+ def RegisterArgs(self, parser):
+ subp = parser.add_parser(
+ self.name, help=self.description,
+ description=self.long_description or self.description,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ self._parser = subp
+ subp.set_defaults(command=self)
+ group = subp.add_argument_group('%s arguments' % self.name)
+ self.RegisterExtraArgs(group)
+
+ def ProcessArgs(self, args):
+ self.args = args
+
+
+class _ListLocalesCommand(_Command):
+ """Implement the 'list-locales' command to list locale lists of interest."""
+ name = 'list-locales'
+ description = 'List supported Chrome locales'
+ long_description = r'''
+List locales of interest, by default this prints all locales supported by
+Chrome, but `--type=ios_unsupported` can be used for the list of locales
+unsupported on iOS.
+
+These values are extracted directly from build/config/locales.gni.
+
+Additionally, use the --as-json argument to print the list as a JSON list,
+instead of the default format (which is a space-separated list of locale names).
+'''
+
+ # Maps type argument to a function returning the corresponding locales list.
+ TYPE_MAP = {
+ 'all': ChromeLocales,
+ 'ios_unsupported': IosUnsupportedLocales,
+ }
+
+ def RegisterExtraArgs(self, group):
+ group.add_argument(
+ '--as-json',
+ action='store_true',
+ help='Output as JSON list.')
+ group.add_argument(
+ '--type',
+ choices=tuple(self.TYPE_MAP.viewkeys()),
+ default='all',
+ help='Select type of locale list to print.')
+
+ def Run(self):
+ locale_list = self.TYPE_MAP[self.args.type]()
+ if self.args.as_json:
+ print('[%s]' % ", ".join("'%s'" % loc for loc in locale_list))
+ else:
+ print(' '.join(locale_list))
+
+
+class _CheckInputFileBaseCommand(_Command):
+ """Used as a base for other _Command subclasses that check input files.
+
+ Subclasses should also define the following class-level variables:
+
+ - select_file_func:
+ A predicate that receives a file name (not path) and return True if it
+ should be selected for inspection. Used when scanning directories with
+ '--scan-dir <dir>'.
+
+ - check_func:
+ - fix_func:
+ Two functions passed as parameters to _ProcessFile(), see relevant
+ documentation in this function's definition.
+ """
+ select_file_func = None
+ check_func = None
+ fix_func = None
+
+ def RegisterExtraArgs(self, group):
+ group.add_argument(
+ '--scan-dir',
+ action='append',
+ help='Optional directory to scan for input files recursively.')
+ group.add_argument(
+ 'input',
+ nargs='*',
+ help='Input file(s) to check.')
+ group.add_argument(
+ '--fix-inplace',
+ action='store_true',
+ help='Try to fix the files in-place too.')
+ group.add_argument(
+ '--add-locales',
+ help='Space-separated list of additional locales to use')
+
+ def Run(self):
+ args = self.args
+ input_files = []
+ if args.input:
+ input_files = args.input
+ if args.scan_dir:
+ input_files.extend(_ScanDirectoriesForFiles(
+ args.scan_dir, self.select_file_func.__func__))
+ locales = ChromeLocales()
+ if args.add_locales:
+ locales.extend(args.add_locales.split(' '))
+
+ locales = set(locales)
+
+ for input_file in input_files:
+ _ProcessFile(input_file,
+ locales,
+ self.check_func.__func__,
+ self.fix_func.__func__ if args.fix_inplace else None)
+ print('%sDone.' % (_CONSOLE_START_LINE))
+
+
+class _CheckGrdAndroidOutputsCommand(_CheckInputFileBaseCommand):
+ name = 'check-grd-android-outputs'
+ description = (
+ 'Check the Android resource (.xml) files outputs in GRIT input files.')
+ long_description = r'''
+Check the Android .xml files outputs in one or more input GRIT (.grd) files
+for the following conditions:
+
+ - Each item has a correct 'lang' attribute.
+ - There are no duplicated lines for the same 'lang' attribute.
+ - That there are no extra locales that Chromium doesn't want.
+ - That no wanted locale is missing.
+ - Filenames exist for each listed locale.
+ - Filenames are well-formed.
+'''
+ select_file_func = _IsGritInputFile
+ check_func = _CheckGrdAndroidOutputElements
+ fix_func = _AddMissingLocalesInGrdAndroidOutputs
+
+
+class _CheckGrdTranslationsCommand(_CheckInputFileBaseCommand):
+ name = 'check-grd-translations'
+ description = (
+ 'Check the translation (.xtb) files outputted by .grd input files.')
+ long_description = r'''
+Check the translation (.xtb) file outputs in one or more input GRIT (.grd) files
+for the following conditions:
+
+ - Each item has a correct 'lang' attribute.
+ - There are no duplicated lines for the same 'lang' attribute.
+ - That there are no extra locales that Chromium doesn't want.
+ - That no wanted locale is missing.
+ - Each item has a 'path' attribute.
+ - Each such path value ends up with '.xtb'.
+'''
+ select_file_func = _IsGritInputFile
+ check_func = _CheckGrdTranslations
+ fix_func = _AddMissingLocalesInGrdTranslations
+
+
+class _CheckGnAndroidOutputsCommand(_CheckInputFileBaseCommand):
+ name = 'check-gn-android-outputs'
+ description = 'Check the Android .xml file lists in GN build files.'
+ long_description = r'''
+Check one or more BUILD.gn file, looking for lists of Android resource .xml
+files, and checking that:
+
+ - There are no duplicated output files in the list.
+ - Each output file belongs to a wanted Chromium locale.
+ - There are no output files for unwanted Chromium locales.
+'''
+ select_file_func = _IsBuildGnInputFile
+ check_func = _CheckGnAndroidOutputs
+ fix_func = _AddMissingLocalesInGnAndroidOutputs
+
+
+class _CheckAllCommand(_CheckInputFileBaseCommand):
+ name = 'check-all'
+ description = 'Check everything.'
+ long_description = 'Equivalent to calling all other check-xxx commands.'
+ select_file_func = _IsAllInputFile
+ check_func = _CheckAllFiles
+ fix_func = _AddMissingLocalesInAllFiles
+
+
+class _UpdateExpectationsCommand(_Command):
+ name = 'update-expectations'
+ description = 'Update translation expectations file.'
+ long_description = r'''
+Update %s files to match the current list of locales supported by Chromium.
+This is especially useful to add new locales before updating any GRIT or GN
+input file with the --add-locales option.
+''' % _EXPECTATIONS_FILENAME
+
+ def RegisterExtraArgs(self, group):
+ group.add_argument(
+ '--add-locales',
+ help='Space-separated list of additional locales to use.')
+
+ def Run(self):
+ locales = ChromeLocales()
+ add_locales = self.args.add_locales
+ if add_locales:
+ locales.extend(add_locales.split(' '))
+
+ expectation_paths = [
+ 'tools/gritsettings/translation_expectations.pyl',
+ 'clank/tools/translation_expectations.pyl',
+ ]
+ missing_expectation_files = []
+ for path in enumerate(expectation_paths):
+ file_path = os.path.join(_TOP_SRC_DIR, path)
+ if not os.path.exists(file_path):
+ missing_expectation_files.append(file_path)
+ continue
+ _UpdateLocalesInExpectationFile(file_path, locales)
+
+ if missing_expectation_files:
+ sys.stderr.write('WARNING: Missing file(s): %s\n' %
+ (', '.join(missing_expectation_files)))
+
+
+class _UnitTestsCommand(_Command):
+ name = 'unit-tests'
+ description = 'Run internal unit-tests for this script'
+
+ def RegisterExtraArgs(self, group):
+ group.add_argument(
+ '-v', '--verbose', action='count', help='Increase test verbosity.')
+ group.add_argument('args', nargs=argparse.REMAINDER)
+
+ def Run(self):
+ argv = [_SCRIPT_NAME] + self.args.args
+ unittest.main(argv=argv, verbosity=self.args.verbose)
+
+
+# List of all commands supported by this script.
+_COMMANDS = [
+ _ListLocalesCommand,
+ _CheckGrdAndroidOutputsCommand,
+ _CheckGrdTranslationsCommand,
+ _CheckGnAndroidOutputsCommand,
+ _CheckAllCommand,
+ _UpdateExpectationsCommand,
+ _UnitTestsCommand,
+]
+
+
+def main(argv):
+ parser = argparse.ArgumentParser(
+ description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+
+ subparsers = parser.add_subparsers()
+ commands = [clazz() for clazz in _COMMANDS]
+ for command in commands:
+ command.RegisterArgs(subparsers)
+
+ if not argv:
+ argv = ['--help']
+
+ args = parser.parse_args(argv)
+ args.command.ProcessArgs(args)
+ args.command.Run()
+
+
+if __name__ == "__main__":
+ main(sys.argv[1:])