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.
This commit is contained in:
commit
e4fef3ebdf
52
README.rst
Normal file
52
README.rst
Normal file
@ -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
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .i18n_subsites import *
|
81
_regenerate_context_helpers.py
Normal file
81
_regenerate_context_helpers.py
Normal file
@ -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
i18n_subsites.py
Normal file
138
i18n_subsites.py
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user