summaryrefslogtreecommitdiffstats
path: root/ipatests/i18n.py
diff options
context:
space:
mode:
Diffstat (limited to 'ipatests/i18n.py')
-rwxr-xr-xipatests/i18n.py838
1 files changed, 838 insertions, 0 deletions
diff --git a/ipatests/i18n.py b/ipatests/i18n.py
new file mode 100755
index 000000000..9c8479bb0
--- /dev/null
+++ b/ipatests/i18n.py
@@ -0,0 +1,838 @@
+#!/usr/bin/python
+# Authors:
+# John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2010 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# WARNING: Do not import ipa modules, this is also used as a
+# stand-alone script (invoked from install/po Makefile).
+import optparse
+import sys
+import gettext
+import locale
+import re
+import os
+import traceback
+import polib
+from collections import namedtuple
+
+'''
+We test our translations by taking the original untranslated string
+(e.g. msgid) and prepend a prefix character and then append a suffix
+character. The test consists of asserting that the first character in the
+translated string is the prefix, the last character in the translated string
+is the suffix and the everything between the first and last character exactly
+matches the original msgid.
+
+We use unicode characters not in the ascii character set for the prefix and
+suffix to enhance the test. To make reading the translated string easier the
+prefix is the unicode right pointing arrow and the suffix left pointing arrow,
+thus the translated string looks like the original string enclosed in
+arrows. In ASCII art the string "foo" would render as:
+-->foo<--
+'''
+
+#-------------------------------------------------------------------------------
+
+verbose = False
+print_traceback = False
+pedantic = False
+show_strings = True
+
+# Unicode right pointing arrow
+prefix = u'\u2192' # utf-8 == '\xe2\x86\x92'
+# Unicode left pointing arrow
+suffix = u'\u2190' # utf-8 == '\xe2\x86\x90'
+
+page_width = 80
+section_seperator = '=' * page_width
+entry_seperator = '-' * page_width
+
+#-------------------------------------------------------------------------------
+# For efficiency compile these regexps just once
+_substitution_regexps = [re.compile(r'%[srduoxf]\b'), # e.g. %s
+ re.compile(r'%\(\w+\)[srduoxf]\b'), # e.g. %(foo)s
+ re.compile(r'\$\w+'), # e.g. $foo
+ re.compile(r'\${\w+}'), # e.g. ${foo}
+ re.compile(r'\$\(\w+\)') # e.g. $(foo)
+ ]
+# Python style substitution, e.g. %(foo)s
+# where foo is the key and s is the format char
+# group 1: whitespace between % and (
+# group 2: whitespace between ( and key
+# group 3: whitespace between key and )
+# group 4: whitespace between ) and format char
+# group 5: format char
+_python_substitution_regexp = re.compile(r'%(\s*)\((\s*)\w+(\s*)\)(\s*)([srduoxf]\b)?')
+
+# Shell style substitution, e.g. $foo $(foo) ${foo}
+# where foo is the variable
+_shell_substitution_regexp = re.compile(r'\$(\s*)([({]?)(\s*)\w+(\s*)([)}]?)')
+# group 1: whitespace between $ and delimiter
+# group 2: begining delimiter
+# group 3: whitespace between beginning delmiter and variable
+# group 4: whitespace between variable and ending delimiter
+# group 5: ending delimiter
+
+printf_fmt_re = re.compile(
+ r"%" # start
+ "(\d+\$)?" # fmt_arg (group 1)
+ "(([#0 +'I]|-(?!\d))*)" # flags (group 2)
+ "(([+-]?([1-9][0-9]*)?)|(\*|\*\d+\$))?" # width (group 4)
+ "(\.((-?\d*)|(\*|)|(\*\d+\$)))?" # precision (group 8)
+ "(h|hh|l|ll|L|j|z|t)?" # length (group 13)
+ "([diouxXeEfFgGaAcspnm%])") # conversion (group 14)
+
+#-------------------------------------------------------------------------------
+
+def get_prog_langs(entry):
+ '''
+ Given an entry in a pot or po file return a set of the
+ programming languges it was found in. It needs to be a set
+ because the same msgid may appear in more than one file which may
+ be in different programming languages.
+
+ Note: One might think you could use the c-format etc. flags to
+ attached to entry to make this determination, but you can't. Those
+ flags refer to the style of the string not the programming
+ language it came from. Also the flags are often omitted and/or are
+ inaccurate.
+
+ For now we just look at the file extension. If we knew the path to
+ the file we could use other heuristics such as looking for the
+ shbang interpreter string.
+
+ The set of possible language types witch might be returned are:
+
+ * c
+ * python
+
+ '''
+ result = set()
+
+ for location in entry.occurrences:
+ filename = location[0]
+ ext = os.path.splitext(filename)[1]
+
+ if ext in ('.c', '.h', '.cxx', '.cpp', '.hxx'):
+ result.add('c')
+ elif ext in ('.py'):
+ result.add('python')
+
+ return result
+
+def parse_printf_fmt(s):
+ '''
+ Parse a printf style format string and return a list of format
+ conversions found in the string.
+
+ Each conversion specification is introduced by the character %, and
+ ends with a conversion specifier. In between there may be (in this
+ order) zero or more flags, an optional minimum field width, an
+ optional precision and an optional length modifier. See "man 3
+ printf" for details.
+
+ Each item in the returned list is a dict whose keys are the
+ sub-parts of a conversion specification. The key and values are:
+
+ fmt
+ The entire format conversion specification
+ fmt_arg
+ The positional index of the matching argument in the argument
+ list, e.g. %1$ indicates the first argument in the argument
+ will be read for this conversion, excludes the leading % but
+ includes the trailing $, 1$ is the fmt_arg in %1$.
+ flags
+ The flag characaters, e.g. 0 is the flag in %08d
+ width
+ The width field, e.g. 20 is the width in %20s
+ precision
+ The precisioin field, e.g. .2 is the precision in %8.2f
+ length
+ The length modifier field, e.g. l is the length modifier in %ld
+ conversion
+ The conversion specifier character, e.g. d is the conversion
+ specification character in %ld
+
+ If the part is not found in the format it's value will be None.
+ '''
+
+ result = []
+
+ # get list of all matches, but skip escaped %
+ matches = [x for x in printf_fmt_re.finditer(s) if x.group(0) != "%%"]
+
+ # build dict of each sub-part of the format, append to result
+ for match in matches:
+ parts = {}
+ parts['fmt'] = match.group(0)
+ parts['fmt_arg'] = match.group(1)
+ parts['flags'] = match.group(2) or None
+ parts['width'] = match.group(4) or None
+ parts['precision'] = match.group(8)
+ parts['length'] = match.group(13)
+ parts['conversion'] = match.group(14)
+
+ result.append(parts)
+
+ return result
+
+def validate_substitutions_match(s1, s2, s1_name='string1', s2_name='string2'):
+ '''
+ Validate both s1 and s2 have the same number of substitution strings.
+ A substitution string would be something that looked like this:
+
+ * %(foo)s
+ * $foo
+ * ${foo}
+ * $(foo)
+
+ The substitutions may appear in any order in s1 and s2, however their
+ format must match exactly and the exact same number of each must exist
+ in both s1 and s2.
+
+ A list of error diagnostics is returned explaining how s1 and s2 failed
+ the validation check. If the returned error list is empty then the
+ validation succeeded.
+
+ :param s1: First string to validate
+ :param s2: First string to validate
+ :param s1_name: In diagnostic messages the name for s1
+ :param s2_name: In diagnostic messages the name for s2
+ :return: List of diagnostic error messages, if empty then success
+ '''
+ errors = []
+
+ def get_subs(s):
+ '''
+ Return a dict whoses keys are each unique substitution and whose
+ value is the count of how many times that substitution appeared.
+ '''
+ subs = {}
+ for regexp in _substitution_regexps:
+ for match in regexp.finditer(s):
+ matched = match.group(0)
+ subs[matched] = subs.get(matched, 0) + 1
+ return subs
+
+ # Get the substitutions and their occurance counts
+ subs1 = get_subs(s1)
+ subs2 = get_subs(s2)
+
+ # Form a set for each strings substitutions and
+ # do set subtraction and interesection
+ set1 = set(subs1.keys())
+ set2 = set(subs2.keys())
+
+ missing1 = set2 - set1
+ missing2 = set1 - set2
+ common = set1 & set2
+
+ # Test for substitutions which are absent in either string
+ if missing1:
+ errors.append("The following substitutions are absent in %s: %s" %
+ (s1_name, ' '.join(missing1)))
+
+ if missing2:
+ errors.append("The following substitutions are absent in %s: %s" %
+ (s2_name, ' '.join(missing2)))
+
+ if pedantic:
+ # For the substitutions which are shared assure they occur an equal number of times
+ for sub in common:
+ if subs1[sub] != subs2[sub]:
+ errors.append("unequal occurances of '%s', %s has %d occurances, %s has %d occurances" %
+ (sub, s1_name, subs1[sub], s2_name, subs2[sub]))
+
+ if errors:
+ if show_strings:
+ errors.append('>>> %s <<<' % s1_name)
+ errors.append(s1.rstrip())
+
+ errors.append('>>> %s <<<' % s2_name)
+ errors.append(s2.rstrip())
+ return errors
+
+
+def validate_substitution_syntax(s, s_name='string'):
+ '''
+ If s has one or more substitution variables then validate they
+ are syntactically correct.
+ A substitution string would be something that looked like this:
+
+ * %(foo)s
+ * $foo
+ * ${foo}
+ * $(foo)
+
+ A list of error diagnostics is returned explaining how s1 and s2 failed
+ the validation check. If the returned error list is empty then the
+ validation succeeded.
+
+ :param s: String to validate
+ :param s_name: In diagnostic messages the name for s
+ :return: List of diagnostic error messages, if empty then success
+ '''
+ errors = []
+
+ # Look for Python style substitutions, e.g. %(foo)s
+ for match in _python_substitution_regexp.finditer(s):
+ if match.group(1):
+ errors.append("%s has whitespace between %% and key in '%s'" %
+ (s_name, match.group(0)))
+ if match.group(2) or match.group(3):
+ errors.append("%s has whitespace next to key in '%s'" %
+ (s_name, match.group(0)))
+ if match.group(4):
+ errors.append("%s has whitespace between key and format character in '%s'" %
+ (s_name, match.group(0)))
+ if not match.group(5):
+ errors.append("%s has no format character in '%s'" %
+ (s_name, match.group(0)))
+
+ # Look for shell style substitutions, e.g. $foo $(foo) ${foo}
+ for match in _shell_substitution_regexp.finditer(s):
+ if match.group(1):
+ errors.append("%s has whitespace between $ and variable in '%s'" %
+ (s_name, match.group(0)))
+ if match.group(3) or (match.group(4) and match.group(5)):
+ errors.append("%s has whitespace next to variable in '%s'" %
+ (s_name, match.group(0)))
+
+ beg_delimiter = match.group(2)
+ end_delimiter = match.group(5)
+ matched_delimiters = {'': '', '(': ')', '{': '}'}
+ if beg_delimiter is not None or end_delimiter is not None:
+ if matched_delimiters[beg_delimiter] != end_delimiter:
+ errors.append("%s variable delimiters do not match in '%s', begin delimiter='%s' end delimiter='%s'" %
+ (s_name, match.group(0), beg_delimiter, end_delimiter))
+
+ if errors:
+ if show_strings:
+ errors.append('>>> %s <<<' % s_name)
+ errors.append(s.rstrip())
+
+ return errors
+
+
+def validate_positional_substitutions(s, prog_langs, s_name='string'):
+ '''
+ We do not permit multiple positional substitutions in translation
+ strings (e.g. '%s') because they do not allow translators to reorder the
+ wording. Instead keyword substitutions should be used when there are
+ more than one.
+ '''
+ errors = []
+
+ fmts = parse_printf_fmt(s)
+ n_fmts = len(fmts)
+
+ errors = []
+ if n_fmts > 1:
+ for i, fmt_parts in enumerate(fmts):
+ fmt = fmt_parts['fmt']
+ fmt_arg = fmt_parts['fmt_arg']
+ width = fmt_parts['width']
+
+ if width == '*':
+ errors.append("Error: * width arg in format '%s should be indexed" % fmt)
+
+ if fmt_arg is None:
+ if 'c' in prog_langs:
+ errors.append("%s format '%s' is positional, should use indexed argument" %
+ (s_name, fmt))
+ else:
+ errors.append("%s format '%s' is positional, should use keyword substitution" %
+ (s_name, fmt))
+
+ if errors:
+ if show_strings:
+ errors.append('>>> %s <<<' % s_name)
+ errors.append(s.rstrip())
+
+ return errors
+
+def validate_file(file_path, validation_mode, reference_pot=None):
+ '''
+ Given a pot or po file scan all it's entries looking for problems
+ with variable substitutions. See the following functions for
+ details on how the validation is performed.
+
+ * validate_substitutions_match()
+ * validate_substitution_syntax()
+ * validate_positional_substitutions()
+
+ Returns the number of entries with errors.
+
+ For po files, ``reference_pot`` gives a pot file to merge with (to recover
+ comments and file locations)
+ '''
+
+ def emit_messages():
+ if n_warnings:
+ warning_lines.insert(0, section_seperator)
+ warning_lines.insert(1, "%d validation warnings in %s" % (n_warnings, file_path))
+ print '\n'.join(warning_lines)
+
+ if n_errors:
+ error_lines.insert(0, section_seperator)
+ error_lines.insert(1, "%d validation errors in %s" % (n_errors, file_path))
+ print '\n'.join(error_lines)
+
+ Result = namedtuple('ValidateFileResult', ['n_entries', 'n_msgids', 'n_msgstrs', 'n_warnings', 'n_errors'])
+
+ warning_lines = []
+ error_lines = []
+ n_entries = 0
+ n_msgids = 0
+ n_msgstrs = 0
+ n_entries = 0
+ n_warnings = 0
+ n_errors = 0
+ n_plural_forms = 0
+
+ if not os.path.isfile(file_path):
+ error_lines.append(entry_seperator)
+ error_lines.append('file does not exist "%s"' % (file_path))
+ n_errors += 1
+ emit_messages()
+ return Result(n_entries=n_entries, n_msgids=n_msgids, n_msgstrs=n_msgstrs, n_warnings=n_warnings, n_errors=n_errors)
+
+ try:
+ po = polib.pofile(file_path)
+ except Exception, e:
+ error_lines.append(entry_seperator)
+ error_lines.append('Unable to parse file "%s": %s' % (file_path, e))
+ n_errors += 1
+ emit_messages()
+ return Result(n_entries=n_entries, n_msgids=n_msgids, n_msgstrs=n_msgstrs, n_warnings=n_warnings, n_errors=n_errors)
+
+ if validation_mode == 'po' and reference_pot:
+ # Merge the .pot file for comments and file locations
+ po.merge(reference_pot)
+
+ if validation_mode == 'po':
+ plural_forms = po.metadata.get('Plural-Forms')
+ if not plural_forms:
+ error_lines.append(entry_seperator)
+ error_lines.append("%s: does not have Plural-Forms header" % file_path)
+ n_errors += 1
+ match = re.search(r'\bnplurals\s*=\s*(\d+)', plural_forms)
+ if match:
+ n_plural_forms = int(match.group(1))
+ else:
+ error_lines.append(entry_seperator)
+ error_lines.append("%s: does not specify integer nplurals in Plural-Forms header" % file_path)
+ n_errors += 1
+
+ n_entries = len(po)
+ for entry in po:
+ entry_warnings = []
+ entry_errors = []
+ have_msgid = entry.msgid.strip() != ''
+ have_msgid_plural = entry.msgid_plural.strip() != ''
+ have_msgstr = entry.msgstr.strip() != ''
+
+ if have_msgid:
+ n_msgids += 1
+ if have_msgid_plural:
+ n_msgids += 1
+ if have_msgstr:
+ n_msgstrs += 1
+
+ if validation_mode == 'pot':
+ prog_langs = get_prog_langs(entry)
+ if have_msgid:
+ errors = validate_positional_substitutions(entry.msgid, prog_langs, 'msgid')
+ entry_errors.extend(errors)
+ if have_msgid_plural:
+ errors = validate_positional_substitutions(entry.msgid_plural, prog_langs, 'msgid_plural')
+ entry_errors.extend(errors)
+ elif validation_mode == 'po':
+ if have_msgid:
+ if have_msgstr:
+ errors = validate_substitutions_match(entry.msgid, entry.msgstr, 'msgid', 'msgstr')
+ entry_errors.extend(errors)
+
+ if have_msgid_plural and have_msgstr:
+ n_plurals = 0
+ for index, msgstr in entry.msgstr_plural.items():
+ have_msgstr_plural = msgstr.strip() != ''
+ if have_msgstr_plural:
+ n_plurals += 1
+ errors = validate_substitutions_match(entry.msgid_plural, msgstr, 'msgid_plural', 'msgstr_plural[%s]' % index)
+ entry_errors.extend(errors)
+ else:
+ entry_errors.append('msgstr_plural[%s] is empty' % (index))
+ if n_plural_forms != n_plurals:
+ entry_errors.append('%d plural forms specified, but this entry has %d plurals' % (n_plural_forms, n_plurals))
+
+ if pedantic:
+ if have_msgid:
+ errors = validate_substitution_syntax(entry.msgid, 'msgid')
+ entry_warnings.extend(errors)
+
+ if have_msgid_plural:
+ errors = validate_substitution_syntax(entry.msgid_plural, 'msgid_plural')
+ entry_warnings.extend(errors)
+
+ errors = validate_substitutions_match(entry.msgid, entry.msgid_plural, 'msgid', 'msgid_plural')
+ entry_warnings.extend(errors)
+
+ for index, msgstr in entry.msgstr_plural.items():
+ have_msgstr_plural = msgstr.strip() != ''
+ if have_msgstr_plural:
+ errors = validate_substitution_syntax(msgstr, 'msgstr_plural[%s]' % index)
+ entry_warnings.extend(errors)
+
+ if have_msgstr:
+ errors = validate_substitution_syntax(entry.msgstr, 'msgstr')
+ entry_warnings.extend(errors)
+
+ if entry_warnings:
+ warning_lines.append(entry_seperator)
+ warning_lines.append('locations: %s' % (', '.join(["%s:%d" % (x[0], int(x[1])) for x in entry.occurrences])))
+ warning_lines.extend(entry_warnings)
+ n_warnings += 1
+
+ if entry_errors:
+ error_lines.append(entry_seperator)
+ error_lines.append('locations: %s' % (', '.join(["%s:%d" % (x[0], int(x[1])) for x in entry.occurrences])))
+ error_lines.extend(entry_errors)
+ n_errors += 1
+
+ emit_messages()
+ return Result(n_entries=n_entries, n_msgids=n_msgids, n_msgstrs=n_msgstrs, n_warnings=n_warnings, n_errors=n_errors)
+
+
+#----------------------------------------------------------------------
+def create_po(pot_file, po_file, mo_file):
+
+ if not os.path.isfile(pot_file):
+ print >>sys.stderr, 'file does not exist "%s"' % (pot_file)
+ return 1
+ try:
+ po = polib.pofile(pot_file)
+ except Exception, e:
+ print >>sys.stderr, 'Unable to parse file "%s": %s' % (pot_file, e)
+ return 1
+
+ # Update the metadata in the po file header
+ # It's case insensitive so search the keys in a case insensitive manner
+ #
+ # We need to update the Plural-Forms otherwise gettext.py will raise the
+ # following error:
+ #
+ # raise ValueError, 'plural forms expression could be dangerous'
+ #
+ # It is demanding the rhs of plural= only contains the identifer 'n'
+
+ for k,v in po.metadata.items():
+ if k.lower() == 'plural-forms':
+ po.metadata[k] = 'nplurals=2; plural=(n != 1)'
+ break
+
+
+ # Iterate over all msgid's and form a the msgstr by prepending
+ # the prefix and appending the suffix
+ for entry in po:
+ if entry.msgid_plural:
+ entry.msgstr_plural = {0: prefix + entry.msgid + suffix,
+ 1: prefix + entry.msgid_plural + suffix}
+ else:
+ entry.msgstr = prefix + entry.msgid + suffix
+
+ # Write out the po and mo files
+ po.save(po_file)
+ print "Wrote: %s" % (po_file)
+
+ po.save_as_mofile(mo_file)
+ print "Wrote: %s" % (mo_file)
+
+ return 0
+
+#----------------------------------------------------------------------
+
+def validate_unicode_edit(msgid, msgstr):
+ # Verify the first character is the test prefix
+ if msgstr[0] != prefix:
+ raise ValueError('First char in translated string "%s" not equal to prefix "%s"' %
+ (msgstr.encode('utf-8'), prefix.encode('utf-8')))
+
+ # Verify the last character is the test suffix
+ if msgstr[-1] != suffix:
+ raise ValueError('Last char in translated string "%s" not equal to suffix "%s"' %
+ (msgstr.encode('utf-8'), suffix.encode('utf-8')))
+
+ # Verify everything between the first and last character is the
+ # original untranslated string
+ if msgstr[1:-1] != msgid:
+ raise ValueError('Translated string "%s" minus the first & last character is not equal to msgid "%s"' %
+ (msgstr.encode('utf-8'), msgid))
+
+ if verbose:
+ msg = 'Success: message string "%s" maps to translated string "%s"' % (msgid, msgstr)
+ print msg.encode('utf-8')
+
+
+def test_translations(po_file, lang, domain, locale_dir):
+ # The test installs the test message catalog under the xh_ZA
+ # (e.g. Zambia Xhosa) language by default. It would be nice to
+ # use a dummy language not associated with any real language,
+ # but the setlocale function demands the locale be a valid
+ # known locale, Zambia Xhosa is a reasonable choice :)
+
+ os.environ['LANG'] = lang
+
+ # Create a gettext translation object specifying our domain as
+ # 'ipa' and the locale_dir as 'test_locale' (i.e. where to
+ # look for the message catalog). Then use that translation
+ # object to obtain the translation functions.
+
+ t = gettext.translation(domain, locale_dir)
+
+ get_msgstr = t.ugettext
+ get_msgstr_plural = t.ungettext
+
+ return po_file_iterate(po_file, get_msgstr, get_msgstr_plural)
+
+def po_file_iterate(po_file, get_msgstr, get_msgstr_plural):
+ try:
+ # Iterate over the msgid's
+ if not os.path.isfile(po_file):
+ print >>sys.stderr, 'file does not exist "%s"' % (po_file)
+ return 1
+ try:
+ po = polib.pofile(po_file)
+ except Exception, e:
+ print >>sys.stderr, 'Unable to parse file "%s": %s' % (po_file, e)
+ return 1
+
+ n_entries = 0
+ n_translations = 0
+ n_valid = 0
+ n_fail = 0
+ for entry in po:
+ if entry.msgid_plural:
+ msgid = entry.msgid
+ msgid_plural = entry.msgid_plural
+ msgstr = get_msgstr_plural(msgid, msgid_plural, 1)
+ msgstr_plural = get_msgstr_plural(msgid, msgid_plural, 2)
+
+ try:
+ n_translations += 1
+ validate_unicode_edit(msgid, msgstr)
+ n_valid += 1
+ except Exception, e:
+ n_fail += 1
+ if print_traceback:
+ traceback.print_exc()
+ print >> sys.stderr, "ERROR: %s" % e
+
+ try:
+ n_translations += 1
+ validate_unicode_edit(msgid_plural, msgstr_plural)
+ n_valid += 1
+ except Exception, e:
+ n_fail += 1
+ if print_traceback:
+ traceback.print_exc()
+ print >> sys.stderr, "ERROR: %s" % e
+
+
+ else:
+ msgid = entry.msgid
+ msgstr = get_msgstr(msgid)
+
+ try:
+ n_translations += 1
+ validate_unicode_edit(msgid, msgstr)
+ n_valid += 1
+ except Exception, e:
+ n_fail += 1
+ if print_traceback:
+ traceback.print_exc()
+ print >> sys.stderr, "ERROR: %s" % e
+
+ n_entries += 1
+
+ except Exception, e:
+ if print_traceback:
+ traceback.print_exc()
+ print >> sys.stderr, "ERROR: %s" % e
+ return 1
+
+ if not n_entries:
+ print >> sys.stderr, "ERROR: no translations found in %s" % (po_filename)
+ return 1
+
+ if n_fail:
+ print >> sys.stderr, "ERROR: %d failures out of %d translations" % (n_fail, n_entries)
+ return 1
+
+ print "%d translations in %d messages successfully tested" % (n_translations, n_entries)
+ return 0
+
+#----------------------------------------------------------------------
+
+usage ='''
+
+%prog --test-gettext
+%prog --create-test
+%prog --validate-pot [pot_file1, ...]
+%prog --validate-po po_file1 [po_file2, ...]
+'''
+
+def main():
+ global verbose, print_traceback, pedantic, show_strings
+
+ parser = optparse.OptionParser(usage=usage)
+
+ mode_group = optparse.OptionGroup(parser, 'Operational Mode',
+ 'You must select one these modes to run in')
+
+ mode_group.add_option('-g', '--test-gettext', action='store_const', const='test_gettext', dest='mode',
+ help='create the test translation file(s) and exercise them')
+ mode_group.add_option('-c', '--create-test', action='store_const', const='create_test', dest='mode',
+ help='create the test translation file(s)')
+ mode_group.add_option('-P', '--validate-pot', action='store_const', const='validate_pot', dest='mode',
+ help='validate pot file(s)')
+ mode_group.add_option('-p', '--validate-po', action='store_const', const='validate_po', dest='mode',
+ help='validate po file(s)')
+
+ parser.add_option_group(mode_group)
+ parser.set_defaults(mode='')
+
+ parser.add_option('-s', '--show-strings', action='store_true', dest='show_strings', default=False,
+ help='show the offending string when an error is detected')
+ parser.add_option('--pedantic', action='store_true', dest='pedantic', default=False,
+ help='be aggressive when validating')
+ parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False,
+ help='be informative')
+ parser.add_option('--traceback', action='store_true', dest='print_traceback', default=False,
+ help='print the traceback when an exception occurs')
+
+ param_group = optparse.OptionGroup(parser, 'Run Time Parameters',
+ 'These may be used to modify the run time defaults')
+
+ param_group.add_option('--test-lang', action='store', dest='test_lang', default='test',
+ help="test po file uses this as it's basename (default=test)")
+ param_group.add_option('--lang', action='store', dest='lang', default='xh_ZA',
+ help='lang used for locale, MUST be a valid lang (default=xh_ZA)')
+ param_group.add_option('--domain', action='store', dest='domain', default='ipa',
+ help='translation domain used during test (default=ipa)')
+ param_group.add_option('--locale-dir', action='store', dest='locale_dir', default='test_locale',
+ help='locale directory used during test (default=test_locale)')
+ param_group.add_option('--pot-file', action='store', dest='pot_file', default='ipa.pot',
+ help='default pot file, used when validating pot file or generating test po and mo files (default=ipa.pot)')
+
+ parser.add_option_group(param_group)
+
+ options, args = parser.parse_args()
+
+ verbose = options.verbose
+ print_traceback = options.print_traceback
+ pedantic = options.pedantic
+ show_strings = options.show_strings
+
+ if not options.mode:
+ print >> sys.stderr, 'ERROR: no mode specified'
+ return 1
+
+ if options.mode == 'validate_pot' or options.mode == 'validate_po':
+ if options.mode == 'validate_pot':
+ files = args
+ if not files:
+ files = [options.pot_file]
+ validation_mode = 'pot'
+ reference_pot = None
+ elif options.mode == 'validate_po':
+ files = args
+ if not files:
+ print >> sys.stderr, 'ERROR: no po files specified'
+ return 1
+ validation_mode = 'po'
+ reference_pot = polib.pofile(options.pot_file)
+ else:
+ print >> sys.stderr, 'ERROR: unknown validation mode "%s"' % (options.mode)
+ return 1
+
+ total_entries = 0
+ total_msgids = 0
+ total_msgstrs = 0
+ total_warnings = 0
+ total_errors = 0
+
+ for f in files:
+ result = validate_file(f, validation_mode, reference_pot)
+ total_entries += result.n_entries
+ total_msgids += result.n_msgids
+ total_msgstrs += result.n_msgstrs
+ total_warnings += result.n_warnings
+ total_errors += result.n_errors
+ print "%s: %d entries, %d msgid, %d msgstr, %d warnings %d errors" % \
+ (f, result.n_entries, result.n_msgids, result.n_msgstrs, result.n_warnings, result.n_errors)
+ if total_errors:
+ print section_seperator
+ print "%d errors in %d files" % (total_errors, len(files))
+ return 1
+ else:
+ return 0
+
+ elif options.mode == 'create_test' or 'test_gettext':
+ po_file = '%s.po' % options.test_lang
+ pot_file = options.pot_file
+
+ msg_dir = os.path.join(options.locale_dir, options.lang, 'LC_MESSAGES')
+ if not os.path.exists(msg_dir):
+ os.makedirs(msg_dir)
+
+ mo_basename = '%s.mo' % options.domain
+ mo_file = os.path.join(msg_dir, mo_basename)
+
+ result = create_po(pot_file, po_file, mo_file)
+ if result:
+ return result
+
+ if options.mode == 'create_test':
+ return result
+
+ # The test installs the test message catalog under the xh_ZA
+ # (e.g. Zambia Xhosa) language by default. It would be nice to
+ # use a dummy language not associated with any real language,
+ # but the setlocale function demands the locale be a valid
+ # known locale, Zambia Xhosa is a reasonable choice :)
+
+ lang = options.lang
+
+ # Create a gettext translation object specifying our domain as
+ # 'ipa' and the locale_dir as 'test_locale' (i.e. where to
+ # look for the message catalog). Then use that translation
+ # object to obtain the translation functions.
+
+ domain = options.domain
+ locale_dir = options.locale_dir
+
+ return test_translations(po_file, lang, domain, locale_dir)
+
+ else:
+ print >> sys.stderr, 'ERROR: unknown mode "%s"' % (options.mode)
+ return 1
+
+if __name__ == "__main__":
+ sys.exit(main())