Commit bb19a5f3 authored by cduvall@chromium.org's avatar cduvall@chromium.org

Extensions Docs Server: Internationalized samples

The samples page can now handle names and descriptions of the form '__MSG_...',
and the locale is chosen based on the Accept-Languages HTTP header.

BUG=131095

Review URL: https://chromiumcodereview.appspot.com/10804036

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@148370 0039d316-1c4b-4281-b951-d872f2087c98
parent 6b8f4a92
...@@ -85,15 +85,15 @@ class Handler(webapp.RequestHandler): ...@@ -85,15 +85,15 @@ class Handler(webapp.RequestHandler):
PUBLIC_TEMPLATE_PATH) PUBLIC_TEMPLATE_PATH)
intro_data_source = IntroDataSource(cache_builder, intro_data_source = IntroDataSource(cache_builder,
[INTRO_PATH, ARTICLE_PATH]) [INTRO_PATH, ARTICLE_PATH])
samples_data_source = SamplesDataSource(file_system, samples_data_source_factory = SamplesDataSource.Factory(file_system,
cache_builder, cache_builder,
EXAMPLES_PATH) EXAMPLES_PATH)
template_data_source_factory = TemplateDataSource.Factory( template_data_source_factory = TemplateDataSource.Factory(
branch, branch,
api_data_source, api_data_source,
api_list_data_source, api_list_data_source,
intro_data_source, intro_data_source,
samples_data_source, samples_data_source_factory,
cache_builder, cache_builder,
PUBLIC_TEMPLATE_PATH, PUBLIC_TEMPLATE_PATH,
PRIVATE_TEMPLATE_PATH) PRIVATE_TEMPLATE_PATH)
...@@ -134,11 +134,7 @@ class Handler(webapp.RequestHandler): ...@@ -134,11 +134,7 @@ class Handler(webapp.RequestHandler):
self.response) self.response)
def main(): def main():
handlers = [ run_wsgi_app(webapp.WSGIApplication([('/.*', Handler)], debug=False))
('/.*', Handler),
]
run_wsgi_app(webapp.WSGIApplication(handlers, debug=False))
if __name__ == '__main__': if __name__ == '__main__':
main() main()
...@@ -2,6 +2,16 @@ ...@@ -2,6 +2,16 @@
# Use of this source code is governed by a BSD-style license that can be # Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. # found in the LICENSE file.
import os
def _ProcessFileData(data, path):
if os.path.splitext(path)[-1] not in ['.js', '.html', '.json']:
return data
try:
return unicode(data, 'utf-8')
except:
return unicode(data, 'latin-1')
class FileSystem(object): class FileSystem(object):
"""A FileSystem interface that can read files and directories. """A FileSystem interface that can read files and directories.
""" """
......
...@@ -26,9 +26,6 @@ class FileSystemCache(object): ...@@ -26,9 +26,6 @@ class FileSystemCache(object):
self._populate_function = populate_function self._populate_function = populate_function
self._cache = {} self._cache = {}
def _FetchFile(self, filename):
return self._file_system.Read([filename]).Get()[filename]
def _RecursiveList(self, files): def _RecursiveList(self, files):
all_files = files[:] all_files = files[:]
dirs = {} dirs = {}
...@@ -49,7 +46,7 @@ class FileSystemCache(object): ...@@ -49,7 +46,7 @@ class FileSystemCache(object):
self._cache.pop(path) self._cache.pop(path)
else: else:
return self._cache[path]._cache_data return self._cache[path]._cache_data
cache_data = self._FetchFile(path) cache_data = self._file_system.ReadSingle(path)
self._cache[path] = self._CacheEntry(self._populate_function(cache_data), self._cache[path] = self._CacheEntry(self._populate_function(cache_data),
version) version)
return self._cache[path]._cache_data return self._cache[path]._cache_data
...@@ -66,7 +63,8 @@ class FileSystemCache(object): ...@@ -66,7 +63,8 @@ class FileSystemCache(object):
self._cache.pop(path) self._cache.pop(path)
else: else:
return self._cache[path]._cache_data return self._cache[path]._cache_data
cache_data = self._RecursiveList([path + f for f in self._FetchFile(path)]) cache_data = self._RecursiveList(
[path + f for f in self._file_system.ReadSingle(path)])
self._cache[path] = self._CacheEntry(self._populate_function(cache_data), self._cache[path] = self._CacheEntry(self._populate_function(cache_data),
version) version)
return self._cache[path]._cache_data return self._cache[path]._cache_data
...@@ -60,16 +60,13 @@ class IntroDataSource(object): ...@@ -60,16 +60,13 @@ class IntroDataSource(object):
self._base_paths = base_paths self._base_paths = base_paths
def _MakeIntroDict(self, intro): def _MakeIntroDict(self, intro):
try: parser = _IntroParser()
parser = _IntroParser() parser.feed(intro)
parser.feed(intro) return {
return { 'intro': Handlebar(intro),
'intro': Handlebar(intro), 'toc': parser.toc,
'toc': parser.toc, 'title': parser.page_title
'title': parser.page_title }
}
except Exception as e:
logging.info(e)
def __getitem__(self, key): def __getitem__(self, key):
return self.get(key) return self.get(key)
......
...@@ -4,10 +4,10 @@ ...@@ -4,10 +4,10 @@
import os import os
from file_system import FileSystem import file_system
from future import Future from future import Future
class LocalFileSystem(FileSystem): class LocalFileSystem(file_system.FileSystem):
"""FileSystem implementation which fetches resources from the local """FileSystem implementation which fetches resources from the local
filesystem. filesystem.
""" """
...@@ -19,7 +19,7 @@ class LocalFileSystem(FileSystem): ...@@ -19,7 +19,7 @@ class LocalFileSystem(FileSystem):
def _ReadFile(self, filename): def _ReadFile(self, filename):
with open(os.path.join(self._base_path, filename), 'r') as f: with open(os.path.join(self._base_path, filename), 'r') as f:
return f.read() return file_system._ProcessFileData(f.read(), filename)
def _ListDir(self, dir_name): def _ListDir(self, dir_name):
all_files = [] all_files = []
......
...@@ -28,7 +28,7 @@ class LocalFileSystemTest(unittest.TestCase): ...@@ -28,7 +28,7 @@ class LocalFileSystemTest(unittest.TestCase):
for i in range(7): for i in range(7):
expected.append('file%d.html' % i) expected.append('file%d.html' % i)
self.assertEqual(expected, self.assertEqual(expected,
sorted(self._file_system.Read(['list/']).Get()['list/'])) sorted(self._file_system.ReadSingle('list/')))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -32,16 +32,14 @@ class LocalFileSystemTest(unittest.TestCase): ...@@ -32,16 +32,14 @@ class LocalFileSystemTest(unittest.TestCase):
expected = ['dir/'] expected = ['dir/']
for i in range(7): for i in range(7):
expected.append('file%d.html' % i) expected.append('file%d.html' % i)
self.assertEqual( self.assertEqual(expected,
expected, sorted(self._file_system.ReadSingle('list/')))
sorted(self._file_system.Read(['list/']).Get()['list/']))
expected.remove('file0.html') expected.remove('file0.html')
self._memcache.Set('list/', self._memcache.Set('list/',
(expected, self._file_system.Stat('list/')), (expected, self._file_system.Stat('list/')),
memcache.MEMCACHE_FILE_SYSTEM_READ) memcache.MEMCACHE_FILE_SYSTEM_READ)
self.assertEqual( self.assertEqual(expected,
expected, sorted(self._file_system.ReadSingle('list/')))
sorted(self._file_system.Read(['list/']).Get()['list/']))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
# found in the LICENSE file. # found in the LICENSE file.
import json import json
import logging
import re import re
DEFAULT_ICON_PATH = '/static/images/sample-default-icon.png' DEFAULT_ICON_PATH = '/static/images/sample-default-icon.png'
...@@ -10,74 +11,139 @@ DEFAULT_ICON_PATH = '/static/images/sample-default-icon.png' ...@@ -10,74 +11,139 @@ DEFAULT_ICON_PATH = '/static/images/sample-default-icon.png'
class SamplesDataSource(object): class SamplesDataSource(object):
"""Constructs a list of samples and their respective files and api calls. """Constructs a list of samples and their respective files and api calls.
""" """
def __init__(self, fetcher, cache_builder, samples_path):
self._fetcher = fetcher
self._cache = cache_builder.build(self._MakeSamplesList)
self._samples_path = samples_path
def _GetApiItems(self, js_file): class Factory(object):
return set(re.findall('(chrome\.[a-zA-Z0-9\.]+)', js_file)) """A factory to create SamplesDataSource instances bound to individual
Requests.
"""
def __init__(self, file_system, cache_builder, samples_path):
self._file_system = file_system
self._cache = cache_builder.build(self._MakeSamplesList)
self._samples_path = samples_path
def Create(self, request):
"""Returns a new SamplesDataSource bound to |request|.
"""
return SamplesDataSource(self._cache,
self._samples_path,
request)
def _MakeApiLink(self, prefix, item): def _GetApiItems(self, js_file):
api, name = item.replace('chrome.', '').split('.', 1) return set(re.findall('(chrome\.[a-zA-Z0-9\.]+)', js_file))
return api + '.html#' + prefix + '-' + name
def _GetDataFromManifest(self, path): def _MakeApiLink(self, prefix, item):
manifest_path = path + '/manifest.json' api, name = item.replace('chrome.', '').split('.', 1)
manifest = self._fetcher.Read([manifest_path]).Get()[manifest_path] return api + '.html#' + prefix + '-' + name
manifest_json = json.loads(manifest)
return {
'name': manifest_json.get('name'),
'description': manifest_json.get('description'),
'icon': manifest_json.get('icons', {}).get('128', None)
}
def _MakeSamplesList(self, files): def _GetDataFromManifest(self, path):
samples_list = [] manifest = self._file_system.ReadSingle(path + '/manifest.json')
for filename in sorted(files): manifest_json = json.loads(manifest)
if filename.rsplit('/')[-1] != 'manifest.json': l10n_data = {
continue 'name': manifest_json.get('name', ''),
# This is a little hacky, but it makes a sample page. 'description': manifest_json.get('description', ''),
sample_path = filename.rsplit('/', 1)[-2] 'icon': manifest_json.get('icons', {}).get('128', None),
sample_files = filter(lambda x: x.startswith(sample_path + '/'), files) 'default_locale': manifest_json.get('default_locale', None),
js_files = filter(lambda x: x.endswith('.js'), sample_files) 'locales': {}
js_contents = self._fetcher.Read(js_files).Get() }
api_items = set() if not l10n_data['default_locale']:
for js in js_contents.values(): return l10n_data
api_items.update(self._GetApiItems(js)) locales_path = path + '/_locales/'
locales_dir = self._file_system.ReadSingle(locales_path)
if locales_dir:
locales_files = self._file_system.Read(
[locales_path + f + 'messages.json' for f in locales_dir]).Get()
locales_json = [(path, json.loads(contents))
for path, contents in locales_files.iteritems()]
for path, json_ in locales_json:
l10n_data['locales'][path[len(locales_path):].split('/')[0]] = json_
return l10n_data
api_calls = [] def _MakeSamplesList(self, files):
for item in api_items: samples_list = []
if len(item.split('.')) < 3: for filename in sorted(files):
if filename.rsplit('/')[-1] != 'manifest.json':
continue continue
if item.endswith('.addListener'): # This is a little hacky, but it makes a sample page.
item = item.replace('.addListener', '') sample_path = filename.rsplit('/', 1)[-2]
api_calls.append({ sample_files = [path for path in files
'name': item, if path.startswith(sample_path + '/')]
'link': self._MakeApiLink('event', item) js_files = [path for path in sample_files if path.endswith('.js')]
}) js_contents = self._file_system.Read(js_files).Get()
api_items = set()
for js in js_contents.values():
api_items.update(self._GetApiItems(js))
api_calls = []
for item in api_items:
if len(item.split('.')) < 3:
continue
if item.endswith('.addListener'):
item = item.replace('.addListener', '')
api_calls.append({
'name': item,
'link': self._MakeApiLink('event', item)
})
else:
api_calls.append({
'name': item,
'link': self._MakeApiLink('method', item)
})
l10n_data = self._GetDataFromManifest(sample_path)
sample_base_path = sample_path.split('/', 1)[1]
if l10n_data['icon'] is None:
icon_path = DEFAULT_ICON_PATH
else: else:
api_calls.append({ icon_path = sample_base_path + '/' + l10n_data['icon']
'name': item, l10n_data.update({
'link': self._MakeApiLink('method', item) 'icon': icon_path,
}) 'path': sample_base_path,
samples_info = self._GetDataFromManifest(sample_path) 'files': [f.replace(sample_path + '/', '') for f in sample_files],
sample_base_path = sample_path.split('/', 1)[1] 'api_calls': api_calls
if samples_info['icon'] is None: })
icon_path = DEFAULT_ICON_PATH samples_list.append(l10n_data)
else:
icon_path = sample_base_path + '/' + samples_info['icon'] return samples_list
samples_info.update({
'icon': icon_path, def __init__(self, cache, samples_path, request):
'path': sample_base_path, self._cache = cache
'files': [f.replace(sample_path + '/', '') for f in sample_files], self._samples_path = samples_path
'api_calls': api_calls self._request = request
})
samples_list.append(samples_info) def _GetAcceptedLanguages(self):
return samples_list accept_language = self._request.headers.get('Accept-Language', None)
if accept_language is None:
return []
return [lang_with_q.split(';')[0].strip()
for lang_with_q in accept_language.split(',')]
def __getitem__(self, key): def __getitem__(self, key):
return self.get(key) return self.get(key)
def get(self, key): def get(self, key):
return self._cache.GetFromFileListing(self._samples_path + '/') samples_list = self._cache.GetFromFileListing(self._samples_path + '/')
return_list = []
for dict_ in samples_list:
name = dict_['name']
description = dict_['description']
if name.startswith('__MSG_') or description.startswith('__MSG_'):
try:
# Copy the sample dict so we don't change the dict in the cache.
sample_data = dict_.copy()
name_key = name[len('__MSG_'):-len('__')]
description_key = description[len('__MSG_'):-len('__')]
locale = sample_data['default_locale']
for lang in self._GetAcceptedLanguages():
if lang in sample_data['locales']:
locale = lang
break
locale_data = sample_data['locales'][locale]
sample_data['name'] = locale_data[name_key]['message']
sample_data['description'] = locale_data[description_key]['message']
except Exception as e:
logging.error(e)
# Revert the sample to the original dict.
sample_data = dict_
return_list.append(sample_data)
else:
return_list.append(dict_)
return return_list
...@@ -282,7 +282,7 @@ span.displayModeWarning { ...@@ -282,7 +282,7 @@ span.displayModeWarning {
#gc-toc { #gc-toc {
margin: 0; margin: 0;
width: 180px; width: 180px;
position: absolute; float: left;
} }
#gc-toc * { #gc-toc * {
......
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
import re import re
import xml.dom.minidom as xml import xml.dom.minidom as xml
from file_system import FileSystem import file_system
from future import Future from future import Future
class SubversionFileSystem(FileSystem): class SubversionFileSystem(file_system.FileSystem):
"""Class to fetch resources from src.chromium.org. """Class to fetch resources from src.chromium.org.
""" """
def __init__(self, fetcher): def __init__(self, fetcher):
...@@ -45,7 +45,7 @@ class _AsyncFetchFuture(object): ...@@ -45,7 +45,7 @@ class _AsyncFetchFuture(object):
elif path.endswith('/'): elif path.endswith('/'):
self._value[path] = self._ListDir(result.content) self._value[path] = self._ListDir(result.content)
else: else:
self._value[path] = result.content self._value[path] = file_system._ProcessFileData(result.content, path)
if self._error is not None: if self._error is not None:
raise self._error raise self._error
return self._value return self._value
......
...@@ -29,7 +29,7 @@ class SubversionFileSystemTest(unittest.TestCase): ...@@ -29,7 +29,7 @@ class SubversionFileSystemTest(unittest.TestCase):
for i in range(7): for i in range(7):
expected.append('file%d.html' % i) expected.append('file%d.html' % i)
self.assertEqual(expected, self.assertEqual(expected,
sorted(self._file_system.Read(['list/']).Get()['list/'])) sorted(self._file_system.ReadSingle('list/')))
def testStat(self): def testStat(self):
# Value is hard-coded into FakeUrlFetcher. # Value is hard-coded into FakeUrlFetcher.
......
...@@ -42,7 +42,7 @@ class TemplateDataSource(object): ...@@ -42,7 +42,7 @@ class TemplateDataSource(object):
api_data_source, api_data_source,
api_list_data_source, api_list_data_source,
intro_data_source, intro_data_source,
samples_data_source, samples_data_source_factory,
cache_builder, cache_builder,
public_template_path, public_template_path,
private_template_path): private_template_path):
...@@ -52,7 +52,7 @@ class TemplateDataSource(object): ...@@ -52,7 +52,7 @@ class TemplateDataSource(object):
self._api_data_source = api_data_source self._api_data_source = api_data_source
self._api_list_data_source = api_list_data_source self._api_list_data_source = api_list_data_source
self._intro_data_source = intro_data_source self._intro_data_source = intro_data_source
self._samples_data_source = samples_data_source self._samples_data_source_factory = samples_data_source_factory
self._cache = cache_builder.build(Handlebar) self._cache = cache_builder.build(Handlebar)
self._public_template_path = public_template_path self._public_template_path = public_template_path
self._private_template_path = private_template_path self._private_template_path = private_template_path
...@@ -65,7 +65,7 @@ class TemplateDataSource(object): ...@@ -65,7 +65,7 @@ class TemplateDataSource(object):
self._api_data_source, self._api_data_source,
self._api_list_data_source, self._api_list_data_source,
self._intro_data_source, self._intro_data_source,
self._samples_data_source, self._samples_data_source_factory,
self._cache, self._cache,
self._public_template_path, self._public_template_path,
self._private_template_path, self._private_template_path,
...@@ -77,7 +77,7 @@ class TemplateDataSource(object): ...@@ -77,7 +77,7 @@ class TemplateDataSource(object):
api_data_source, api_data_source,
api_list_data_source, api_list_data_source,
intro_data_source, intro_data_source,
samples_data_source, samples_data_source_factory,
cache, cache,
public_template_path, public_template_path,
private_template_path, private_template_path,
...@@ -87,7 +87,7 @@ class TemplateDataSource(object): ...@@ -87,7 +87,7 @@ class TemplateDataSource(object):
self._api_data_source = api_data_source self._api_data_source = api_data_source
self._api_list_data_source = api_list_data_source self._api_list_data_source = api_list_data_source
self._intro_data_source = intro_data_source self._intro_data_source = intro_data_source
self._samples_data_source = samples_data_source self._samples_data_source = samples_data_source_factory.Create(request)
self._cache = cache self._cache = cache
self._public_template_path = public_template_path self._public_template_path = public_template_path
self._private_template_path = private_template_path self._private_template_path = private_template_path
......
...@@ -13,16 +13,19 @@ from template_data_source import TemplateDataSource ...@@ -13,16 +13,19 @@ from template_data_source import TemplateDataSource
from third_party.handlebar import Handlebar from third_party.handlebar import Handlebar
class _FakeRequest(object): class _FakeRequest(object):
def __init__(self):
pass pass
class _FakeSamplesDataSource(object):
def Create(self, request):
return {}
class TemplateDataSourceTest(unittest.TestCase): class TemplateDataSourceTest(unittest.TestCase):
def setUp(self): def setUp(self):
self._base_path = os.path.join('test_data', 'template_data_source') self._base_path = os.path.join('test_data', 'template_data_source')
self._fake_api_data_source = {} self._fake_api_data_source = {}
self._fake_api_list_data_source = {} self._fake_api_list_data_source = {}
self._fake_intro_data_source = {} self._fake_intro_data_source = {}
self._fake_samples_data_source = {} self._fake_samples_data_source = _FakeSamplesDataSource()
def _ReadLocalFile(self, filename): def _ReadLocalFile(self, filename):
with open(os.path.join(self._base_path, filename), 'r') as f: with open(os.path.join(self._base_path, filename), 'r') as f:
......
...@@ -37,7 +37,7 @@ relating to the app. ...@@ -37,7 +37,7 @@ relating to the app.
Packaged apps have no traditional chrome: Packaged apps have no traditional chrome:
the omnibox (address bar), tab strip, the omnibox (address bar), tab strip,
and other browser interface elements no longer appear. and other browser interface elements no longer appear.
Like native apps, they dont live within the browser. Like native apps, they don't live within the browser.
When launched, packaged apps can open in windows When launched, packaged apps can open in windows
that look like this (and you can style that look like this (and you can style
your windows in all different ways): your windows in all different ways):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment