1#!/usr/bin/env vpython3 2# Copyright 2019 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Helper script used to manage locale-related files in Chromium. 7 8This script is used to check, and potentially fix, many locale-related files 9in your Chromium workspace, such as: 10 11 - GRIT input files (.grd) and the corresponding translations (.xtb). 12 13 - BUILD.gn files listing Android localized resource string resource .xml 14 generated by GRIT for all supported Chrome locales. These correspond to 15 <output> elements that use the type="android" attribute. 16 17The --scan-dir <dir> option can be used to check for all files under a specific 18directory, and the --fix-inplace option can be used to try fixing any file 19that doesn't pass the check. 20 21This can be very handy to avoid tedious and repetitive work when adding new 22translations / locales to the Chrome code base, since this script can update 23said input files for you. 24 25Important note: checks and fix may fail on some input files. For example 26remoting/resources/remoting_strings.grd contains an in-line comment element 27inside its <outputs> section that breaks the script. The check will fail, and 28trying to fix it too, but at least the file will not be modified. 29""" 30 31 32import argparse 33import json 34import os 35import re 36import shutil 37import subprocess 38import sys 39import unittest 40 41# Assume this script is under build/ 42_SCRIPT_DIR = os.path.dirname(__file__) 43_SCRIPT_NAME = os.path.join(_SCRIPT_DIR, os.path.basename(__file__)) 44_TOP_SRC_DIR = os.path.join(_SCRIPT_DIR, '..') 45 46# Need to import android/gyp/util/resource_utils.py here. 47sys.path.insert(0, os.path.join(_SCRIPT_DIR, 'android/gyp')) 48 49from util import build_utils 50from util import resource_utils 51 52 53# This locale is the default and doesn't have translations. 54_DEFAULT_LOCALE = 'en-US' 55 56# Misc terminal codes to provide human friendly progress output. 57_CONSOLE_CODE_MOVE_CURSOR_TO_COLUMN_0 = '\x1b[0G' 58_CONSOLE_CODE_ERASE_LINE = '\x1b[K' 59_CONSOLE_START_LINE = ( 60 _CONSOLE_CODE_MOVE_CURSOR_TO_COLUMN_0 + _CONSOLE_CODE_ERASE_LINE) 61 62########################################################################## 63########################################################################## 64##### 65##### G E N E R I C H E L P E R F U N C T I O N S 66##### 67########################################################################## 68########################################################################## 69 70def _FixChromiumLangAttribute(lang): 71 """Map XML "lang" attribute values to Chromium locale names.""" 72 _CHROMIUM_LANG_FIXES = { 73 'en': 'en-US', # For now, Chromium doesn't have an 'en' locale. 74 'iw': 'he', # 'iw' is the obsolete form of ISO 639-1 for Hebrew 75 'no': 'nb', # 'no' is used by the Translation Console for Norwegian (nb). 76 } 77 return _CHROMIUM_LANG_FIXES.get(lang, lang) 78 79 80def _FixTranslationConsoleLocaleName(locale): 81 _FIXES = { 82 'nb': 'no', # Norwegian. 83 'he': 'iw', # Hebrew 84 } 85 return _FIXES.get(locale, locale) 86 87 88def _CompareLocaleLists(list_a, list_expected, list_name): 89 """Compare two lists of locale names. Print errors if they differ. 90 91 Args: 92 list_a: First list of locales. 93 list_expected: Second list of locales, as expected. 94 list_name: Name of list printed in error messages. 95 Returns: 96 On success, return False. On error, print error messages and return True. 97 """ 98 errors = [] 99 missing_locales = sorted(set(list_a) - set(list_expected)) 100 if missing_locales: 101 errors.append('Missing locales: %s' % missing_locales) 102 103 extra_locales = sorted(set(list_expected) - set(list_a)) 104 if extra_locales: 105 errors.append('Unexpected locales: %s' % extra_locales) 106 107 if errors: 108 print('Errors in %s definition:' % list_name) 109 for error in errors: 110 print(' %s\n' % error) 111 return True 112 113 return False 114 115 116def _BuildIntervalList(input_list, predicate): 117 """Find ranges of contiguous list items that pass a given predicate. 118 119 Args: 120 input_list: An input list of items of any type. 121 predicate: A function that takes a list item and return True if it 122 passes a given test. 123 Returns: 124 A list of (start_pos, end_pos) tuples, where all items in 125 [start_pos, end_pos) pass the predicate. 126 """ 127 result = [] 128 size = len(input_list) 129 start = 0 130 while True: 131 # Find first item in list that passes the predicate. 132 while start < size and not predicate(input_list[start]): 133 start += 1 134 135 if start >= size: 136 return result 137 138 # Find first item in the rest of the list that does not pass the 139 # predicate. 140 end = start + 1 141 while end < size and predicate(input_list[end]): 142 end += 1 143 144 result.append((start, end)) 145 start = end + 1 146 147 148def _SortListSubRange(input_list, start, end, key_func): 149 """Sort an input list's sub-range according to a specific key function. 150 151 Args: 152 input_list: An input list. 153 start: Sub-range starting position in list. 154 end: Sub-range limit position in list. 155 key_func: A function that extracts a sort key from a line. 156 Returns: 157 A copy of |input_list|, with all items in [|start|, |end|) sorted 158 according to |key_func|. 159 """ 160 result = input_list[:start] 161 inputs = [] 162 for pos in xrange(start, end): 163 line = input_list[pos] 164 key = key_func(line) 165 inputs.append((key, line)) 166 167 for _, line in sorted(inputs): 168 result.append(line) 169 170 result += input_list[end:] 171 return result 172 173 174def _SortElementsRanges(lines, element_predicate, element_key): 175 """Sort all elements of a given type in a list of lines by a given key. 176 177 Args: 178 lines: input lines. 179 element_predicate: predicate function to select elements to sort. 180 element_key: lambda returning a comparison key for each element that 181 passes the predicate. 182 Returns: 183 A new list of input lines, with lines [start..end) sorted. 184 """ 185 intervals = _BuildIntervalList(lines, element_predicate) 186 for start, end in intervals: 187 lines = _SortListSubRange(lines, start, end, element_key) 188 189 return lines 190 191 192def _ProcessFile(input_file, locales, check_func, fix_func): 193 """Process a given input file, potentially fixing it. 194 195 Args: 196 input_file: Input file path. 197 locales: List of Chrome locales to consider / expect. 198 check_func: A lambda called to check the input file lines with 199 (input_lines, locales) argument. It must return an list of error 200 messages, or None on success. 201 fix_func: None, or a lambda called to fix the input file lines with 202 (input_lines, locales). It must return the new list of lines for 203 the input file, and may raise an Exception in case of error. 204 Returns: 205 True at the moment. 206 """ 207 print('%sProcessing %s...' % (_CONSOLE_START_LINE, input_file), end=' ') 208 sys.stdout.flush() 209 with open(input_file) as f: 210 input_lines = f.readlines() 211 errors = check_func(input_file, input_lines, locales) 212 if errors: 213 print('\n%s%s' % (_CONSOLE_START_LINE, '\n'.join(errors))) 214 if fix_func: 215 try: 216 input_lines = fix_func(input_file, input_lines, locales) 217 output = ''.join(input_lines) 218 with open(input_file, 'wt') as f: 219 f.write(output) 220 print('Fixed %s.' % input_file) 221 except Exception as e: # pylint: disable=broad-except 222 print('Skipped %s: %s' % (input_file, e)) 223 224 return True 225 226 227def _ScanDirectoriesForFiles(scan_dirs, file_predicate): 228 """Scan a directory for files that match a given predicate. 229 230 Args: 231 scan_dir: A list of top-level directories to start scan in. 232 file_predicate: lambda function which is passed the file's base name 233 and returns True if its full path, relative to |scan_dir|, should be 234 passed in the result. 235 Returns: 236 A list of file full paths. 237 """ 238 result = [] 239 for src_dir in scan_dirs: 240 for root, _, files in os.walk(src_dir): 241 result.extend(os.path.join(root, f) for f in files if file_predicate(f)) 242 return result 243 244 245def _WriteFile(file_path, file_data): 246 """Write |file_data| to |file_path|.""" 247 with open(file_path, 'w') as f: 248 f.write(file_data) 249 250 251def _FindGnExecutable(): 252 """Locate the real GN executable used by this Chromium checkout. 253 254 This is needed because the depot_tools 'gn' wrapper script will look 255 for .gclient and other things we really don't need here. 256 257 Returns: 258 Path of real host GN executable from current Chromium src/ checkout. 259 """ 260 # Simply scan buildtools/*/gn and return the first one found so we don't 261 # have to guess the platform-specific sub-directory name (e.g. 'linux64' 262 # for 64-bit Linux machines). 263 buildtools_dir = os.path.join(_TOP_SRC_DIR, 'buildtools') 264 for subdir in os.listdir(buildtools_dir): 265 subdir_path = os.path.join(buildtools_dir, subdir) 266 if not os.path.isdir(subdir_path): 267 continue 268 gn_path = os.path.join(subdir_path, 'gn') 269 if os.path.exists(gn_path): 270 return gn_path 271 return None 272 273 274def _PrettyPrintListAsLines(input_list, available_width, trailing_comma=False): 275 result = [] 276 input_str = ', '.join(input_list) 277 while len(input_str) > available_width: 278 pos = input_str.rfind(',', 0, available_width) 279 result.append(input_str[:pos + 1]) 280 input_str = input_str[pos + 1:].lstrip() 281 if trailing_comma and input_str: 282 input_str += ',' 283 result.append(input_str) 284 return result 285 286 287class _PrettyPrintListAsLinesTest(unittest.TestCase): 288 289 def test_empty_list(self): 290 self.assertListEqual([''], _PrettyPrintListAsLines([], 10)) 291 292 def test_wrapping(self): 293 input_list = ['foo', 'bar', 'zoo', 'tool'] 294 self.assertListEqual( 295 _PrettyPrintListAsLines(input_list, 8), 296 ['foo,', 'bar,', 'zoo,', 'tool']) 297 self.assertListEqual( 298 _PrettyPrintListAsLines(input_list, 12), ['foo, bar,', 'zoo, tool']) 299 self.assertListEqual( 300 _PrettyPrintListAsLines(input_list, 79), ['foo, bar, zoo, tool']) 301 302 def test_trailing_comma(self): 303 input_list = ['foo', 'bar', 'zoo', 'tool'] 304 self.assertListEqual( 305 _PrettyPrintListAsLines(input_list, 8, trailing_comma=True), 306 ['foo,', 'bar,', 'zoo,', 'tool,']) 307 self.assertListEqual( 308 _PrettyPrintListAsLines(input_list, 12, trailing_comma=True), 309 ['foo, bar,', 'zoo, tool,']) 310 self.assertListEqual( 311 _PrettyPrintListAsLines(input_list, 79, trailing_comma=True), 312 ['foo, bar, zoo, tool,']) 313 314 315########################################################################## 316########################################################################## 317##### 318##### L O C A L E S L I S T S 319##### 320########################################################################## 321########################################################################## 322 323# Various list of locales that will be extracted from build/config/locales.gni 324# Do not use these directly, use ChromeLocales(), and IosUnsupportedLocales() 325# instead to access these lists. 326_INTERNAL_CHROME_LOCALES = [] 327_INTERNAL_IOS_UNSUPPORTED_LOCALES = [] 328 329 330def ChromeLocales(): 331 """Return the list of all locales supported by Chrome.""" 332 if not _INTERNAL_CHROME_LOCALES: 333 _ExtractAllChromeLocalesLists() 334 return _INTERNAL_CHROME_LOCALES 335 336 337def IosUnsupportedLocales(): 338 """Return the list of locales that are unsupported on iOS.""" 339 if not _INTERNAL_IOS_UNSUPPORTED_LOCALES: 340 _ExtractAllChromeLocalesLists() 341 return _INTERNAL_IOS_UNSUPPORTED_LOCALES 342 343 344def _PrepareTinyGnWorkspace(work_dir, out_subdir_name='out'): 345 """Populate an empty directory with a tiny set of working GN config files. 346 347 This allows us to run 'gn gen <out> --root <work_dir>' as fast as possible 348 to generate files containing the locales list. This takes about 300ms on 349 a decent machine, instead of more than 5 seconds when running the equivalent 350 commands from a real Chromium workspace, which requires regenerating more 351 than 23k targets. 352 353 Args: 354 work_dir: target working directory. 355 out_subdir_name: Name of output sub-directory. 356 Returns: 357 Full path of output directory created inside |work_dir|. 358 """ 359 # Create top-level .gn file that must point to the BUILDCONFIG.gn. 360 _WriteFile(os.path.join(work_dir, '.gn'), 361 'buildconfig = "//BUILDCONFIG.gn"\n') 362 # Create BUILDCONFIG.gn which must set a default toolchain. Also add 363 # all variables that may be used in locales.gni in a declare_args() block. 364 _WriteFile( 365 os.path.join(work_dir, 'BUILDCONFIG.gn'), 366 r'''set_default_toolchain("toolchain") 367declare_args () { 368 is_ios = false 369 is_android = true 370} 371''') 372 373 # Create fake toolchain required by BUILDCONFIG.gn. 374 os.mkdir(os.path.join(work_dir, 'toolchain')) 375 _WriteFile(os.path.join(work_dir, 'toolchain', 'BUILD.gn'), 376 r'''toolchain("toolchain") { 377 tool("stamp") { 378 command = "touch {{output}}" # Required by action() 379 } 380} 381''') 382 383 # Create top-level BUILD.gn, GN requires at least one target to build so do 384 # that with a fake action which will never be invoked. Also write the locales 385 # to misc files in the output directory. 386 _WriteFile( 387 os.path.join(work_dir, 'BUILD.gn'), r'''import("//locales.gni") 388 389action("create_foo") { # fake action to avoid GN complaints. 390 script = "//build/create_foo.py" 391 inputs = [] 392 outputs = [ "$target_out_dir/$target_name" ] 393} 394 395# Write the locales lists to files in the output directory. 396_filename = root_build_dir + "/foo" 397write_file(_filename + ".locales", locales, "json") 398write_file(_filename + ".ios_unsupported_locales", 399 ios_unsupported_locales, 400 "json") 401''') 402 403 # Copy build/config/locales.gni to the workspace, as required by BUILD.gn. 404 shutil.copyfile(os.path.join(_TOP_SRC_DIR, 'build', 'config', 'locales.gni'), 405 os.path.join(work_dir, 'locales.gni')) 406 407 # Create output directory. 408 out_path = os.path.join(work_dir, out_subdir_name) 409 os.mkdir(out_path) 410 411 # And ... we're good. 412 return out_path 413 414 415# Set this global variable to the path of a given temporary directory 416# before calling _ExtractAllChromeLocalesLists() if you want to debug 417# the locales list extraction process. 418_DEBUG_LOCALES_WORK_DIR = None 419 420 421def _ReadJsonList(file_path): 422 """Read a JSON file that must contain a list, and return it.""" 423 with open(file_path) as f: 424 data = json.load(f) 425 assert isinstance(data, list), "JSON file %s is not a list!" % file_path 426 return [item.encode('utf8') for item in data] 427 428 429def _ExtractAllChromeLocalesLists(): 430 with build_utils.TempDir() as tmp_path: 431 if _DEBUG_LOCALES_WORK_DIR: 432 tmp_path = _DEBUG_LOCALES_WORK_DIR 433 build_utils.DeleteDirectory(tmp_path) 434 build_utils.MakeDirectory(tmp_path) 435 436 out_path = _PrepareTinyGnWorkspace(tmp_path, 'out') 437 438 # NOTE: The file suffixes used here should be kept in sync with 439 # build/config/locales.gni 440 gn_executable = _FindGnExecutable() 441 try: 442 subprocess.check_output( 443 [gn_executable, 'gen', out_path, '--root=' + tmp_path]) 444 except subprocess.CalledProcessError as e: 445 print(e.output) 446 raise e 447 448 global _INTERNAL_CHROME_LOCALES 449 _INTERNAL_CHROME_LOCALES = _ReadJsonList( 450 os.path.join(out_path, 'foo.locales')) 451 452 global _INTERNAL_IOS_UNSUPPORTED_LOCALES 453 _INTERNAL_IOS_UNSUPPORTED_LOCALES = _ReadJsonList( 454 os.path.join(out_path, 'foo.ios_unsupported_locales')) 455 456 457########################################################################## 458########################################################################## 459##### 460##### G R D H E L P E R F U N C T I O N S 461##### 462########################################################################## 463########################################################################## 464 465# Technical note: 466# 467# Even though .grd files are XML, an xml parser library is not used in order 468# to preserve the original file's structure after modification. ElementTree 469# tends to re-order attributes in each element when re-writing an XML 470# document tree, which is undesirable here. 471# 472# Thus simple line-based regular expression matching is used instead. 473# 474 475# Misc regular expressions used to match elements and their attributes. 476_RE_OUTPUT_ELEMENT = re.compile(r'<output (.*)\s*/>') 477_RE_TRANSLATION_ELEMENT = re.compile(r'<file( | .* )path="(.*\.xtb)".*/>') 478_RE_FILENAME_ATTRIBUTE = re.compile(r'filename="([^"]*)"') 479_RE_LANG_ATTRIBUTE = re.compile(r'lang="([^"]*)"') 480_RE_PATH_ATTRIBUTE = re.compile(r'path="([^"]*)"') 481_RE_TYPE_ANDROID_ATTRIBUTE = re.compile(r'type="android"') 482 483 484 485def _IsGritInputFile(input_file): 486 """Returns True iff this is a GRIT input file.""" 487 return input_file.endswith('.grd') 488 489 490def _GetXmlLangAttribute(xml_line): 491 """Extract the lang attribute value from an XML input line.""" 492 m = _RE_LANG_ATTRIBUTE.search(xml_line) 493 if not m: 494 return None 495 return m.group(1) 496 497 498class _GetXmlLangAttributeTest(unittest.TestCase): 499 TEST_DATA = { 500 '': None, 501 'foo': None, 502 'lang=foo': None, 503 'lang="foo"': 'foo', 504 '<something lang="foo bar" />': 'foo bar', 505 '<file lang="fr-CA" path="path/to/strings_fr-CA.xtb" />': 'fr-CA', 506 } 507 508 def test_GetXmlLangAttribute(self): 509 for test_line, expected in self.TEST_DATA.items(): 510 self.assertEquals(_GetXmlLangAttribute(test_line), expected) 511 512 513def _SortGrdElementsRanges(grd_lines, element_predicate): 514 """Sort all .grd elements of a given type by their lang attribute.""" 515 return _SortElementsRanges(grd_lines, element_predicate, _GetXmlLangAttribute) 516 517 518def _CheckGrdElementRangeLang(grd_lines, start, end, wanted_locales): 519 """Check the element 'lang' attributes in specific .grd lines range. 520 521 This really checks the following: 522 - Each item has a correct 'lang' attribute. 523 - There are no duplicated lines for the same 'lang' attribute. 524 - That there are no extra locales that Chromium doesn't want. 525 - That no wanted locale is missing. 526 527 Args: 528 grd_lines: Input .grd lines. 529 start: Sub-range start position in input line list. 530 end: Sub-range limit position in input line list. 531 wanted_locales: Set of wanted Chromium locale names. 532 Returns: 533 List of error message strings for this input. Empty on success. 534 """ 535 errors = [] 536 locales = set() 537 for pos in xrange(start, end): 538 line = grd_lines[pos] 539 lang = _GetXmlLangAttribute(line) 540 if not lang: 541 errors.append('%d: Missing "lang" attribute in <output> element' % pos + 542 1) 543 continue 544 cr_locale = _FixChromiumLangAttribute(lang) 545 if cr_locale in locales: 546 errors.append( 547 '%d: Redefinition of <output> for "%s" locale' % (pos + 1, lang)) 548 locales.add(cr_locale) 549 550 extra_locales = locales.difference(wanted_locales) 551 if extra_locales: 552 errors.append('%d-%d: Extra locales found: %s' % (start + 1, end + 1, 553 sorted(extra_locales))) 554 555 missing_locales = wanted_locales.difference(locales) 556 if missing_locales: 557 errors.append('%d-%d: Missing locales: %s' % (start + 1, end + 1, 558 sorted(missing_locales))) 559 560 return errors 561 562 563########################################################################## 564########################################################################## 565##### 566##### G R D A N D R O I D O U T P U T S 567##### 568########################################################################## 569########################################################################## 570 571def _IsGrdAndroidOutputLine(line): 572 """Returns True iff this is an Android-specific <output> line.""" 573 m = _RE_OUTPUT_ELEMENT.search(line) 574 if m: 575 return 'type="android"' in m.group(1) 576 return False 577 578assert _IsGrdAndroidOutputLine(' <output type="android"/>') 579 580# Many of the functions below have unused arguments due to genericity. 581# pylint: disable=unused-argument 582 583def _CheckGrdElementRangeAndroidOutputFilename(grd_lines, start, end, 584 wanted_locales): 585 """Check all <output> elements in specific input .grd lines range. 586 587 This really checks the following: 588 - Filenames exist for each listed locale. 589 - Filenames are well-formed. 590 591 Args: 592 grd_lines: Input .grd lines. 593 start: Sub-range start position in input line list. 594 end: Sub-range limit position in input line list. 595 wanted_locales: Set of wanted Chromium locale names. 596 Returns: 597 List of error message strings for this input. Empty on success. 598 """ 599 errors = [] 600 for pos in xrange(start, end): 601 line = grd_lines[pos] 602 lang = _GetXmlLangAttribute(line) 603 if not lang: 604 continue 605 cr_locale = _FixChromiumLangAttribute(lang) 606 607 m = _RE_FILENAME_ATTRIBUTE.search(line) 608 if not m: 609 errors.append('%d: Missing filename attribute in <output> element' % pos + 610 1) 611 else: 612 filename = m.group(1) 613 if not filename.endswith('.xml'): 614 errors.append( 615 '%d: Filename should end with ".xml": %s' % (pos + 1, filename)) 616 617 dirname = os.path.basename(os.path.dirname(filename)) 618 prefix = ('values-%s' % resource_utils.ToAndroidLocaleName(cr_locale) 619 if cr_locale != _DEFAULT_LOCALE else 'values') 620 if dirname != prefix: 621 errors.append( 622 '%s: Directory name should be %s: %s' % (pos + 1, prefix, filename)) 623 624 return errors 625 626 627def _CheckGrdAndroidOutputElements(grd_file, grd_lines, wanted_locales): 628 """Check all <output> elements related to Android. 629 630 Args: 631 grd_file: Input .grd file path. 632 grd_lines: List of input .grd lines. 633 wanted_locales: set of wanted Chromium locale names. 634 Returns: 635 List of error message strings. Empty on success. 636 """ 637 intervals = _BuildIntervalList(grd_lines, _IsGrdAndroidOutputLine) 638 errors = [] 639 for start, end in intervals: 640 errors += _CheckGrdElementRangeLang(grd_lines, start, end, wanted_locales) 641 errors += _CheckGrdElementRangeAndroidOutputFilename(grd_lines, start, end, 642 wanted_locales) 643 return errors 644 645 646def _AddMissingLocalesInGrdAndroidOutputs(grd_file, grd_lines, wanted_locales): 647 """Fix an input .grd line by adding missing Android outputs. 648 649 Args: 650 grd_file: Input .grd file path. 651 grd_lines: Input .grd line list. 652 wanted_locales: set of Chromium locale names. 653 Returns: 654 A new list of .grd lines, containing new <output> elements when needed 655 for locales from |wanted_locales| that were not part of the input. 656 """ 657 intervals = _BuildIntervalList(grd_lines, _IsGrdAndroidOutputLine) 658 for start, end in reversed(intervals): 659 locales = set() 660 for pos in xrange(start, end): 661 lang = _GetXmlLangAttribute(grd_lines[pos]) 662 locale = _FixChromiumLangAttribute(lang) 663 locales.add(locale) 664 665 missing_locales = wanted_locales.difference(locales) 666 if not missing_locales: 667 continue 668 669 src_locale = 'bg' 670 src_lang_attribute = 'lang="%s"' % src_locale 671 src_line = None 672 for pos in xrange(start, end): 673 if src_lang_attribute in grd_lines[pos]: 674 src_line = grd_lines[pos] 675 break 676 677 if not src_line: 678 raise Exception( 679 'Cannot find <output> element with "%s" lang attribute' % src_locale) 680 681 line_count = end - 1 682 for locale in missing_locales: 683 android_locale = resource_utils.ToAndroidLocaleName(locale) 684 dst_line = src_line.replace( 685 'lang="%s"' % src_locale, 'lang="%s"' % locale).replace( 686 'values-%s/' % src_locale, 'values-%s/' % android_locale) 687 grd_lines.insert(line_count, dst_line) 688 line_count += 1 689 690 # Sort the new <output> elements. 691 return _SortGrdElementsRanges(grd_lines, _IsGrdAndroidOutputLine) 692 693 694########################################################################## 695########################################################################## 696##### 697##### G R D T R A N S L A T I O N S 698##### 699########################################################################## 700########################################################################## 701 702 703def _IsTranslationGrdOutputLine(line): 704 """Returns True iff this is an output .xtb <file> element.""" 705 m = _RE_TRANSLATION_ELEMENT.search(line) 706 return m is not None 707 708 709class _IsTranslationGrdOutputLineTest(unittest.TestCase): 710 711 def test_GrdTranslationOutputLines(self): 712 _VALID_INPUT_LINES = [ 713 '<file path="foo/bar.xtb" />', 714 '<file path="foo/bar.xtb"/>', 715 '<file lang="fr-CA" path="translations/aw_strings_fr-CA.xtb"/>', 716 '<file lang="fr-CA" path="translations/aw_strings_fr-CA.xtb" />', 717 ' <file path="translations/aw_strings_ar.xtb" lang="ar" />', 718 ] 719 _INVALID_INPUT_LINES = ['<file path="foo/bar.xml" />'] 720 721 for line in _VALID_INPUT_LINES: 722 self.assertTrue( 723 _IsTranslationGrdOutputLine(line), 724 '_IsTranslationGrdOutputLine() returned False for [%s]' % line) 725 726 for line in _INVALID_INPUT_LINES: 727 self.assertFalse( 728 _IsTranslationGrdOutputLine(line), 729 '_IsTranslationGrdOutputLine() returned True for [%s]' % line) 730 731 732def _CheckGrdTranslationElementRange(grd_lines, start, end, 733 wanted_locales): 734 """Check all <translations> sub-elements in specific input .grd lines range. 735 736 This really checks the following: 737 - Each item has a 'path' attribute. 738 - Each such path value ends up with '.xtb'. 739 740 Args: 741 grd_lines: Input .grd lines. 742 start: Sub-range start position in input line list. 743 end: Sub-range limit position in input line list. 744 wanted_locales: Set of wanted Chromium locale names. 745 Returns: 746 List of error message strings for this input. Empty on success. 747 """ 748 errors = [] 749 for pos in xrange(start, end): 750 line = grd_lines[pos] 751 lang = _GetXmlLangAttribute(line) 752 if not lang: 753 continue 754 m = _RE_PATH_ATTRIBUTE.search(line) 755 if not m: 756 errors.append('%d: Missing path attribute in <file> element' % pos + 757 1) 758 else: 759 filename = m.group(1) 760 if not filename.endswith('.xtb'): 761 errors.append( 762 '%d: Path should end with ".xtb": %s' % (pos + 1, filename)) 763 764 return errors 765 766 767def _CheckGrdTranslations(grd_file, grd_lines, wanted_locales): 768 """Check all <file> elements that correspond to an .xtb output file. 769 770 Args: 771 grd_file: Input .grd file path. 772 grd_lines: List of input .grd lines. 773 wanted_locales: set of wanted Chromium locale names. 774 Returns: 775 List of error message strings. Empty on success. 776 """ 777 wanted_locales = wanted_locales - set([_DEFAULT_LOCALE]) 778 intervals = _BuildIntervalList(grd_lines, _IsTranslationGrdOutputLine) 779 errors = [] 780 for start, end in intervals: 781 errors += _CheckGrdElementRangeLang(grd_lines, start, end, wanted_locales) 782 errors += _CheckGrdTranslationElementRange(grd_lines, start, end, 783 wanted_locales) 784 return errors 785 786 787# Regular expression used to replace the lang attribute inside .xtb files. 788_RE_TRANSLATIONBUNDLE = re.compile('<translationbundle lang="(.*)">') 789 790 791def _CreateFakeXtbFileFrom(src_xtb_path, dst_xtb_path, dst_locale): 792 """Create a fake .xtb file. 793 794 Args: 795 src_xtb_path: Path to source .xtb file to copy from. 796 dst_xtb_path: Path to destination .xtb file to write to. 797 dst_locale: Destination locale, the lang attribute in the source file 798 will be substituted with this value before its lines are written 799 to the destination file. 800 """ 801 with open(src_xtb_path) as f: 802 src_xtb_lines = f.readlines() 803 804 def replace_xtb_lang_attribute(line): 805 m = _RE_TRANSLATIONBUNDLE.search(line) 806 if not m: 807 return line 808 return line[:m.start(1)] + dst_locale + line[m.end(1):] 809 810 dst_xtb_lines = [replace_xtb_lang_attribute(line) for line in src_xtb_lines] 811 with build_utils.AtomicOutput(dst_xtb_path) as tmp: 812 tmp.writelines(dst_xtb_lines) 813 814 815def _AddMissingLocalesInGrdTranslations(grd_file, grd_lines, wanted_locales): 816 """Fix an input .grd line by adding missing Android outputs. 817 818 This also creates fake .xtb files from the one provided for 'en-GB'. 819 820 Args: 821 grd_file: Input .grd file path. 822 grd_lines: Input .grd line list. 823 wanted_locales: set of Chromium locale names. 824 Returns: 825 A new list of .grd lines, containing new <output> elements when needed 826 for locales from |wanted_locales| that were not part of the input. 827 """ 828 wanted_locales = wanted_locales - set([_DEFAULT_LOCALE]) 829 intervals = _BuildIntervalList(grd_lines, _IsTranslationGrdOutputLine) 830 for start, end in reversed(intervals): 831 locales = set() 832 for pos in xrange(start, end): 833 lang = _GetXmlLangAttribute(grd_lines[pos]) 834 locale = _FixChromiumLangAttribute(lang) 835 locales.add(locale) 836 837 missing_locales = wanted_locales.difference(locales) 838 if not missing_locales: 839 continue 840 841 src_locale = 'en-GB' 842 src_lang_attribute = 'lang="%s"' % src_locale 843 src_line = None 844 for pos in xrange(start, end): 845 if src_lang_attribute in grd_lines[pos]: 846 src_line = grd_lines[pos] 847 break 848 849 if not src_line: 850 raise Exception( 851 'Cannot find <file> element with "%s" lang attribute' % src_locale) 852 853 src_path = os.path.join( 854 os.path.dirname(grd_file), 855 _RE_PATH_ATTRIBUTE.search(src_line).group(1)) 856 857 line_count = end - 1 858 for locale in missing_locales: 859 dst_line = src_line.replace( 860 'lang="%s"' % src_locale, 'lang="%s"' % locale).replace( 861 '_%s.xtb' % src_locale, '_%s.xtb' % locale) 862 grd_lines.insert(line_count, dst_line) 863 line_count += 1 864 865 dst_path = src_path.replace('_%s.xtb' % src_locale, '_%s.xtb' % locale) 866 _CreateFakeXtbFileFrom(src_path, dst_path, locale) 867 868 869 # Sort the new <output> elements. 870 return _SortGrdElementsRanges(grd_lines, _IsTranslationGrdOutputLine) 871 872 873########################################################################## 874########################################################################## 875##### 876##### G N A N D R O I D O U T P U T S 877##### 878########################################################################## 879########################################################################## 880 881_RE_GN_VALUES_LIST_LINE = re.compile( 882 r'^\s*".*values(\-([A-Za-z0-9-]+))?/.*\.xml",\s*$') 883 884def _IsBuildGnInputFile(input_file): 885 """Returns True iff this is a BUILD.gn file.""" 886 return os.path.basename(input_file) == 'BUILD.gn' 887 888 889def _GetAndroidGnOutputLocale(line): 890 """Check a GN list, and return its Android locale if it is an output .xml""" 891 m = _RE_GN_VALUES_LIST_LINE.match(line) 892 if not m: 893 return None 894 895 if m.group(1): # First group is optional and contains group 2. 896 return m.group(2) 897 898 return resource_utils.ToAndroidLocaleName(_DEFAULT_LOCALE) 899 900 901def _IsAndroidGnOutputLine(line): 902 """Returns True iff this is an Android-specific localized .xml output.""" 903 return _GetAndroidGnOutputLocale(line) != None 904 905 906def _CheckGnOutputsRangeForLocalizedStrings(gn_lines, start, end): 907 """Check that a range of GN lines corresponds to localized strings. 908 909 Special case: Some BUILD.gn files list several non-localized .xml files 910 that should be ignored by this function, e.g. in 911 components/cronet/android/BUILD.gn, the following appears: 912 913 inputs = [ 914 ... 915 "sample/res/layout/activity_main.xml", 916 "sample/res/layout/dialog_url.xml", 917 "sample/res/values/dimens.xml", 918 "sample/res/values/strings.xml", 919 ... 920 ] 921 922 These are non-localized strings, and should be ignored. This function is 923 used to detect them quickly. 924 """ 925 for pos in xrange(start, end): 926 if not 'values/' in gn_lines[pos]: 927 return True 928 return False 929 930 931def _CheckGnOutputsRange(gn_lines, start, end, wanted_locales): 932 if not _CheckGnOutputsRangeForLocalizedStrings(gn_lines, start, end): 933 return [] 934 935 errors = [] 936 locales = set() 937 for pos in xrange(start, end): 938 line = gn_lines[pos] 939 android_locale = _GetAndroidGnOutputLocale(line) 940 assert android_locale != None 941 cr_locale = resource_utils.ToChromiumLocaleName(android_locale) 942 if cr_locale in locales: 943 errors.append('%s: Redefinition of output for "%s" locale' % 944 (pos + 1, android_locale)) 945 locales.add(cr_locale) 946 947 extra_locales = locales.difference(wanted_locales) 948 if extra_locales: 949 errors.append('%d-%d: Extra locales: %s' % (start + 1, end + 1, 950 sorted(extra_locales))) 951 952 missing_locales = wanted_locales.difference(locales) 953 if missing_locales: 954 errors.append('%d-%d: Missing locales: %s' % (start + 1, end + 1, 955 sorted(missing_locales))) 956 957 return errors 958 959 960def _CheckGnAndroidOutputs(gn_file, gn_lines, wanted_locales): 961 intervals = _BuildIntervalList(gn_lines, _IsAndroidGnOutputLine) 962 errors = [] 963 for start, end in intervals: 964 errors += _CheckGnOutputsRange(gn_lines, start, end, wanted_locales) 965 return errors 966 967 968def _AddMissingLocalesInGnAndroidOutputs(gn_file, gn_lines, wanted_locales): 969 intervals = _BuildIntervalList(gn_lines, _IsAndroidGnOutputLine) 970 # NOTE: Since this may insert new lines to each interval, process the 971 # list in reverse order to maintain valid (start,end) positions during 972 # the iteration. 973 for start, end in reversed(intervals): 974 if not _CheckGnOutputsRangeForLocalizedStrings(gn_lines, start, end): 975 continue 976 977 locales = set() 978 for pos in xrange(start, end): 979 lang = _GetAndroidGnOutputLocale(gn_lines[pos]) 980 locale = resource_utils.ToChromiumLocaleName(lang) 981 locales.add(locale) 982 983 missing_locales = wanted_locales.difference(locales) 984 if not missing_locales: 985 continue 986 987 src_locale = 'bg' 988 src_values = 'values-%s/' % resource_utils.ToAndroidLocaleName(src_locale) 989 src_line = None 990 for pos in xrange(start, end): 991 if src_values in gn_lines[pos]: 992 src_line = gn_lines[pos] 993 break 994 995 if not src_line: 996 raise Exception( 997 'Cannot find output list item with "%s" locale' % src_locale) 998 999 line_count = end - 1 1000 for locale in missing_locales: 1001 if locale == _DEFAULT_LOCALE: 1002 dst_line = src_line.replace('values-%s/' % src_locale, 'values/') 1003 else: 1004 dst_line = src_line.replace( 1005 'values-%s/' % src_locale, 1006 'values-%s/' % resource_utils.ToAndroidLocaleName(locale)) 1007 gn_lines.insert(line_count, dst_line) 1008 line_count += 1 1009 1010 gn_lines = _SortListSubRange( 1011 gn_lines, start, line_count, 1012 lambda line: _RE_GN_VALUES_LIST_LINE.match(line).group(1)) 1013 1014 return gn_lines 1015 1016 1017########################################################################## 1018########################################################################## 1019##### 1020##### T R A N S L A T I O N E X P E C T A T I O N S 1021##### 1022########################################################################## 1023########################################################################## 1024 1025_EXPECTATIONS_FILENAME = 'translation_expectations.pyl' 1026 1027# Technical note: the format of translation_expectations.pyl 1028# is a 'Python literal', which defines a python dictionary, so should 1029# be easy to parse. However, when modifying it, care should be taken 1030# to respect the line comments and the order of keys within the text 1031# file. 1032 1033 1034def _ReadPythonLiteralFile(pyl_path): 1035 """Read a .pyl file into a Python data structure.""" 1036 with open(pyl_path) as f: 1037 pyl_content = f.read() 1038 # Evaluate as a Python data structure, use an empty global 1039 # and local dictionary. 1040 return eval(pyl_content, dict(), dict()) 1041 1042 1043def _UpdateLocalesInExpectationLines(pyl_lines, 1044 wanted_locales, 1045 available_width=79): 1046 """Update the locales list(s) found in an expectations file. 1047 1048 Args: 1049 pyl_lines: Iterable of input lines from the file. 1050 wanted_locales: Set or list of new locale names. 1051 available_width: Optional, number of character colums used 1052 to word-wrap the new list items. 1053 Returns: 1054 New list of updated lines. 1055 """ 1056 locales_list = ['"%s"' % loc for loc in sorted(wanted_locales)] 1057 result = [] 1058 line_count = len(pyl_lines) 1059 line_num = 0 1060 DICT_START = '"languages": [' 1061 while line_num < line_count: 1062 line = pyl_lines[line_num] 1063 line_num += 1 1064 result.append(line) 1065 # Look for start of "languages" dictionary. 1066 pos = line.find(DICT_START) 1067 if pos < 0: 1068 continue 1069 1070 start_margin = pos 1071 start_line = line_num 1072 # Skip over all lines from the list. 1073 while (line_num < line_count and 1074 not pyl_lines[line_num].rstrip().endswith('],')): 1075 line_num += 1 1076 continue 1077 1078 if line_num == line_count: 1079 raise Exception('%d: Missing list termination!' % start_line) 1080 1081 # Format the new list according to the new margin. 1082 locale_width = available_width - (start_margin + 2) 1083 locale_lines = _PrettyPrintListAsLines( 1084 locales_list, locale_width, trailing_comma=True) 1085 for locale_line in locale_lines: 1086 result.append(' ' * (start_margin + 2) + locale_line) 1087 result.append(' ' * start_margin + '],') 1088 line_num += 1 1089 1090 return result 1091 1092 1093class _UpdateLocalesInExpectationLinesTest(unittest.TestCase): 1094 1095 def test_simple(self): 1096 self.maxDiff = 1000 1097 input_text = r''' 1098# This comment should be preserved 1099# 23456789012345678901234567890123456789 1100{ 1101 "android_grd": { 1102 "languages": [ 1103 "aa", "bb", "cc", "dd", "ee", 1104 "ff", "gg", "hh", "ii", "jj", 1105 "kk"], 1106 }, 1107 # Example with bad indentation in input. 1108 "another_grd": { 1109 "languages": [ 1110 "aa", "bb", "cc", "dd", "ee", "ff", "gg", "hh", "ii", "jj", "kk", 1111 ], 1112 }, 1113} 1114''' 1115 expected_text = r''' 1116# This comment should be preserved 1117# 23456789012345678901234567890123456789 1118{ 1119 "android_grd": { 1120 "languages": [ 1121 "A2", "AA", "BB", "CC", "DD", 1122 "E2", "EE", "FF", "GG", "HH", 1123 "I2", "II", "JJ", "KK", 1124 ], 1125 }, 1126 # Example with bad indentation in input. 1127 "another_grd": { 1128 "languages": [ 1129 "A2", "AA", "BB", "CC", "DD", 1130 "E2", "EE", "FF", "GG", "HH", 1131 "I2", "II", "JJ", "KK", 1132 ], 1133 }, 1134} 1135''' 1136 input_lines = input_text.splitlines() 1137 test_locales = ([ 1138 'AA', 'BB', 'CC', 'DD', 'EE', 'FF', 'GG', 'HH', 'II', 'JJ', 'KK', 'A2', 1139 'E2', 'I2' 1140 ]) 1141 expected_lines = expected_text.splitlines() 1142 self.assertListEqual( 1143 _UpdateLocalesInExpectationLines(input_lines, test_locales, 40), 1144 expected_lines) 1145 1146 def test_missing_list_termination(self): 1147 input_lines = r''' 1148 "languages": [' 1149 "aa", "bb", "cc", "dd" 1150'''.splitlines() 1151 with self.assertRaises(Exception) as cm: 1152 _UpdateLocalesInExpectationLines(input_lines, ['a', 'b'], 40) 1153 1154 self.assertEqual(str(cm.exception), '2: Missing list termination!') 1155 1156 1157def _UpdateLocalesInExpectationFile(pyl_path, wanted_locales): 1158 """Update all locales listed in a given expectations file. 1159 1160 Args: 1161 pyl_path: Path to .pyl file to update. 1162 wanted_locales: List of locales that need to be written to 1163 the file. 1164 """ 1165 tc_locales = { 1166 _FixTranslationConsoleLocaleName(locale) 1167 for locale in set(wanted_locales) - set([_DEFAULT_LOCALE]) 1168 } 1169 1170 with open(pyl_path) as f: 1171 input_lines = [l.rstrip() for l in f.readlines()] 1172 1173 updated_lines = _UpdateLocalesInExpectationLines(input_lines, tc_locales) 1174 with build_utils.AtomicOutput(pyl_path) as f: 1175 f.writelines('\n'.join(updated_lines) + '\n') 1176 1177 1178########################################################################## 1179########################################################################## 1180##### 1181##### C H E C K E V E R Y T H I N G 1182##### 1183########################################################################## 1184########################################################################## 1185 1186# pylint: enable=unused-argument 1187 1188 1189def _IsAllInputFile(input_file): 1190 return _IsGritInputFile(input_file) or _IsBuildGnInputFile(input_file) 1191 1192 1193def _CheckAllFiles(input_file, input_lines, wanted_locales): 1194 errors = [] 1195 if _IsGritInputFile(input_file): 1196 errors += _CheckGrdTranslations(input_file, input_lines, wanted_locales) 1197 errors += _CheckGrdAndroidOutputElements( 1198 input_file, input_lines, wanted_locales) 1199 elif _IsBuildGnInputFile(input_file): 1200 errors += _CheckGnAndroidOutputs(input_file, input_lines, wanted_locales) 1201 return errors 1202 1203 1204def _AddMissingLocalesInAllFiles(input_file, input_lines, wanted_locales): 1205 if _IsGritInputFile(input_file): 1206 lines = _AddMissingLocalesInGrdTranslations( 1207 input_file, input_lines, wanted_locales) 1208 lines = _AddMissingLocalesInGrdAndroidOutputs( 1209 input_file, lines, wanted_locales) 1210 elif _IsBuildGnInputFile(input_file): 1211 lines = _AddMissingLocalesInGnAndroidOutputs( 1212 input_file, input_lines, wanted_locales) 1213 return lines 1214 1215 1216########################################################################## 1217########################################################################## 1218##### 1219##### C O M M A N D H A N D L I N G 1220##### 1221########################################################################## 1222########################################################################## 1223 1224class _Command(object): 1225 """A base class for all commands recognized by this script. 1226 1227 Usage is the following: 1228 1) Derived classes must re-define the following class-based fields: 1229 - name: Command name (e.g. 'list-locales') 1230 - description: Command short description. 1231 - long_description: Optional. Command long description. 1232 NOTE: As a convenience, if the first character is a newline, 1233 it will be omitted in the help output. 1234 1235 2) Derived classes for commands that take arguments should override 1236 RegisterExtraArgs(), which receives a corresponding argparse 1237 sub-parser as argument. 1238 1239 3) Derived classes should implement a Run() command, which can read 1240 the current arguments from self.args. 1241 """ 1242 name = None 1243 description = None 1244 long_description = None 1245 1246 def __init__(self): 1247 self._parser = None 1248 self.args = None 1249 1250 def RegisterExtraArgs(self, subparser): 1251 pass 1252 1253 def RegisterArgs(self, parser): 1254 subp = parser.add_parser( 1255 self.name, help=self.description, 1256 description=self.long_description or self.description, 1257 formatter_class=argparse.RawDescriptionHelpFormatter) 1258 self._parser = subp 1259 subp.set_defaults(command=self) 1260 group = subp.add_argument_group('%s arguments' % self.name) 1261 self.RegisterExtraArgs(group) 1262 1263 def ProcessArgs(self, args): 1264 self.args = args 1265 1266 1267class _ListLocalesCommand(_Command): 1268 """Implement the 'list-locales' command to list locale lists of interest.""" 1269 name = 'list-locales' 1270 description = 'List supported Chrome locales' 1271 long_description = r''' 1272List locales of interest, by default this prints all locales supported by 1273Chrome, but `--type=ios_unsupported` can be used for the list of locales 1274unsupported on iOS. 1275 1276These values are extracted directly from build/config/locales.gni. 1277 1278Additionally, use the --as-json argument to print the list as a JSON list, 1279instead of the default format (which is a space-separated list of locale names). 1280''' 1281 1282 # Maps type argument to a function returning the corresponding locales list. 1283 TYPE_MAP = { 1284 'all': ChromeLocales, 1285 'ios_unsupported': IosUnsupportedLocales, 1286 } 1287 1288 def RegisterExtraArgs(self, group): 1289 group.add_argument( 1290 '--as-json', 1291 action='store_true', 1292 help='Output as JSON list.') 1293 group.add_argument( 1294 '--type', 1295 choices=tuple(self.TYPE_MAP.viewkeys()), 1296 default='all', 1297 help='Select type of locale list to print.') 1298 1299 def Run(self): 1300 locale_list = self.TYPE_MAP[self.args.type]() 1301 if self.args.as_json: 1302 print('[%s]' % ", ".join("'%s'" % loc for loc in locale_list)) 1303 else: 1304 print(' '.join(locale_list)) 1305 1306 1307class _CheckInputFileBaseCommand(_Command): 1308 """Used as a base for other _Command subclasses that check input files. 1309 1310 Subclasses should also define the following class-level variables: 1311 1312 - select_file_func: 1313 A predicate that receives a file name (not path) and return True if it 1314 should be selected for inspection. Used when scanning directories with 1315 '--scan-dir <dir>'. 1316 1317 - check_func: 1318 - fix_func: 1319 Two functions passed as parameters to _ProcessFile(), see relevant 1320 documentation in this function's definition. 1321 """ 1322 select_file_func = None 1323 check_func = None 1324 fix_func = None 1325 1326 def RegisterExtraArgs(self, group): 1327 group.add_argument( 1328 '--scan-dir', 1329 action='append', 1330 help='Optional directory to scan for input files recursively.') 1331 group.add_argument( 1332 'input', 1333 nargs='*', 1334 help='Input file(s) to check.') 1335 group.add_argument( 1336 '--fix-inplace', 1337 action='store_true', 1338 help='Try to fix the files in-place too.') 1339 group.add_argument( 1340 '--add-locales', 1341 help='Space-separated list of additional locales to use') 1342 1343 def Run(self): 1344 args = self.args 1345 input_files = [] 1346 if args.input: 1347 input_files = args.input 1348 if args.scan_dir: 1349 input_files.extend(_ScanDirectoriesForFiles( 1350 args.scan_dir, self.select_file_func.__func__)) 1351 locales = ChromeLocales() 1352 if args.add_locales: 1353 locales.extend(args.add_locales.split(' ')) 1354 1355 locales = set(locales) 1356 1357 for input_file in input_files: 1358 _ProcessFile(input_file, 1359 locales, 1360 self.check_func.__func__, 1361 self.fix_func.__func__ if args.fix_inplace else None) 1362 print('%sDone.' % (_CONSOLE_START_LINE)) 1363 1364 1365class _CheckGrdAndroidOutputsCommand(_CheckInputFileBaseCommand): 1366 name = 'check-grd-android-outputs' 1367 description = ( 1368 'Check the Android resource (.xml) files outputs in GRIT input files.') 1369 long_description = r''' 1370Check the Android .xml files outputs in one or more input GRIT (.grd) files 1371for the following conditions: 1372 1373 - Each item has a correct 'lang' attribute. 1374 - There are no duplicated lines for the same 'lang' attribute. 1375 - That there are no extra locales that Chromium doesn't want. 1376 - That no wanted locale is missing. 1377 - Filenames exist for each listed locale. 1378 - Filenames are well-formed. 1379''' 1380 select_file_func = _IsGritInputFile 1381 check_func = _CheckGrdAndroidOutputElements 1382 fix_func = _AddMissingLocalesInGrdAndroidOutputs 1383 1384 1385class _CheckGrdTranslationsCommand(_CheckInputFileBaseCommand): 1386 name = 'check-grd-translations' 1387 description = ( 1388 'Check the translation (.xtb) files outputted by .grd input files.') 1389 long_description = r''' 1390Check the translation (.xtb) file outputs in one or more input GRIT (.grd) files 1391for the following conditions: 1392 1393 - Each item has a correct 'lang' attribute. 1394 - There are no duplicated lines for the same 'lang' attribute. 1395 - That there are no extra locales that Chromium doesn't want. 1396 - That no wanted locale is missing. 1397 - Each item has a 'path' attribute. 1398 - Each such path value ends up with '.xtb'. 1399''' 1400 select_file_func = _IsGritInputFile 1401 check_func = _CheckGrdTranslations 1402 fix_func = _AddMissingLocalesInGrdTranslations 1403 1404 1405class _CheckGnAndroidOutputsCommand(_CheckInputFileBaseCommand): 1406 name = 'check-gn-android-outputs' 1407 description = 'Check the Android .xml file lists in GN build files.' 1408 long_description = r''' 1409Check one or more BUILD.gn file, looking for lists of Android resource .xml 1410files, and checking that: 1411 1412 - There are no duplicated output files in the list. 1413 - Each output file belongs to a wanted Chromium locale. 1414 - There are no output files for unwanted Chromium locales. 1415''' 1416 select_file_func = _IsBuildGnInputFile 1417 check_func = _CheckGnAndroidOutputs 1418 fix_func = _AddMissingLocalesInGnAndroidOutputs 1419 1420 1421class _CheckAllCommand(_CheckInputFileBaseCommand): 1422 name = 'check-all' 1423 description = 'Check everything.' 1424 long_description = 'Equivalent to calling all other check-xxx commands.' 1425 select_file_func = _IsAllInputFile 1426 check_func = _CheckAllFiles 1427 fix_func = _AddMissingLocalesInAllFiles 1428 1429 1430class _UpdateExpectationsCommand(_Command): 1431 name = 'update-expectations' 1432 description = 'Update translation expectations file.' 1433 long_description = r''' 1434Update %s files to match the current list of locales supported by Chromium. 1435This is especially useful to add new locales before updating any GRIT or GN 1436input file with the --add-locales option. 1437''' % _EXPECTATIONS_FILENAME 1438 1439 def RegisterExtraArgs(self, group): 1440 group.add_argument( 1441 '--add-locales', 1442 help='Space-separated list of additional locales to use.') 1443 1444 def Run(self): 1445 locales = ChromeLocales() 1446 add_locales = self.args.add_locales 1447 if add_locales: 1448 locales.extend(add_locales.split(' ')) 1449 1450 expectation_paths = [ 1451 'tools/gritsettings/translation_expectations.pyl', 1452 'clank/tools/translation_expectations.pyl', 1453 ] 1454 missing_expectation_files = [] 1455 for path in enumerate(expectation_paths): 1456 file_path = os.path.join(_TOP_SRC_DIR, path) 1457 if not os.path.exists(file_path): 1458 missing_expectation_files.append(file_path) 1459 continue 1460 _UpdateLocalesInExpectationFile(file_path, locales) 1461 1462 if missing_expectation_files: 1463 sys.stderr.write('WARNING: Missing file(s): %s\n' % 1464 (', '.join(missing_expectation_files))) 1465 1466 1467class _UnitTestsCommand(_Command): 1468 name = 'unit-tests' 1469 description = 'Run internal unit-tests for this script' 1470 1471 def RegisterExtraArgs(self, group): 1472 group.add_argument( 1473 '-v', '--verbose', action='count', help='Increase test verbosity.') 1474 group.add_argument('args', nargs=argparse.REMAINDER) 1475 1476 def Run(self): 1477 argv = [_SCRIPT_NAME] + self.args.args 1478 unittest.main(argv=argv, verbosity=self.args.verbose) 1479 1480 1481# List of all commands supported by this script. 1482_COMMANDS = [ 1483 _ListLocalesCommand, 1484 _CheckGrdAndroidOutputsCommand, 1485 _CheckGrdTranslationsCommand, 1486 _CheckGnAndroidOutputsCommand, 1487 _CheckAllCommand, 1488 _UpdateExpectationsCommand, 1489 _UnitTestsCommand, 1490] 1491 1492 1493def main(argv): 1494 parser = argparse.ArgumentParser( 1495 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) 1496 1497 subparsers = parser.add_subparsers() 1498 commands = [clazz() for clazz in _COMMANDS] 1499 for command in commands: 1500 command.RegisterArgs(subparsers) 1501 1502 if not argv: 1503 argv = ['--help'] 1504 1505 args = parser.parse_args(argv) 1506 args.command.ProcessArgs(args) 1507 args.command.Run() 1508 1509 1510if __name__ == "__main__": 1511 main(sys.argv[1:]) 1512