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:
		
							
								
								
									
										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) | ||||
		Reference in New Issue
	
	Block a user