| # 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 |