| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2013 The Chromium Authors. All rights reserved. | 2 # Copyright 2013 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """ | 6 """ |
| 7 localize.py -- Generates an output file from the given template replacing | 7 localize.py -- Generates an output file from the given template replacing |
| 8 variables and localizing strings. | 8 variables and localizing strings. |
| 9 | 9 |
| 10 The script uses Jinja2 template processing library (src/third_party/jinja2). | 10 The script uses Jinja2 template processing library (src/third_party/jinja2). |
| 11 Variables available to the templates: | 11 Variables available to the templates: |
| 12 - |languages| - the list of languages passed on the command line. ('-l'). | 12 - |languages| - the list of languages passed on the command line. ('-l'). |
| 13 - Each KEY=VALUE define ('-d') can be accesses as |KEY|. | 13 - Each NAME=VALUE define ('-d') can be accesses as {{ NAME }}. |
| 14 - |official_build| is set to '1' when CHROME_BUILD_TYPE environment variable | 14 - |official_build| is set to '1' when CHROME_BUILD_TYPE environment variable |
| 15 is set to "_official". | 15 is set to "_official". |
| 16 | 16 |
| 17 Filters: | 17 Filters: |
| 18 - GetCodepage - returns the code page for the given language. | 18 - GetCodepage - returns the code page for the given language. |
| 19 - GetCodepageDecimal same as GetCodepage, but returns a decimal value. | 19 - GetCodepageDecimal same as GetCodepage, but returns a decimal value. |
| 20 - GetLangId - returns Win32 LANGID. | 20 - GetLangId - returns Win32 LANGID. |
| 21 - GetPrimaryLanguage - returns a named Win32 constant specifing the primary | 21 - GetPrimaryLanguage - returns a named Win32 constant specifing the primary |
| 22 language ID. | 22 language ID. |
| 23 - GetSublanguage - returns a named Win32 constant specifing the sublanguage | 23 - GetSublanguage - returns a named Win32 constant specifing the sublanguage |
| 24 ID. | 24 ID. |
| 25 | 25 |
| 26 Globals: | 26 Globals: |
| 27 - IsRtlLanguage(language) - returns True if the language is right-to-left. |
| 27 - SelectLanguage(language) - allows to select the language to the used by | 28 - SelectLanguage(language) - allows to select the language to the used by |
| 28 {% trans %}{% endtrans %} statements. | 29 {% trans %}{% endtrans %} statements. |
| 29 | 30 |
| 30 """ | 31 """ |
| 31 | 32 |
| 32 import io | 33 import io |
| 33 import json | 34 import json |
| 34 from optparse import OptionParser | 35 from optparse import OptionParser |
| 35 import os | 36 import os |
| 36 import sys | 37 import sys |
| 38 from string import Template |
| 39 |
| 37 | 40 |
| 38 # Win32 primary languages IDs. | 41 # Win32 primary languages IDs. |
| 39 _LANGUAGE_PRIMARY = { | 42 _LANGUAGE_PRIMARY = { |
| 40 'LANG_NEUTRAL' : 0x00, | 43 'LANG_NEUTRAL' : 0x00, |
| 41 'LANG_INVARIANT' : 0x7f, | 44 'LANG_INVARIANT' : 0x7f, |
| 42 'LANG_AFRIKAANS' : 0x36, | 45 'LANG_AFRIKAANS' : 0x36, |
| 43 'LANG_ALBANIAN' : 0x1c, | 46 'LANG_ALBANIAN' : 0x1c, |
| 44 'LANG_ALSATIAN' : 0x84, | 47 'LANG_ALSATIAN' : 0x84, |
| 45 'LANG_AMHARIC' : 0x5e, | 48 'LANG_AMHARIC' : 0x5e, |
| 46 'LANG_ARABIC' : 0x01, | 49 'LANG_ARABIC' : 0x01, |
| (...skipping 488 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 535 | 538 |
| 536 | 539 |
| 537 def IsRtlLanguage(language): | 540 def IsRtlLanguage(language): |
| 538 return language in _RTL_LANGUAGES; | 541 return language in _RTL_LANGUAGES; |
| 539 | 542 |
| 540 | 543 |
| 541 def NormalizeLanguageCode(language): | 544 def NormalizeLanguageCode(language): |
| 542 return language.replace('_', '-', 1) | 545 return language.replace('_', '-', 1) |
| 543 | 546 |
| 544 | 547 |
| 548 def GetDataPackageSuffix(language): |
| 549 lang = NormalizeLanguageCode(language) |
| 550 if lang == 'en': |
| 551 lang = 'en-US' |
| 552 return lang |
| 553 |
| 554 |
| 555 def GetJsonSuffix(language): |
| 556 return language.replace('-', '_', 1) |
| 557 |
| 558 |
| 545 def ReadValuesFromFile(values_dict, file_name): | 559 def ReadValuesFromFile(values_dict, file_name): |
| 546 """ | 560 """ |
| 547 Reads KEYWORD=VALUE settings from the specified file. | 561 Reads NAME=VALUE settings from the specified file. |
| 548 | 562 |
| 549 Everything to the left of the first '=' is the keyword, | 563 Everything to the left of the first '=' is the keyword, |
| 550 everything to the right is the value. No stripping of | 564 everything to the right is the value. No stripping of |
| 551 white space, so beware. | 565 white space, so beware. |
| 552 | 566 |
| 553 The file must exist, otherwise you get the Python exception from open(). | 567 The file must exist, otherwise you get the Python exception from open(). |
| 554 """ | 568 """ |
| 555 for line in open(file_name, 'r').readlines(): | 569 for line in open(file_name, 'r').readlines(): |
| 556 key, val = line.rstrip('\r\n').split('=', 1) | 570 key, val = line.rstrip('\r\n').split('=', 1) |
| 557 values_dict[key] = val | 571 values_dict[key] = val |
| (...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 589 else: | 603 else: |
| 590 if contents == old_contents: | 604 if contents == old_contents: |
| 591 return | 605 return |
| 592 target.close() | 606 target.close() |
| 593 os.unlink(file_name) | 607 os.unlink(file_name) |
| 594 io.open(file_name, 'w', encoding=encoding).write(contents) | 608 io.open(file_name, 'w', encoding=encoding).write(contents) |
| 595 | 609 |
| 596 | 610 |
| 597 class MessageMap: | 611 class MessageMap: |
| 598 """ Provides a dictionary of localized messages for each language.""" | 612 """ Provides a dictionary of localized messages for each language.""" |
| 599 def __init__(self, languages, messages_path): | 613 def __init__(self, languages, locale_dir): |
| 600 self.language = None | 614 self.language = None |
| 601 self.message_map = {} | 615 self.message_map = {} |
| 602 | 616 |
| 603 # Populate the message map | 617 # Populate the message map |
| 604 if messages_path: | 618 if locale_dir: |
| 605 for language in languages: | 619 for language in languages: |
| 606 file_name = os.path.join(messages_path, | 620 file_name = os.path.join(locale_dir, |
| 607 language.replace('-', '_', 1), | 621 GetJsonSuffix(language), |
| 608 'messages.json') | 622 'messages.json') |
| 609 self.message_map[language] = ReadMessagesFromFile(file_name) | 623 self.message_map[language] = ReadMessagesFromFile(file_name) |
| 610 | 624 |
| 611 def GetText(self, message): | 625 def GetText(self, message): |
| 612 """ Returns a localized message for the current language. """ | 626 """ Returns a localized message for the current language. """ |
| 613 return self.message_map[self.language][message] | 627 return self.message_map[self.language][message] |
| 614 | 628 |
| 615 def SelectLanguage(self, language): | 629 def SelectLanguage(self, language): |
| 616 """ Selects the language to be used when retrieving localized messages. """ | 630 """ Selects the language to be used when retrieving localized messages. """ |
| 617 self.language = language | 631 self.language = language |
| 618 | 632 |
| 619 def MakeSelectLanguage(self): | 633 def MakeSelectLanguage(self): |
| 620 """ Returns a function that can be used to select the current language. """ | 634 """ Returns a function that can be used to select the current language. """ |
| 621 return lambda language: self.SelectLanguage(language) | 635 return lambda language: self.SelectLanguage(language) |
| 622 | 636 |
| 623 def MakeGetText(self): | 637 def MakeGetText(self): |
| 624 """ Returns a function that can be used to retrieve a localized message. """ | 638 """ Returns a function that can be used to retrieve a localized message. """ |
| 625 return lambda message: self.GetText(message) | 639 return lambda message: self.GetText(message) |
| 626 | 640 |
| 627 | 641 |
| 628 def Localize(source, target, options): | 642 def Localize(source, locales, options): |
| 629 # Load jinja2 library. | 643 # Set the list of languages to use. |
| 630 if options.jinja2: | 644 languages = map(NormalizeLanguageCode, locales) |
| 631 jinja2_path = os.path.normpath(options.jinja2) | 645 context = { 'languages' : languages } |
| 632 else: | |
| 633 jinja2_path = os.path.normpath(os.path.join(os.path.abspath(__file__), | |
| 634 '../../../third_party/jinja2')) | |
| 635 sys.path.append(os.path.split(jinja2_path)[0]) | |
| 636 from jinja2 import Environment, FileSystemLoader | |
| 637 | 646 |
| 638 # Create jinja2 environment. | 647 # Load the localized messages. |
| 639 (template_path, template_name) = os.path.split(source) | 648 message_map = MessageMap(languages, options.locale_dir) |
| 640 env = Environment(loader=FileSystemLoader(template_path), | |
| 641 extensions=['jinja2.ext.do', 'jinja2.ext.i18n']) | |
| 642 | |
| 643 # Register custom filters. | |
| 644 env.filters['GetCodepage'] = GetCodepage | |
| 645 env.filters['GetCodepageDecimal'] = GetCodepageDecimal | |
| 646 env.filters['GetLangId'] = GetLangId | |
| 647 env.filters['GetPrimaryLanguage'] = GetPrimaryLanguage | |
| 648 env.filters['GetSublanguage'] = GetSublanguage | |
| 649 | |
| 650 # Set the list of languages to use | |
| 651 languages = map(NormalizeLanguageCode, options.languages) | |
| 652 context = { 'languages' : languages } | |
| 653 env.globals['IsRtlLanguage'] = IsRtlLanguage | |
| 654 | |
| 655 # Load the localized messages and register the message map with jinja2.i18n | |
| 656 # extension. | |
| 657 message_map = MessageMap(languages, options.messages_path) | |
| 658 env.globals['SelectLanguage'] = message_map.MakeSelectLanguage() | |
| 659 env.install_gettext_callables(message_map.MakeGetText(), | |
| 660 message_map.MakeGetText()); | |
| 661 | 649 |
| 662 # Add OFFICIAL_BUILD variable the same way chrome/tools/build/version.py | 650 # Add OFFICIAL_BUILD variable the same way chrome/tools/build/version.py |
| 663 # does. | 651 # does. |
| 664 if os.environ.get('CHROME_BUILD_TYPE') == '_official': | 652 if os.environ.get('CHROME_BUILD_TYPE') == '_official': |
| 665 context['official_build'] = '1' | 653 context['official_build'] = '1' |
| 666 else: | 654 else: |
| 667 context['official_build'] = '0' | 655 context['official_build'] = '0' |
| 668 | 656 |
| 669 # Add all variables defined in the command line. | 657 # Add all variables defined in the command line. |
| 670 if options.define: | 658 if options.define: |
| 671 for define in options.define: | 659 for define in options.define: |
| 672 context.update(dict([define.split('=', 1)])); | 660 context.update(dict([define.split('=', 1)])); |
| 673 | 661 |
| 674 # Read KEYWORD=VALUE variables from file. | 662 # Read NAME=VALUE variables from file. |
| 675 if options.input: | 663 if options.variables: |
| 676 for file_name in options.input: | 664 for file_name in options.variables: |
| 677 ReadValuesFromFile(context, file_name) | 665 ReadValuesFromFile(context, file_name) |
| 678 | 666 |
| 679 template = env.get_template(template_name) | 667 env = None |
| 680 WriteIfChanged(target, template.render(context), options.encoding); | 668 template = None |
| 681 return 0; | 669 |
| 670 if source: |
| 671 # Load jinja2 library. |
| 672 if options.jinja2: |
| 673 jinja2_path = os.path.normpath(options.jinja2) |
| 674 else: |
| 675 jinja2_path = os.path.normpath( |
| 676 os.path.join(os.path.abspath(__file__), |
| 677 '../../../../third_party/jinja2')) |
| 678 sys.path.append(os.path.split(jinja2_path)[0]) |
| 679 from jinja2 import Environment, FileSystemLoader |
| 680 |
| 681 # Create jinja2 environment. |
| 682 (template_path, template_name) = os.path.split(source) |
| 683 env = Environment(loader=FileSystemLoader(template_path), |
| 684 extensions=['jinja2.ext.do', 'jinja2.ext.i18n']) |
| 685 |
| 686 # Register custom filters. |
| 687 env.filters['GetCodepage'] = GetCodepage |
| 688 env.filters['GetCodepageDecimal'] = GetCodepageDecimal |
| 689 env.filters['GetLangId'] = GetLangId |
| 690 env.filters['GetPrimaryLanguage'] = GetPrimaryLanguage |
| 691 env.filters['GetSublanguage'] = GetSublanguage |
| 692 |
| 693 # Register the message map with jinja2.i18n extension. |
| 694 env.globals['IsRtlLanguage'] = IsRtlLanguage |
| 695 env.globals['SelectLanguage'] = message_map.MakeSelectLanguage() |
| 696 env.install_gettext_callables(message_map.MakeGetText(), |
| 697 message_map.MakeGetText()); |
| 698 |
| 699 template = env.get_template(template_name) |
| 700 |
| 701 # Generate a separate file per each locale if requested. |
| 702 outputs = [] |
| 703 if options.locale_output: |
| 704 target = Template(options.locale_output) |
| 705 for lang in languages: |
| 706 context['languages'] = [ lang ] |
| 707 context['language'] = lang |
| 708 context['pak_suffix'] = GetDataPackageSuffix(lang) |
| 709 context['json_suffix'] = GetJsonSuffix(lang) |
| 710 |
| 711 template_file_name = target.safe_substitute(context) |
| 712 outputs.append(template_file_name) |
| 713 if not options.print_only: |
| 714 WriteIfChanged(template_file_name, template.render(context), |
| 715 options.encoding) |
| 716 else: |
| 717 outputs.append(options.output) |
| 718 if not options.print_only: |
| 719 WriteIfChanged(options.output, template.render(context), options.encoding) |
| 720 |
| 721 if options.print_only: |
| 722 # Quote each element so filename spaces don't mess up gyp's attempt to parse |
| 723 # it into a list. |
| 724 return " ".join(['"%s"' % x for x in outputs]) |
| 725 |
| 726 return |
| 682 | 727 |
| 683 | 728 |
| 684 def main(): | 729 def DoMain(argv): |
| 685 usage = "Usage: localize [options] <input> <output>" | 730 usage = "Usage: localize [options] locales" |
| 686 parser = OptionParser(usage=usage) | 731 parser = OptionParser(usage=usage) |
| 687 parser.add_option( | 732 parser.add_option( |
| 688 '-d', '--define', dest='define', action='append', type='string', | 733 '-d', '--define', dest='define', action='append', type='string', |
| 689 help='define a variable (VAR=VALUE).') | 734 help='define a variable (NAME=VALUE).') |
| 690 parser.add_option( | |
| 691 '-i', '--input', dest='input', action='append', type='string', | |
| 692 help='read variables from INPUT.') | |
| 693 parser.add_option( | |
| 694 '-l', '--language', dest='languages', action='append', type='string', | |
| 695 help='add LANGUAGE to the list of languages to use.') | |
| 696 parser.add_option( | 735 parser.add_option( |
| 697 '--encoding', dest='encoding', type='string', default='utf-16', | 736 '--encoding', dest='encoding', type='string', default='utf-16', |
| 698 help="set the encoding of <output>. 'utf-16' is the default.") | 737 help="set the encoding of <output>. 'utf-16' is the default.") |
| 699 parser.add_option( | 738 parser.add_option( |
| 700 '--jinja2', dest='jinja2', type='string', | 739 '--jinja2', dest='jinja2', type='string', |
| 701 help="specifies path to the jinja2 library.") | 740 help="specifies path to the jinja2 library.") |
| 702 parser.add_option( | 741 parser.add_option( |
| 703 '--messages_path', dest='messages_path', type='string', | 742 '--locale_dir', dest='locale_dir', type='string', |
| 704 help="set path to localized messages.") | 743 help="set path to localized message files.") |
| 744 parser.add_option( |
| 745 '--locale_output', dest='locale_output', type='string', |
| 746 help='specify the per-locale output file name.') |
| 747 parser.add_option( |
| 748 '-o', '--output', dest='output', type='string', |
| 749 help="specify the output file name.") |
| 750 parser.add_option( |
| 751 '--print_only', dest='print_only', action='store_true', |
| 752 default=False, help='print the output file names only.') |
| 753 parser.add_option( |
| 754 '-t', '--template', dest='template', type='string', |
| 755 help="specify the template file name.") |
| 756 parser.add_option( |
| 757 '--variables', dest='variables', action='append', type='string', |
| 758 help='read variables (NAME=VALUE) from file.') |
| 705 | 759 |
| 706 options, args = parser.parse_args() | 760 options, locales = parser.parse_args(argv) |
| 707 if len(args) != 2: | 761 if not locales: |
| 708 parser.error('Two positional arguments (<input> and <output>) are expected') | 762 parser.error('At least one locale must be specified') |
| 709 if not options.languages: | 763 if bool(options.output) == bool(options.locale_output): |
| 710 parser.error('At least one language must be specified') | 764 parser.error( |
| 711 if not options.messages_path: | 765 'Either --output or --locale_output must be specified but not both') |
| 712 parser.error('--messages_path is required') | 766 if not options.template and not options.print_only: |
| 767 parser.error('The template name is required unless --print_only is used') |
| 713 | 768 |
| 714 return Localize(args[0], args[1], options) | 769 return Localize(options.template, locales, options) |
| 715 | 770 |
| 716 if __name__ == '__main__': | 771 if __name__ == '__main__': |
| 717 sys.exit(main()) | 772 sys.exit(DoMain(sys.argv[1:])) |
| 718 | 773 |
| OLD | NEW |