commit e4fef3ebdfbe3da4f02d72a91660fd3ed479ad84 Author: Ondrej Grover Date: Fri Jan 31 10:06:52 2014 +0100 new plugin: i18n_subsites This plugin extends the translations functionality by creating i8n-ized sub-sites for the default site. This commit implements the basic generation functionality. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..c58fa6a --- /dev/null +++ b/README.rst @@ -0,0 +1,52 @@ +i18n subsites plugin +=================== + +This plugin extends the translations functionality by creating i8n-ized sub-sites for the default site. +It is therefore redundant with the *\*_LANG_{SAVE_AS,URL}* variables, so it disables them to prevent conflicts. + +What it does +------------ +1. The *\*_LANG_URL* and *\*_LANG_SAVE_AS* variables are set to their normal counterparts (e.g. *ARTICLE_URL*) so they don't conflict with this scheme. +2. While building the site for *DEFAULT_LANG* the translations of pages and articles are not generated, but their relations to the original content is kept via links to them. +3. For each non-default language a "sub-site" with a modified config [#conf]_ is created [#run]_, linking the translations to the originals (if available). The configured language code is appended to the *OUTPUT_PATH* and *SITEURL* of each sub-site. + +If *HIDE_UNTRANSLATED_CONTENT* is True (default), content without a translation for a language is generated as hidden (for pages) or draft (for articles) for the corresponding language sub-site. + +.. [#conf] for each language a config override is given in the *I18N_SUBSITES* dictionary +.. [#run] using a new *PELICAN_CLASS* instance and its ``run`` method, so each subsite could even have a different *PELICAN_CLASS* if specified in *I18N_SUBSITES* conf overrides. + +Setting it up +------------- + +For each extra used language code a language specific variables overrides dictionary must be given (but can be empty) in the *I18N_SUBSITES* dictionary:: + + PLUGINS = ['i18n_subsites', ...] + + # mapping: language_code -> conf_overrides_dict + I18N_SUBSITES = { + 'cz': { + 'SITENAME': 'Hezkej blog', + } + } + +- The language code is the language identifier used in the *lang* metadata. It is appended to *OUTPUT_PATH* and *SITEURL* of each i18n sub-site. +- The i18n-ized config overrides dictionary may specify configuration variable overrides, e.g. a different *LOCALE*, *SITENAME*, *TIMEZONE*, etc. + However, it **must not** override *OUTPUT_PATH* and *SITEURL* as they are modified automatically by appending the language subpath. + Most importantly, a localized [#local]_ theme can be specified in *THEME*. + +.. [#local] It is convenient to add language buttons to your theme in addition to the translations links. + +Usage notes +----------- +- It is **mandatory** to specify *lang* metadata for each article and page as *DEFAULT_LANG* is later changed for each sub-site. +- As with the original translations functionality, *slug* metadata is used to group translations. It is therefore often + convenient to compensate for this by overriding the content url (which defaults to slug) using the *url* and *save_as* metadata. + +Future plans +------------ +- Instead of specifying a different theme for each language, the ``jinja2.ext.i18n`` extension could be used. + This would require some gettext and babel infrastructure. + +Development +----------- +Please file issues, pull requests at https://github.com/smartass101/pelican-plugins diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..7dfbde0 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from .i18n_subsites import * diff --git a/_regenerate_context_helpers.py b/_regenerate_context_helpers.py new file mode 100644 index 0000000..556f448 --- /dev/null +++ b/_regenerate_context_helpers.py @@ -0,0 +1,81 @@ + +import math +import random +from collections import defaultdict +from operator import attrgetter, itemgetter + + +def regenerate_context_articles(generator): + """Helper to regenerate context after modifying articles draft state + + essentially just a copy from pelican.generators.ArticlesGenerator.generate_context + after process_translations up to signal sending + + This has to be kept in sync untill a better solution is found + This is for Pelican version 3.3.0 + """ + # Simulate __init__ for fields that need it + generator.dates = {} + generator.tags = defaultdict(list) + generator.categories = defaultdict(list) + generator.authors = defaultdict(list) + + + # Simulate ArticlesGenerator.generate_context + for article in generator.articles: + # only main articles are listed in categories and tags + # not translations + generator.categories[article.category].append(article) + if hasattr(article, 'tags'): + for tag in article.tags: + generator.tags[tag].append(article) + # ignore blank authors as well as undefined + if hasattr(article, 'author') and article.author.name != '': + generator.authors[article.author].append(article) + + + # sort the articles by date + generator.articles.sort(key=attrgetter('date'), reverse=True) + generator.dates = list(generator.articles) + generator.dates.sort(key=attrgetter('date'), + reverse=generator.context['NEWEST_FIRST_ARCHIVES']) + + # create tag cloud + tag_cloud = defaultdict(int) + for article in generator.articles: + for tag in getattr(article, 'tags', []): + tag_cloud[tag] += 1 + + tag_cloud = sorted(tag_cloud.items(), key=itemgetter(1), reverse=True) + tag_cloud = tag_cloud[:generator.settings.get('TAG_CLOUD_MAX_ITEMS')] + + tags = list(map(itemgetter(1), tag_cloud)) + if tags: + max_count = max(tags) + steps = generator.settings.get('TAG_CLOUD_STEPS') + + # calculate word sizes + generator.tag_cloud = [ + ( + tag, + int(math.floor(steps - (steps - 1) * math.log(count) + / (math.log(max_count)or 1))) + ) + for tag, count in tag_cloud + ] + # put words in chaos + random.shuffle(generator.tag_cloud) + + # and generate the output :) + + # order the categories per name + generator.categories = list(generator.categories.items()) + generator.categories.sort( + reverse=generator.settings['REVERSE_CATEGORY_ORDER']) + + generator.authors = list(generator.authors.items()) + generator.authors.sort() + + generator._update_context(('articles', 'dates', 'tags', 'categories', + 'tag_cloud', 'authors', 'related_posts')) + diff --git a/i18n_subsites.py b/i18n_subsites.py new file mode 100644 index 0000000..155aaf3 --- /dev/null +++ b/i18n_subsites.py @@ -0,0 +1,138 @@ +"""i18n_subsites plugin creates i18n-ized subsites of the default site""" + + + +import os +import six +import logging +from itertools import chain + +from pelican import signals, Pelican +from pelican.contents import Page, Article + +from ._regenerate_context_helpers import regenerate_context_articles + + + +# Global vars +_main_site_generated = False +_main_site_lang = "en" +logger = logging.getLogger(__name__) + + + +def disable_lang_vars(pelican_obj): + """Set lang specific url and save_as vars to the non-lang defaults + + e.g. ARTICLE_LANG_URL = ARTICLE_URL + They would conflict with this plugin otherwise + """ + s = pelican_obj.settings + for content in ['ARTICLE', 'PAGE']: + for meta in ['_URL', '_SAVE_AS']: + s[content + '_LANG' + meta] = s[content + meta] + + + +def create_lang_subsites(pelican_obj): + """For each language create a subsite using the lang-specific config + + for each generated lang append language subpath to SITEURL and OUTPUT_PATH + and set DEFAULT_LANG to the language code to change perception of what is translated + and set DELETE_OUTPUT_DIRECTORY to False to prevent deleting output from previous runs + Then generate the subsite using a PELICAN_CLASS instance and its run method. + """ + global _main_site_generated, _main_site_lang + if _main_site_generated: # make sure this is only called once + return + else: + _main_site_generated = True + + orig_settings = pelican_obj.settings + _main_site_lang = orig_settings['DEFAULT_LANG'] + for lang, overrides in orig_settings.get('I18N_SUBSITES', {}).items(): + settings = orig_settings.copy() + settings.update(overrides) + settings['SITEURL'] = orig_settings['SITEURL'] + '/' + lang + settings['OUTPUT_PATH'] = os.path.join(orig_settings['OUTPUT_PATH'], lang, '') + settings['DEFAULT_LANG'] = lang # to change what is perceived as translations + settings['DELETE_OUTPUT_DIRECTORY'] = False # prevent deletion of previous runs + + cls = settings['PELICAN_CLASS'] + if isinstance(cls, six.string_types): + module, cls_name = cls.rsplit('.', 1) + module = __import__(module) + cls = getattr(module, cls_name) + + pelican_obj = cls(settings) + logger.debug("Generating i18n subsite for lang '{}' using class '{}'".format(lang, str(cls))) + pelican_obj.run() + + + +def move_translations_links(content_object): + """This function points translations links to the sub-sites + + by prepending their location with the language code + or directs an original DEFAULT_LANG translation back to top level site + """ + for translation in content_object.translations: + if translation.lang == _main_site_lang: + # cannot prepend, must take to top level + lang_prepend = '../' + else: + lang_prepend = translation.lang + '/' + translation.override_url = lang_prepend + translation.url + + + +def update_generator_contents(generator, *args): + """Update the contents lists of a generator + + Empty the (hidden_)translation attribute of article and pages generators + to prevent generating the translations as they will be generated in the lang sub-site + and point the content translations links to the sub-sites + + Hide content without a translation for current DEFAULT_LANG + if HIDE_UNTRANSLATED_CONTENT is True + """ + generator.translations = [] + is_pages_gen = hasattr(generator, 'pages') + if is_pages_gen: + generator.hidden_translations = [] + for page in chain(generator.pages, generator.hidden_pages): + move_translations_links(page) + else: # is an article generator + for article in chain(generator.articles, generator.drafts): + move_translations_links(article) + + if not generator.settings.get('HIDE_UNTRANSLATED_CONTENT', True): + return + contents = generator.pages if is_pages_gen else generator.articles + hidden_contents = generator.hidden_pages if is_pages_gen else generator.drafts + default_lang = generator.settings['DEFAULT_LANG'] + for content_object in contents: + if content_object.lang != default_lang: + if isinstance(content_object, Page): + content_object.status = 'hidden' + elif isinstance(content_object, Article): + content_object.status = 'draft' + contents.remove(content_object) + hidden_contents.append(content_object) + if not is_pages_gen: # regenerate categories, tags, etc. for articles + if hasattr(generator, '_generate_context_aggregate'): # if implemented + # Simulate __init__ for fields that need it + generator.dates = {} + generator.tags = defaultdict(list) + generator.categories = defaultdict(list) + generator.authors = defaultdict(list) + generator._generate_context_aggregate() + else: # fallback for Pelican 3.3.0 + regenerate_context_articles(generator) + + +def register(): + signals.initialized.connect(disable_lang_vars) + signals.article_generator_finalized.connect(update_generator_contents) + signals.page_generator_finalized.connect(update_generator_contents) + signals.finalized.connect(create_lang_subsites)