summaryrefslogtreecommitdiffstats
path: root/python/compare-locales/compare_locales/webapps.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/compare-locales/compare_locales/webapps.py')
-rw-r--r--python/compare-locales/compare_locales/webapps.py235
1 files changed, 235 insertions, 0 deletions
diff --git a/python/compare-locales/compare_locales/webapps.py b/python/compare-locales/compare_locales/webapps.py
new file mode 100644
index 000000000..42f5b5657
--- /dev/null
+++ b/python/compare-locales/compare_locales/webapps.py
@@ -0,0 +1,235 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+'''gaia-style web apps support
+
+This variant supports manifest.webapp localization as well as
+.properties files with a naming scheme of locales/foo.*.properties.
+'''
+
+from collections import defaultdict
+import json
+import os
+import os.path
+import re
+
+from compare_locales.paths import File, EnumerateDir
+from compare_locales.compare import AddRemove, ContentComparer
+
+
+class WebAppCompare(object):
+ '''For a given directory, analyze
+ /manifest.webapp
+ /locales/*.*.properties
+
+ Deduce the present locale codes.
+ '''
+ ignore_dirs = EnumerateDir.ignore_dirs
+ reference_locale = 'en-US'
+
+ def __init__(self, basedir):
+ '''Constructor
+ :param basedir: Directory of the web app to inspect
+ '''
+ self.basedir = basedir
+ self.manifest = Manifest(basedir, self.reference_locale)
+ self.files = FileComparison(basedir, self.reference_locale)
+ self.watcher = None
+
+ def compare(self, locales):
+ '''Compare the manifest.webapp and the locales/*.*.properties
+ '''
+ if not locales:
+ locales = self.locales()
+ self.manifest.compare(locales)
+ self.files.compare(locales)
+
+ def setWatcher(self, watcher):
+ self.watcher = watcher
+ self.manifest.watcher = watcher
+ self.files.watcher = watcher
+
+ def locales(self):
+ '''Inspect files on disk to find present languages.
+ :rtype: List of locales, sorted, including reference.
+ '''
+ locales = set(self.manifest.strings.keys())
+ locales.update(self.files.locales())
+ locales = list(sorted(locales))
+ return locales
+
+
+class Manifest(object):
+ '''Class that helps with parsing and inspection of manifest.webapp.
+ '''
+
+ def __init__(self, basedir, reference_locale):
+ self.file = File(os.path.join(basedir, 'manifest.webapp'),
+ 'manifest.webapp')
+ self.reference_locale = reference_locale
+ self._strings = None
+ self.watcher = None
+
+ @property
+ def strings(self):
+ if self._strings is None:
+ self._strings = self.load_and_parse()
+ return self._strings
+
+ def load_and_parse(self):
+ try:
+ manifest = json.load(open(self.file.fullpath))
+ except (ValueError, IOError), e:
+ if self.watcher:
+ self.watcher.notify('error', self.file, str(e))
+ return False
+ return self.extract_manifest_strings(manifest)
+
+ def extract_manifest_strings(self, manifest_fragment):
+ '''Extract localizable strings from a manifest dict.
+ This method is recursive, and returns a two-level dict,
+ first level being locale codes, second level being generated
+ key and localized value. Keys are generated by concatenating
+ each level in the json with a ".".
+ '''
+ rv = defaultdict(dict)
+ localizable = manifest_fragment.pop('locales', {})
+ if localizable:
+ for locale, keyvalue in localizable.iteritems():
+ for key, value in keyvalue.iteritems():
+ key = '.'.join(['locales', 'AB_CD', key])
+ rv[locale][key] = value
+ for key, sub_manifest in manifest_fragment.iteritems():
+ if not isinstance(sub_manifest, dict):
+ continue
+ subdict = self.extract_manifest_strings(sub_manifest)
+ if subdict:
+ for locale, keyvalue in subdict:
+ rv[locale].update((key + '.' + subkey, value)
+ for subkey, value
+ in keyvalue.iteritems())
+ return rv
+
+ def compare(self, locales):
+ strings = self.strings
+ if not strings:
+ return
+ # create a copy so that we can mock around with it
+ strings = strings.copy()
+ reference = strings.pop(self.reference_locale)
+ for locale in locales:
+ if locale == self.reference_locale:
+ continue
+ self.compare_strings(reference,
+ strings.get(locale, {}),
+ locale)
+
+ def compare_strings(self, reference, l10n, locale):
+ add_remove = AddRemove()
+ add_remove.set_left(sorted(reference.keys()))
+ add_remove.set_right(sorted(l10n.keys()))
+ missing = obsolete = changed = unchanged = 0
+ for op, item_or_pair in add_remove:
+ if op == 'equal':
+ if reference[item_or_pair[0]] == l10n[item_or_pair[1]]:
+ unchanged += 1
+ else:
+ changed += 1
+ else:
+ key = item_or_pair.replace('.AB_CD.',
+ '.%s.' % locale)
+ if op == 'add':
+ # obsolete entry
+ obsolete += 1
+ self.watcher.notify('obsoleteEntity', self.file, key)
+ else:
+ # missing entry
+ missing += 1
+ self.watcher.notify('missingEntity', self.file, key)
+
+
+class FileComparison(object):
+ '''Compare the locales/*.*.properties files inside a webapp.
+ '''
+ prop = re.compile('(?P<base>.*)\\.'
+ '(?P<locale>[a-zA-Z]+(?:-[a-zA-Z]+)*)'
+ '\\.properties$')
+
+ def __init__(self, basedir, reference_locale):
+ self.basedir = basedir
+ self.reference_locale = reference_locale
+ self.watcher = None
+ self._reference = self._files = None
+
+ def locales(self):
+ '''Get the locales present in the webapp
+ '''
+ self.files()
+ locales = self._files.keys()
+ locales.sort()
+ return locales
+
+ def compare(self, locales):
+ self.files()
+ for locale in locales:
+ l10n = self._files[locale]
+ filecmp = AddRemove()
+ filecmp.set_left(sorted(self._reference.keys()))
+ filecmp.set_right(sorted(l10n.keys()))
+ for op, item_or_pair in filecmp:
+ if op == 'equal':
+ self.watcher.compare(self._reference[item_or_pair[0]],
+ l10n[item_or_pair[1]])
+ elif op == 'add':
+ # obsolete file
+ self.watcher.remove(l10n[item_or_pair])
+ else:
+ # missing file
+ _path = '.'.join([item_or_pair, locale, 'properties'])
+ missingFile = File(
+ os.path.join(self.basedir, 'locales', _path),
+ 'locales/' + _path)
+ self.watcher.add(self._reference[item_or_pair],
+ missingFile)
+
+ def files(self):
+ '''Read the list of locales from disk.
+ '''
+ if self._reference:
+ return
+ self._reference = {}
+ self._files = defaultdict(dict)
+ path_list = self._listdir()
+ for path in path_list:
+ match = self.prop.match(path)
+ if match is None:
+ continue
+ locale = match.group('locale')
+ if locale == self.reference_locale:
+ target = self._reference
+ else:
+ target = self._files[locale]
+ fullpath = os.path.join(self.basedir, 'locales', path)
+ target[match.group('base')] = File(fullpath, 'locales/' + path)
+
+ def _listdir(self):
+ 'Monkey-patch this for testing.'
+ return os.listdir(os.path.join(self.basedir, 'locales'))
+
+
+def compare_web_app(basedir, locales, other_observer=None):
+ '''Compare gaia-style web app.
+
+ Optional arguments are:
+ - other_observer. A object implementing
+ notify(category, _file, data)
+ The return values of that callback are ignored.
+ '''
+ comparer = ContentComparer()
+ if other_observer is not None:
+ comparer.add_observer(other_observer)
+ webapp_comp = WebAppCompare(basedir)
+ webapp_comp.setWatcher(comparer)
+ webapp_comp.compare(locales)
+ return comparer.observer