Quellcode durchsuchen

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.
Ondrej Grover vor 10 Jahren
Ursprung
Commit
5e07f2dfc6

+ 52 - 0
i18n_subsites/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

+ 1 - 0
i18n_subsites/__init__.py

@@ -0,0 +1 @@
+from .i18n_subsites import *

+ 81 - 0
i18n_subsites/_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'))
+    

+ 138 - 0
i18n_subsites/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)