зеркало из https://github.com/mozilla/gecko-dev.git
236 строки
8.2 KiB
Python
236 строки
8.2 KiB
Python
|
# 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
|