
User experience =============== For users it means we can provide localized web-sites to enrich their overall experiences. Although for the Blender Cloud this doesn't make much sense (since the content is in English), Flamenco and Attract can really benefit from this. New configuration settings ========================== There are two new parameters in config.py: * DEFAULT_LOCALE='en_US' * SUPPORT_ENGLISH=True They are both properly documented in the `config.py` file. Technicall details ================== We are using the 'Accept-Languages' header to match the available translations with the user supported languages. If an extension has a `translations` folder, it's used for translations. However the main application (e.g., Blender Cloud) is the one that determines the supported languages based on its `languages` folder. How to mark strings for translation =================================== See the documentation in README.md. But as an example, 404.pug and pillar/__init__.py::handle_sdk_resource_invalid have marked up strings that will be extracted once you install pillar, or run any of the translations commangs. Remember to **gulp** after you update the template files. How to setup translations ========================= You will need to create translation for the main project, and for each extension that you want to see translated. I added a new entry-point to the installation of Pillar. So all you need is to use the `translations` script to initialize, update and compile your translations. Pending tasks ============= Aside from marking more strings for extraction and start the translation effort it would be interesting to replace the pretty_date routine with momentjs. Acknowledgement =============== Many thanks for Sybren Stüvel for the suggestions and throughout code review. Thanks also to Francesco Siddi for the original documentation and suggesting me to tackle this. And Kudos for Pablo Vazquez for the motivational support and for the upcoming "strings mark up" task force! The core of the implementation is based on Miguel Grinberg i18n chapter of his great 'The Mega Flask Tutorial'. Reviewers: sybren Differential Revision: https://developer.blender.org/D2826
162 lines
5.1 KiB
Python
162 lines
5.1 KiB
Python
"""Pillar extensions support.
|
|
|
|
Each Pillar extension should create a subclass of PillarExtension, which
|
|
can then be registered to the application at app creation time:
|
|
|
|
from pillar_server import PillarServer
|
|
from attract_server import AttractExtension
|
|
|
|
app = PillarServer('.')
|
|
app.load_extension(AttractExtension(), url_prefix='/attract')
|
|
app.process_extensions() # Always process extensions after the last one is loaded.
|
|
|
|
if __name__ == '__main__':
|
|
app.run('::0', 5000)
|
|
|
|
"""
|
|
|
|
import abc
|
|
import inspect
|
|
import pathlib
|
|
import typing
|
|
|
|
import flask
|
|
import pillarsdk
|
|
|
|
|
|
class PillarExtension(object, metaclass=abc.ABCMeta):
|
|
# Set to True when your extension implements the project_settings() method.
|
|
has_project_settings = False
|
|
|
|
# Set to True when your extension implements the context_processor() method.
|
|
has_context_processor = False
|
|
|
|
# List of Celery task modules introduced by this extension.
|
|
celery_task_modules: typing.List[str] = []
|
|
|
|
# Set of user roles used/introduced by this extension.
|
|
user_roles: typing.Set[str] = set()
|
|
user_roles_indexable: typing.Set[str] = set()
|
|
|
|
# User capabilities introduced by this extension. The final set of
|
|
# capabilities is the union of all app-level and extension-level caps.
|
|
user_caps: typing.Mapping[str, typing.FrozenSet] = {}
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def name(self):
|
|
"""The name of this extension.
|
|
|
|
The name determines the path at which Eve exposes the extension's
|
|
resources (/{extension name}/{resource name}), as well as the
|
|
MongoDB collection in which those resources are stored
|
|
({extensions name}.{resource name}).
|
|
|
|
:rtype: unicode
|
|
"""
|
|
|
|
@property
|
|
def icon(self) -> str:
|
|
"""Returns the icon HTML class, for use like i.pi-{{ext.icon}}
|
|
|
|
Defaults to the extension name.
|
|
"""
|
|
return self.name
|
|
|
|
@abc.abstractmethod
|
|
def flask_config(self):
|
|
"""Returns extension-specific defaults for the Flask configuration.
|
|
|
|
Use this to set sensible default values for configuration settings
|
|
introduced by the extension.
|
|
|
|
:rtype: dict
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def blueprints(self):
|
|
"""Returns the list of top-level blueprints for the extension.
|
|
|
|
These blueprints will be mounted at the url prefix given to
|
|
app.load_extension().
|
|
|
|
:rtype: list of flask.Blueprint objects.
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def eve_settings(self):
|
|
"""Returns extensions to the Eve settings.
|
|
|
|
Currently only the DOMAIN key is used to insert new resources into
|
|
Eve's configuration.
|
|
|
|
:rtype: dict
|
|
"""
|
|
|
|
@property
|
|
def template_path(self):
|
|
"""Returns the path where templates for this extension are stored.
|
|
|
|
Note that this path is not connected to any blueprint, so it is up to
|
|
the extension to provide extension-unique subdirectories.
|
|
"""
|
|
return None
|
|
|
|
@property
|
|
def static_path(self):
|
|
"""Returns the path where static files are stored.
|
|
|
|
Registers an endpoint named 'static_<extension name>', to use like:
|
|
`url_for('static_attract', filename='js/somefile.js')`
|
|
|
|
May return None, in which case the extension will not be able to serve
|
|
static files.
|
|
"""
|
|
return None
|
|
|
|
@property
|
|
def translations_path(self) -> typing.Union[pathlib.Path, None]:
|
|
"""Returns the path where the translations for this extension are stored.
|
|
|
|
This is top folder that contains a "translations" sub-folder
|
|
|
|
May return None, in which case English will always be used for this extension.
|
|
"""
|
|
class_filename = pathlib.Path(inspect.getfile(self.__class__))
|
|
|
|
# Pillar extensions instantiate the PillarExtension from a sub-folder in
|
|
# the main project (e.g. //blender_cloud/blender_cloud/__init__.py), but
|
|
# the translations folders is in the main project folder.
|
|
translations_path = class_filename.parents[1] / 'translations'
|
|
|
|
return translations_path if translations_path.is_dir() else None
|
|
|
|
def setup_app(self, app):
|
|
"""Called during app startup, after all extensions have loaded."""
|
|
|
|
def sidebar_links(self, project: pillarsdk.Project) -> str:
|
|
"""Returns the sidebar link(s) for the given projects.
|
|
|
|
:returns: HTML as a string for the sidebar.
|
|
"""
|
|
|
|
return ''
|
|
|
|
def project_settings(self, project: pillarsdk.Project, **template_args: dict) -> flask.Response:
|
|
"""Renders the project settings page for this extension.
|
|
|
|
Set YourExtension.has_project_settings = True and Pillar will call this function.
|
|
|
|
:param project: the project for which to render the settings.
|
|
:param template_args: additional template arguments.
|
|
:returns: a Flask HTTP response
|
|
"""
|
|
|
|
def context_processor(self) -> dict:
|
|
"""Returns a dictionary that gets injected into the Flask Jinja2 namespace.
|
|
|
|
Set has_context_processor to True when your extension implements this method.
|
|
"""
|
|
|
|
return {}
|