# 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