# 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.*)\\.' '(?P[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