Начало
This commit is contained in:
commit
7e8c591b51
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*/__pycache__/
|
||||||
|
*.py[cod]
|
165
i18n_subsites/README.rst
Normal file
165
i18n_subsites/README.rst
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
=======================
|
||||||
|
I18N Sub-sites Plugin
|
||||||
|
=======================
|
||||||
|
|
||||||
|
This plugin extends the translations functionality by creating
|
||||||
|
internationalized sub-sites for the default site.
|
||||||
|
|
||||||
|
This plugin is designed for Pelican 3.4 and later.
|
||||||
|
|
||||||
|
What it does
|
||||||
|
============
|
||||||
|
|
||||||
|
1. When the content of the main site is being generated, the settings
|
||||||
|
are saved and the generation stops when content is ready to be
|
||||||
|
written. While reading source files and generating content objects,
|
||||||
|
the output queue is modified in certain ways:
|
||||||
|
|
||||||
|
- translations that will appear as native in a different (sub-)site
|
||||||
|
will be removed
|
||||||
|
- untranslated articles will be transformed to drafts if
|
||||||
|
``I18N_UNTRANSLATED_ARTICLES`` is ``'hide'`` (default), removed if
|
||||||
|
``'remove'`` or kept as they are if ``'keep'``.
|
||||||
|
- untranslated pages will be transformed into hidden pages if
|
||||||
|
``I18N_UNTRANSLATED_PAGES`` is ``'hide'`` (default), removed if
|
||||||
|
``'remove'`` or kept as they are if ``'keep'``.''
|
||||||
|
- additional content manipulation similar to articles and pages can
|
||||||
|
be specified for custom generators in the ``I18N_GENERATOR_INFO``
|
||||||
|
setting.
|
||||||
|
|
||||||
|
2. For each language specified in the ``I18N_SUBSITES`` dictionary the
|
||||||
|
settings overrides are applied to the settings from the main site
|
||||||
|
and a new sub-site is generated in the same way as with the main
|
||||||
|
site until content is ready to be written.
|
||||||
|
3. When all (sub-)sites are waiting for content writing, all removed
|
||||||
|
contents, translations and static files are interlinked across the
|
||||||
|
(sub-)sites.
|
||||||
|
4. Finally, all the output is written.
|
||||||
|
|
||||||
|
Setting it up
|
||||||
|
=============
|
||||||
|
|
||||||
|
For each extra used language code, a language-specific settings overrides
|
||||||
|
dictionary must be given (but can be empty) in the ``I18N_SUBSITES`` dictionary
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
PLUGINS = ['i18n_subsites', ...]
|
||||||
|
|
||||||
|
# mapping: language_code -> settings_overrides_dict
|
||||||
|
I18N_SUBSITES = {
|
||||||
|
'cz': {
|
||||||
|
'SITENAME': 'Hezkej blog',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
You must also have the following in your pelican configuration
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
JINJA_ENVIRONMENT = {
|
||||||
|
'extensions': ['jinja2.ext.i18n'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Default and special overrides
|
||||||
|
-----------------------------
|
||||||
|
The settings overrides may contain arbitrary settings, however, there
|
||||||
|
are some that are handled in a special way:
|
||||||
|
|
||||||
|
``SITEURL``
|
||||||
|
Any overrides to this setting should ensure that there is some level
|
||||||
|
of hierarchy between all (sub-)sites, because Pelican makes all URLs
|
||||||
|
relative to ``SITEURL`` and the plugin can only cross-link between
|
||||||
|
the sites using this hierarchy. For instance, with the main site
|
||||||
|
``http://example.com`` a sub-site ``http://example.com/de`` will
|
||||||
|
work, but ``http://de.example.com`` will not. If not overridden, the
|
||||||
|
language code (the language identifier used in the ``lang``
|
||||||
|
metadata) is appended to the main ``SITEURL`` for each sub-site.
|
||||||
|
``OUTPUT_PATH``, ``CACHE_PATH``
|
||||||
|
If not overridden, the language code is appended as with ``SITEURL``.
|
||||||
|
Separate cache paths are required as parser results depend on the locale.
|
||||||
|
``STATIC_PATHS``, ``THEME_STATIC_PATHS``
|
||||||
|
If not overridden, they are set to ``[]`` and all links to static
|
||||||
|
files are cross-linked to the main site.
|
||||||
|
``THEME``, ``THEME_STATIC_DIR``
|
||||||
|
If overridden, the logic with ``THEME_STATIC_PATHS`` does not apply.
|
||||||
|
``DEFAULT_LANG``
|
||||||
|
This should not be overridden as the plugin changes it to the
|
||||||
|
language code of each sub-site to change what is perceived as translations.
|
||||||
|
|
||||||
|
Localizing templates
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Most importantly, this plugin can use localized templates for each
|
||||||
|
sub-site. There are two approaches to having the templates localized:
|
||||||
|
|
||||||
|
- You can set a different ``THEME`` override for each language in
|
||||||
|
``I18N_SUBSITES``, e.g. by making a copy of a theme ``my_theme`` to
|
||||||
|
``my_theme_lang`` and then editing the templates in the new
|
||||||
|
localized theme. This approach means you don't have to deal with
|
||||||
|
gettext ``*.po`` files, but it is harder to maintain over time.
|
||||||
|
- You use only one theme and localize the templates using the
|
||||||
|
`jinja2.ext.i18n Jinja2 extension
|
||||||
|
<http://jinja.pocoo.org/docs/templates/#i18n>`_. For a kickstart
|
||||||
|
read this `guide <./localizing_using_jinja2.rst>`_.
|
||||||
|
|
||||||
|
Additional context variables
|
||||||
|
............................
|
||||||
|
|
||||||
|
It may be convenient to add language buttons to your theme in addition
|
||||||
|
to the translation links of articles and pages. These buttons could,
|
||||||
|
for example, point to the ``SITEURL`` of each (sub-)site. For this
|
||||||
|
reason the plugin adds these variables to the template context:
|
||||||
|
|
||||||
|
``main_lang``
|
||||||
|
The language of the main site — the original ``DEFAULT_LANG``
|
||||||
|
``main_siteurl``
|
||||||
|
The ``SITEURL`` of the main site — the original ``SITEURL``
|
||||||
|
``lang_siteurls``
|
||||||
|
An ordered dictionary, mapping all used languages to their
|
||||||
|
``SITEURL``. The ``main_lang`` is the first key with ``main_siteurl``
|
||||||
|
as the value. This dictionary is useful for implementing global
|
||||||
|
language buttons that show the language of the currently viewed
|
||||||
|
(sub-)site too.
|
||||||
|
``extra_siteurls``
|
||||||
|
An ordered dictionary, subset of ``lang_siteurls``, the current
|
||||||
|
``DEFAULT_LANG`` of the rendered (sub-)site is not included, so for
|
||||||
|
each (sub-)site ``set(extra_siteurls) == set(lang_siteurls) -
|
||||||
|
set([DEFAULT_LANG])``. This dictionary is useful for implementing
|
||||||
|
global language buttons that do not show the current language.
|
||||||
|
``relpath_to_site``
|
||||||
|
A function that returns a relative path from the first (sub-)site to
|
||||||
|
the second (sub-)site where the (sub-)sites are identified by the
|
||||||
|
language codes given as two arguments.
|
||||||
|
|
||||||
|
If you don't like the default ordering of the ordered dictionaries,
|
||||||
|
use a Jinja2 filter to alter the ordering.
|
||||||
|
|
||||||
|
All the siteurls above are always absolute even in the case of
|
||||||
|
``RELATIVE_URLS == True`` (it would be to complicated to replicate the
|
||||||
|
Pelican internals for local siteurls), so you may rather use something
|
||||||
|
like ``{{ SITEURL }}/{{ relpath_to_site(DEFAULT_LANG, main_lang }}``
|
||||||
|
to link to the main site.
|
||||||
|
|
||||||
|
This short `howto <./implementing_language_buttons.rst>`_ shows two
|
||||||
|
example implementations of language buttons.
|
||||||
|
|
||||||
|
Usage notes
|
||||||
|
===========
|
||||||
|
- It is **mandatory** to specify ``lang`` metadata for each article
|
||||||
|
and page as ``DEFAULT_LANG`` is later changed for each sub-site, so
|
||||||
|
content without ``lang`` metadata would be rendered in every
|
||||||
|
(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. You could also
|
||||||
|
give articles e.g. ``name`` metadata and use it in ``ARTICLE_URL =
|
||||||
|
'{name}.html'``.
|
||||||
|
|
||||||
|
Development
|
||||||
|
===========
|
||||||
|
|
||||||
|
- A demo and a test site is in the ``gh-pages`` branch and can be seen
|
||||||
|
at http://smartass101.github.io/pelican-plugins/
|
1
i18n_subsites/__init__.py
Normal file
1
i18n_subsites/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .i18n_subsites import *
|
462
i18n_subsites/i18n_subsites.py
Normal file
462
i18n_subsites/i18n_subsites.py
Normal file
@ -0,0 +1,462 @@
|
|||||||
|
"""i18n_subsites plugin creates i18n-ized subsites of the default site
|
||||||
|
|
||||||
|
This plugin is designed for Pelican 3.4 and later
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import six
|
||||||
|
import logging
|
||||||
|
import posixpath
|
||||||
|
|
||||||
|
from copy import copy
|
||||||
|
from itertools import chain
|
||||||
|
from operator import attrgetter
|
||||||
|
try:
|
||||||
|
from collections.abc import OrderedDict
|
||||||
|
except ImportError:
|
||||||
|
from collections import OrderedDict
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from six.moves.urllib.parse import urlparse
|
||||||
|
|
||||||
|
import gettext
|
||||||
|
import locale
|
||||||
|
|
||||||
|
from pelican import signals
|
||||||
|
from pelican.generators import ArticlesGenerator, PagesGenerator
|
||||||
|
from pelican.settings import configure_settings
|
||||||
|
try:
|
||||||
|
from pelican.contents import Draft
|
||||||
|
except ImportError:
|
||||||
|
from pelican.contents import Article as Draft
|
||||||
|
|
||||||
|
|
||||||
|
# Global vars
|
||||||
|
_MAIN_SETTINGS = None # settings dict of the main Pelican instance
|
||||||
|
_MAIN_LANG = None # lang of the main Pelican instance
|
||||||
|
_MAIN_SITEURL = None # siteurl of the main Pelican instance
|
||||||
|
_MAIN_STATIC_FILES = None # list of Static instances the main Pelican instance
|
||||||
|
_SUBSITE_QUEUE = {} # map: lang -> settings overrides
|
||||||
|
_SITE_DB = OrderedDict() # OrderedDict: lang -> siteurl
|
||||||
|
_SITES_RELPATH_DB = {} # map: (lang, base_lang) -> relpath
|
||||||
|
# map: generator -> list of removed contents that need interlinking
|
||||||
|
_GENERATOR_DB = {}
|
||||||
|
_NATIVE_CONTENT_URL_DB = {} # map: source_path -> content in its native lang
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def temporary_locale(temp_locale=None):
|
||||||
|
'''Enable code to run in a context with a temporary locale
|
||||||
|
|
||||||
|
Resets the locale back when exiting context.
|
||||||
|
Can set a temporary locale if provided
|
||||||
|
'''
|
||||||
|
orig_locale = locale.setlocale(locale.LC_ALL)
|
||||||
|
if temp_locale is not None:
|
||||||
|
locale.setlocale(locale.LC_ALL, temp_locale)
|
||||||
|
yield
|
||||||
|
locale.setlocale(locale.LC_ALL, orig_locale)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_dbs(settings):
|
||||||
|
'''Initialize internal DBs using the Pelican settings dict
|
||||||
|
|
||||||
|
This clears the DBs for e.g. autoreload mode to work
|
||||||
|
'''
|
||||||
|
global _MAIN_SETTINGS, _MAIN_SITEURL, _MAIN_LANG, _SUBSITE_QUEUE
|
||||||
|
_MAIN_SETTINGS = settings
|
||||||
|
_MAIN_LANG = settings['DEFAULT_LANG']
|
||||||
|
_MAIN_SITEURL = settings['SITEURL']
|
||||||
|
_SUBSITE_QUEUE = settings.get('I18N_SUBSITES', {}).copy()
|
||||||
|
prepare_site_db_and_overrides()
|
||||||
|
# clear databases in case of autoreload mode
|
||||||
|
_SITES_RELPATH_DB.clear()
|
||||||
|
_NATIVE_CONTENT_URL_DB.clear()
|
||||||
|
_GENERATOR_DB.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_site_db_and_overrides():
|
||||||
|
'''Prepare overrides and create _SITE_DB
|
||||||
|
|
||||||
|
_SITE_DB.keys() need to be ready for filter_translations
|
||||||
|
'''
|
||||||
|
_SITE_DB.clear()
|
||||||
|
_SITE_DB[_MAIN_LANG] = _MAIN_SITEURL
|
||||||
|
# make sure it works for both root-relative and absolute
|
||||||
|
main_siteurl = '/' if _MAIN_SITEURL == '' else _MAIN_SITEURL
|
||||||
|
for lang, overrides in _SUBSITE_QUEUE.items():
|
||||||
|
if 'SITEURL' not in overrides:
|
||||||
|
overrides['SITEURL'] = posixpath.join(main_siteurl, lang)
|
||||||
|
_SITE_DB[lang] = overrides['SITEURL']
|
||||||
|
# default subsite hierarchy
|
||||||
|
if 'OUTPUT_PATH' not in overrides:
|
||||||
|
overrides['OUTPUT_PATH'] = os.path.join(
|
||||||
|
_MAIN_SETTINGS['OUTPUT_PATH'], lang)
|
||||||
|
if 'CACHE_PATH' not in overrides:
|
||||||
|
overrides['CACHE_PATH'] = os.path.join(
|
||||||
|
_MAIN_SETTINGS['CACHE_PATH'], lang)
|
||||||
|
if 'STATIC_PATHS' not in overrides:
|
||||||
|
overrides['STATIC_PATHS'] = []
|
||||||
|
if ('THEME' not in overrides and 'THEME_STATIC_DIR' not in overrides and
|
||||||
|
'THEME_STATIC_PATHS' not in overrides):
|
||||||
|
relpath = relpath_to_site(lang, _MAIN_LANG)
|
||||||
|
overrides['THEME_STATIC_DIR'] = posixpath.join(
|
||||||
|
relpath, _MAIN_SETTINGS['THEME_STATIC_DIR'])
|
||||||
|
overrides['THEME_STATIC_PATHS'] = []
|
||||||
|
# to change what is perceived as translations
|
||||||
|
overrides['DEFAULT_LANG'] = lang
|
||||||
|
|
||||||
|
|
||||||
|
def subscribe_filter_to_signals(settings):
|
||||||
|
'''Subscribe content filter to requested signals'''
|
||||||
|
for sig in settings.get('I18N_FILTER_SIGNALS', []):
|
||||||
|
sig.connect(filter_contents_translations)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_plugin(pelican_obj):
|
||||||
|
'''Initialize plugin variables and Pelican settings'''
|
||||||
|
if _MAIN_SETTINGS is None:
|
||||||
|
initialize_dbs(pelican_obj.settings)
|
||||||
|
subscribe_filter_to_signals(pelican_obj.settings)
|
||||||
|
|
||||||
|
|
||||||
|
def get_site_path(url):
|
||||||
|
'''Get the path component of an url, excludes siteurl
|
||||||
|
|
||||||
|
also normalizes '' to '/' for relpath to work,
|
||||||
|
otherwise it could be interpreted as a relative filesystem path
|
||||||
|
'''
|
||||||
|
path = urlparse(url).path
|
||||||
|
if path == '':
|
||||||
|
path = '/'
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def relpath_to_site(lang, target_lang):
|
||||||
|
'''Get relative path from siteurl of lang to siteurl of base_lang
|
||||||
|
|
||||||
|
the output is cached in _SITES_RELPATH_DB
|
||||||
|
'''
|
||||||
|
path = _SITES_RELPATH_DB.get((lang, target_lang), None)
|
||||||
|
if path is None:
|
||||||
|
siteurl = _SITE_DB.get(lang, _MAIN_SITEURL)
|
||||||
|
target_siteurl = _SITE_DB.get(target_lang, _MAIN_SITEURL)
|
||||||
|
path = posixpath.relpath(get_site_path(target_siteurl),
|
||||||
|
get_site_path(siteurl))
|
||||||
|
_SITES_RELPATH_DB[(lang, target_lang)] = path
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def save_generator(generator):
|
||||||
|
'''Save the generator for later use
|
||||||
|
|
||||||
|
initialize the removed content list
|
||||||
|
'''
|
||||||
|
_GENERATOR_DB[generator] = []
|
||||||
|
|
||||||
|
|
||||||
|
def article2draft(article):
|
||||||
|
'''Transform an Article to Draft'''
|
||||||
|
draft = Draft(article._content, article.metadata, article.settings,
|
||||||
|
article.source_path, article._context)
|
||||||
|
draft.status = 'draft'
|
||||||
|
return draft
|
||||||
|
|
||||||
|
|
||||||
|
def page2hidden_page(page):
|
||||||
|
'''Transform a Page to a hidden Page'''
|
||||||
|
page.status = 'hidden'
|
||||||
|
return page
|
||||||
|
|
||||||
|
|
||||||
|
class GeneratorInspector(object):
|
||||||
|
'''Inspector of generator instances'''
|
||||||
|
|
||||||
|
generators_info = {
|
||||||
|
ArticlesGenerator: {
|
||||||
|
'translations_lists': ['translations', 'drafts_translations'],
|
||||||
|
'contents_lists': [('articles', 'drafts')],
|
||||||
|
'hiding_func': article2draft,
|
||||||
|
'policy': 'I18N_UNTRANSLATED_ARTICLES',
|
||||||
|
},
|
||||||
|
PagesGenerator: {
|
||||||
|
'translations_lists': ['translations', 'hidden_translations'],
|
||||||
|
'contents_lists': [('pages', 'hidden_pages')],
|
||||||
|
'hiding_func': page2hidden_page,
|
||||||
|
'policy': 'I18N_UNTRANSLATED_PAGES',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, generator):
|
||||||
|
'''Identify the best known class of the generator instance
|
||||||
|
|
||||||
|
The class '''
|
||||||
|
self.generator = generator
|
||||||
|
self.generators_info.update(generator.settings.get(
|
||||||
|
'I18N_GENERATORS_INFO', {}))
|
||||||
|
for cls in generator.__class__.__mro__:
|
||||||
|
if cls in self.generators_info:
|
||||||
|
self.info = self.generators_info[cls]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.info = {}
|
||||||
|
|
||||||
|
def translations_lists(self):
|
||||||
|
'''Iterator over lists of content translations'''
|
||||||
|
return (getattr(self.generator, name) for name in
|
||||||
|
self.info.get('translations_lists', []))
|
||||||
|
|
||||||
|
def contents_list_pairs(self):
|
||||||
|
'''Iterator over pairs of normal and hidden contents'''
|
||||||
|
return (tuple(getattr(self.generator, name) for name in names)
|
||||||
|
for names in self.info.get('contents_lists', []))
|
||||||
|
|
||||||
|
def hiding_function(self):
|
||||||
|
'''Function for transforming content to a hidden version'''
|
||||||
|
hiding_func = self.info.get('hiding_func', lambda x: x)
|
||||||
|
return hiding_func
|
||||||
|
|
||||||
|
def untranslated_policy(self, default):
|
||||||
|
'''Get the policy for untranslated content'''
|
||||||
|
return self.generator.settings.get(self.info.get('policy', None),
|
||||||
|
default)
|
||||||
|
|
||||||
|
def all_contents(self):
|
||||||
|
'''Iterator over all contents'''
|
||||||
|
translations_iterator = chain(*self.translations_lists())
|
||||||
|
return chain(translations_iterator,
|
||||||
|
*(pair[i] for pair in self.contents_list_pairs()
|
||||||
|
for i in (0, 1)))
|
||||||
|
|
||||||
|
|
||||||
|
def filter_contents_translations(generator):
|
||||||
|
'''Filter the content and translations lists of a generator
|
||||||
|
|
||||||
|
Filters out
|
||||||
|
1) translations which will be generated in a different site
|
||||||
|
2) content that is not in the language of the currently
|
||||||
|
generated site but in that of a different site, content in a
|
||||||
|
language which has no site is generated always. The filtering
|
||||||
|
method bay be modified by the respective untranslated policy
|
||||||
|
'''
|
||||||
|
inspector = GeneratorInspector(generator)
|
||||||
|
current_lang = generator.settings['DEFAULT_LANG']
|
||||||
|
langs_with_sites = _SITE_DB.keys()
|
||||||
|
removed_contents = _GENERATOR_DB[generator]
|
||||||
|
|
||||||
|
for translations in inspector.translations_lists():
|
||||||
|
for translation in translations[:]: # copy to be able to remove
|
||||||
|
if translation.lang in langs_with_sites:
|
||||||
|
translations.remove(translation)
|
||||||
|
removed_contents.append(translation)
|
||||||
|
|
||||||
|
hiding_func = inspector.hiding_function()
|
||||||
|
untrans_policy = inspector.untranslated_policy(default='hide')
|
||||||
|
for (contents, other_contents) in inspector.contents_list_pairs():
|
||||||
|
for content in other_contents: # save any hidden native content first
|
||||||
|
if content.lang == current_lang: # in native lang
|
||||||
|
# save the native URL attr formatted in the current locale
|
||||||
|
_NATIVE_CONTENT_URL_DB[content.source_path] = content.url
|
||||||
|
for content in contents[:]: # copy for removing in loop
|
||||||
|
if content.lang == current_lang: # in native lang
|
||||||
|
# save the native URL attr formatted in the current locale
|
||||||
|
_NATIVE_CONTENT_URL_DB[content.source_path] = content.url
|
||||||
|
elif content.lang in langs_with_sites and untrans_policy != 'keep':
|
||||||
|
contents.remove(content)
|
||||||
|
if untrans_policy == 'hide':
|
||||||
|
other_contents.append(hiding_func(content))
|
||||||
|
elif untrans_policy == 'remove':
|
||||||
|
removed_contents.append(content)
|
||||||
|
|
||||||
|
|
||||||
|
def install_templates_translations(generator):
|
||||||
|
'''Install gettext translations in the jinja2.Environment
|
||||||
|
|
||||||
|
Only if the 'jinja2.ext.i18n' jinja2 extension is enabled
|
||||||
|
the translations for the current DEFAULT_LANG are installed.
|
||||||
|
'''
|
||||||
|
if 'JINJA_ENVIRONMENT' in generator.settings: # pelican 3.7+
|
||||||
|
jinja_extensions = generator.settings['JINJA_ENVIRONMENT'].get(
|
||||||
|
'extensions', [])
|
||||||
|
else:
|
||||||
|
jinja_extensions = generator.settings['JINJA_EXTENSIONS']
|
||||||
|
|
||||||
|
if 'jinja2.ext.i18n' in jinja_extensions:
|
||||||
|
domain = generator.settings.get('I18N_GETTEXT_DOMAIN', 'messages')
|
||||||
|
localedir = generator.settings.get('I18N_GETTEXT_LOCALEDIR')
|
||||||
|
if localedir is None:
|
||||||
|
localedir = os.path.join(generator.theme, 'translations')
|
||||||
|
current_lang = generator.settings['DEFAULT_LANG']
|
||||||
|
if current_lang == generator.settings.get('I18N_TEMPLATES_LANG',
|
||||||
|
_MAIN_LANG):
|
||||||
|
translations = gettext.NullTranslations()
|
||||||
|
else:
|
||||||
|
langs = [current_lang]
|
||||||
|
try:
|
||||||
|
translations = gettext.translation(domain, localedir, langs)
|
||||||
|
except (IOError, OSError):
|
||||||
|
_LOGGER.error((
|
||||||
|
"Cannot find translations for language '{}' in '{}' with "
|
||||||
|
"domain '{}'. Installing NullTranslations.").format(
|
||||||
|
langs[0], localedir, domain))
|
||||||
|
translations = gettext.NullTranslations()
|
||||||
|
newstyle = generator.settings.get('I18N_GETTEXT_NEWSTYLE', True)
|
||||||
|
generator.env.install_gettext_translations(translations, newstyle)
|
||||||
|
|
||||||
|
|
||||||
|
def add_variables_to_context(generator):
|
||||||
|
'''Adds useful iterable variables to template context'''
|
||||||
|
context = generator.context # minimize attr lookup
|
||||||
|
context['relpath_to_site'] = relpath_to_site
|
||||||
|
context['main_siteurl'] = _MAIN_SITEURL
|
||||||
|
context['main_lang'] = _MAIN_LANG
|
||||||
|
context['lang_siteurls'] = _SITE_DB
|
||||||
|
current_lang = generator.settings['DEFAULT_LANG']
|
||||||
|
extra_siteurls = _SITE_DB.copy()
|
||||||
|
extra_siteurls.pop(current_lang)
|
||||||
|
context['extra_siteurls'] = extra_siteurls
|
||||||
|
|
||||||
|
|
||||||
|
def interlink_translations(content):
|
||||||
|
'''Link content to translations in their main language
|
||||||
|
|
||||||
|
so the URL (including localized month names) of the different subsites
|
||||||
|
will be honored
|
||||||
|
'''
|
||||||
|
lang = content.lang
|
||||||
|
# sort translations by lang
|
||||||
|
content.translations.sort(key=attrgetter('lang'))
|
||||||
|
for translation in content.translations:
|
||||||
|
relpath = relpath_to_site(lang, translation.lang)
|
||||||
|
url = _NATIVE_CONTENT_URL_DB[translation.source_path]
|
||||||
|
translation.override_url = posixpath.join(relpath, url)
|
||||||
|
|
||||||
|
|
||||||
|
def interlink_translated_content(generator):
|
||||||
|
'''Make translations link to the native locations
|
||||||
|
|
||||||
|
for generators that may contain translated content
|
||||||
|
'''
|
||||||
|
inspector = GeneratorInspector(generator)
|
||||||
|
for content in inspector.all_contents():
|
||||||
|
interlink_translations(content)
|
||||||
|
|
||||||
|
|
||||||
|
def interlink_removed_content(generator):
|
||||||
|
'''For all contents removed from generation queue update interlinks
|
||||||
|
|
||||||
|
link to the native location
|
||||||
|
'''
|
||||||
|
current_lang = generator.settings['DEFAULT_LANG']
|
||||||
|
for content in _GENERATOR_DB[generator]:
|
||||||
|
url = _NATIVE_CONTENT_URL_DB[content.source_path]
|
||||||
|
relpath = relpath_to_site(current_lang, content.lang)
|
||||||
|
content.override_url = posixpath.join(relpath, url)
|
||||||
|
|
||||||
|
|
||||||
|
def interlink_static_files(generator):
|
||||||
|
'''Add links to static files in the main site if necessary'''
|
||||||
|
if generator.settings['STATIC_PATHS'] != []:
|
||||||
|
return # customized STATIC_PATHS
|
||||||
|
try: # minimize attr lookup
|
||||||
|
static_content = generator.context['static_content']
|
||||||
|
except KeyError:
|
||||||
|
static_content = generator.context['filenames']
|
||||||
|
relpath = relpath_to_site(generator.settings['DEFAULT_LANG'], _MAIN_LANG)
|
||||||
|
for staticfile in _MAIN_STATIC_FILES:
|
||||||
|
if staticfile.get_relative_source_path() not in static_content:
|
||||||
|
staticfile = copy(staticfile) # prevent override in main site
|
||||||
|
staticfile.override_url = posixpath.join(relpath, staticfile.url)
|
||||||
|
try:
|
||||||
|
generator.add_source_path(staticfile, static=True)
|
||||||
|
except TypeError:
|
||||||
|
generator.add_source_path(staticfile)
|
||||||
|
|
||||||
|
|
||||||
|
def save_main_static_files(static_generator):
|
||||||
|
'''Save the static files generated for the main site'''
|
||||||
|
global _MAIN_STATIC_FILES
|
||||||
|
# test just for current lang as settings change in autoreload mode
|
||||||
|
if static_generator.settings['DEFAULT_LANG'] == _MAIN_LANG:
|
||||||
|
_MAIN_STATIC_FILES = static_generator.staticfiles
|
||||||
|
|
||||||
|
|
||||||
|
def update_generators():
|
||||||
|
'''Update the context of all generators
|
||||||
|
|
||||||
|
Ads useful variables and translations into the template context
|
||||||
|
and interlink translations
|
||||||
|
'''
|
||||||
|
for generator in _GENERATOR_DB.keys():
|
||||||
|
install_templates_translations(generator)
|
||||||
|
add_variables_to_context(generator)
|
||||||
|
interlink_static_files(generator)
|
||||||
|
interlink_removed_content(generator)
|
||||||
|
interlink_translated_content(generator)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pelican_cls(settings):
|
||||||
|
'''Get the Pelican class requested in settings'''
|
||||||
|
cls = settings['PELICAN_CLASS']
|
||||||
|
if isinstance(cls, six.string_types):
|
||||||
|
module, cls_name = cls.rsplit('.', 1)
|
||||||
|
module = __import__(module)
|
||||||
|
cls = getattr(module, cls_name)
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
def create_next_subsite(pelican_obj):
|
||||||
|
'''Create the next subsite using the lang-specific config
|
||||||
|
|
||||||
|
If there are no more subsites in the generation queue, update all
|
||||||
|
the generators (interlink translations and removed content, add
|
||||||
|
variables and translations to template context). Otherwise get the
|
||||||
|
language and overrides for next the subsite in the queue and apply
|
||||||
|
overrides. Then generate the subsite using a PELICAN_CLASS
|
||||||
|
instance and its run method. Finally, restore the previous locale.
|
||||||
|
'''
|
||||||
|
global _MAIN_SETTINGS
|
||||||
|
if len(_SUBSITE_QUEUE) == 0:
|
||||||
|
_LOGGER.debug(
|
||||||
|
'i18n: Updating cross-site links and context of all generators.')
|
||||||
|
update_generators()
|
||||||
|
_MAIN_SETTINGS = None # to initialize next time
|
||||||
|
else:
|
||||||
|
with temporary_locale():
|
||||||
|
settings = _MAIN_SETTINGS.copy()
|
||||||
|
lang, overrides = _SUBSITE_QUEUE.popitem()
|
||||||
|
settings.update(overrides)
|
||||||
|
settings = configure_settings(settings) # to set LOCALE, etc.
|
||||||
|
cls = get_pelican_cls(settings)
|
||||||
|
|
||||||
|
new_pelican_obj = cls(settings)
|
||||||
|
_LOGGER.debug(("Generating i18n subsite for language '{}' "
|
||||||
|
"using class {}").format(lang, cls))
|
||||||
|
new_pelican_obj.run()
|
||||||
|
|
||||||
|
|
||||||
|
# map: signal name -> function name
|
||||||
|
_SIGNAL_HANDLERS_DB = {
|
||||||
|
'get_generators': initialize_plugin,
|
||||||
|
'article_generator_pretaxonomy': filter_contents_translations,
|
||||||
|
'page_generator_finalized': filter_contents_translations,
|
||||||
|
'get_writer': create_next_subsite,
|
||||||
|
'static_generator_finalized': save_main_static_files,
|
||||||
|
'generator_init': save_generator,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
'''Register the plugin only if required signals are available'''
|
||||||
|
for sig_name in _SIGNAL_HANDLERS_DB.keys():
|
||||||
|
if not hasattr(signals, sig_name):
|
||||||
|
_LOGGER.error((
|
||||||
|
'The i18n_subsites plugin requires the {} '
|
||||||
|
'signal available for sure in Pelican 3.4.0 and later, '
|
||||||
|
'plugin will not be used.').format(sig_name))
|
||||||
|
return
|
||||||
|
|
||||||
|
for sig_name, handler in _SIGNAL_HANDLERS_DB.items():
|
||||||
|
sig = getattr(signals, sig_name)
|
||||||
|
sig.connect(handler)
|
128
i18n_subsites/implementing_language_buttons.rst
Normal file
128
i18n_subsites/implementing_language_buttons.rst
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
-----------------------------
|
||||||
|
Implementing language buttons
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Each article with translations has translations links, but that's the
|
||||||
|
only way to switch between language subsites.
|
||||||
|
|
||||||
|
For this reason it is convenient to add language buttons to the top
|
||||||
|
menu bar to make it simple to switch between the language subsites on
|
||||||
|
all pages.
|
||||||
|
|
||||||
|
Example designs
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Language buttons showing other available languages
|
||||||
|
..................................................
|
||||||
|
|
||||||
|
The ``extra_siteurls`` dictionary is a mapping of all other (not the
|
||||||
|
``DEFAULT_LANG`` of the current (sub-)site) languages to the
|
||||||
|
``SITEURL`` of the respective (sub-)sites
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
<!-- SNIP -->
|
||||||
|
<nav><ul>
|
||||||
|
{% if extra_siteurls %}
|
||||||
|
{% for lang, url in extra_siteurls.items() %}
|
||||||
|
<li><a href="{{ url }}/">{{ lang }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
<!-- separator -->
|
||||||
|
<li style="background-color: white; padding: 5px;"> </li>
|
||||||
|
{% endif %}
|
||||||
|
{% for title, link in MENUITEMS %}
|
||||||
|
<!-- SNIP -->
|
||||||
|
|
||||||
|
Notice the slash ``/`` after ``{{ url }}``, this makes sure it works
|
||||||
|
with local development when ``SITEURL == ''``.
|
||||||
|
|
||||||
|
Language buttons showing all available languages, current is active
|
||||||
|
...................................................................
|
||||||
|
|
||||||
|
The ``lang_subsites`` dictionary is a mapping of all languages to the
|
||||||
|
``SITEURL`` of the respective (sub-)sites. This template sets the
|
||||||
|
language of the current (sub-)site as active.
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
<!-- SNIP -->
|
||||||
|
<nav><ul>
|
||||||
|
{% if lang_siteurls %}
|
||||||
|
{% for lang, url in lang_siteurls.items() %}
|
||||||
|
<li{% if lang == DEFAULT_LANG %} class="active"{% endif %}><a href="{{ url }}/">{{ lang }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
<!-- separator -->
|
||||||
|
<li style="background-color: white; padding: 5px;"> </li>
|
||||||
|
{% endif %}
|
||||||
|
{% for title, link in MENUITEMS %}
|
||||||
|
<!-- SNIP -->
|
||||||
|
|
||||||
|
|
||||||
|
Tips and tricks
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Showing more than language codes
|
||||||
|
................................
|
||||||
|
|
||||||
|
If you want the language buttons to show e.g. the names of the
|
||||||
|
languages or flags [#flags]_, not just the language codes, you can use
|
||||||
|
a jinja filter to translate the language codes
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
languages_lookup = {
|
||||||
|
'en': 'English',
|
||||||
|
'cz': 'Čeština',
|
||||||
|
}
|
||||||
|
|
||||||
|
def lookup_lang_name(lang_code):
|
||||||
|
return languages_lookup[lang_code]
|
||||||
|
|
||||||
|
JINJA_FILTERS = {
|
||||||
|
...
|
||||||
|
'lookup_lang_name': lookup_lang_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
And then the link content becomes
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
<!-- SNIP -->
|
||||||
|
{% for lang, siteurl in lang_siteurls.items() %}
|
||||||
|
<li{% if lang == DEFAULT_LANG %} class="active"{% endif %}><a href="{{ siteurl }}/">{{ lang | lookup_lang_name }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
<!-- SNIP -->
|
||||||
|
|
||||||
|
|
||||||
|
Changing the order of language buttons
|
||||||
|
......................................
|
||||||
|
|
||||||
|
Because ``lang_siteurls`` and ``extra_siteurls`` are instances of
|
||||||
|
``OrderedDict`` with ``main_lang`` being always the first key, you can
|
||||||
|
change the order through a jinja filter.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def my_ordered_items(ordered_dict):
|
||||||
|
items = list(ordered_dict.items())
|
||||||
|
# swap first and last using tuple unpacking
|
||||||
|
items[0], items[-1] = items[-1], items[0]
|
||||||
|
return items
|
||||||
|
|
||||||
|
JINJA_FILTERS = {
|
||||||
|
...
|
||||||
|
'my_ordered_items': my_ordered_items,
|
||||||
|
}
|
||||||
|
|
||||||
|
And then the ``for`` loop line in the template becomes
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
<!-- SNIP -->
|
||||||
|
{% for lang, siteurl in lang_siteurls | my_ordered_items %}
|
||||||
|
<!-- SNIP -->
|
||||||
|
|
||||||
|
|
||||||
|
.. [#flags] Although it may look nice, `w3 discourages it
|
||||||
|
<http://www.w3.org/TR/i18n-html-tech-lang/#ri20040808.173208643>`_.
|
202
i18n_subsites/localizing_using_jinja2.rst
Normal file
202
i18n_subsites/localizing_using_jinja2.rst
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
-----------------------------
|
||||||
|
Localizing themes with Jinja2
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
1. Localize templates
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
To enable the |ext| extension in your templates, you must add it to
|
||||||
|
``JINJA_ENVIRONMENT`` in your Pelican configuration
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
JINJA_ENVIRONMENT = {
|
||||||
|
'extensions': ['jinja2.ext.i18n', ...]
|
||||||
|
}
|
||||||
|
|
||||||
|
Then follow the `Jinja2 templating documentation for the I18N plugin
|
||||||
|
<http://jinja.pocoo.org/docs/templates/#i18n>`_ to make your templates
|
||||||
|
localizable. This usually means surrounding strings with the ``{%
|
||||||
|
trans %}`` directive or using ``gettext()`` in expressions
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% trans %}translatable content{% endtrans %}
|
||||||
|
{{ gettext('a translatable string') }}
|
||||||
|
|
||||||
|
For pluralization support, etc. consult the documentation.
|
||||||
|
|
||||||
|
To enable `newstyle gettext calls
|
||||||
|
<http://jinja.pocoo.org/docs/extensions/#newstyle-gettext>`_ the
|
||||||
|
``I18N_GETTEXT_NEWSTYLE`` config variable must be set to ``True``
|
||||||
|
(default).
|
||||||
|
|
||||||
|
.. |ext| replace:: ``jinja2.ext.i18n``
|
||||||
|
|
||||||
|
2. Specify translations location
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
The |ext| extension uses the `Python gettext library
|
||||||
|
<http://docs.python.org/library/gettext.html>`_ for translating
|
||||||
|
strings.
|
||||||
|
|
||||||
|
In your Pelican config you can give the path in which to look for
|
||||||
|
translations in the ``I18N_GETTEXT_LOCALEDIR`` variable. If not given,
|
||||||
|
it is assumed to be the ``translations`` subfolder in the top folder
|
||||||
|
of the theme specified by ``THEME``.
|
||||||
|
|
||||||
|
The domain of the translations (the name of each translation file is
|
||||||
|
``domain.mo``) is controlled by the ``I18N_GETTEXT_DOMAIN`` config
|
||||||
|
variable (defaults to ``messages``).
|
||||||
|
|
||||||
|
Example
|
||||||
|
.......
|
||||||
|
|
||||||
|
With the following in your Pelican settings file
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
I18N_GETTEXT_LOCALEDIR = 'some/path/'
|
||||||
|
I18N_GETTEXT_DOMAIN = 'my_domain'
|
||||||
|
|
||||||
|
the translation for language 'cz' will be expected to be in
|
||||||
|
``some/path/cz/LC_MESSAGES/my_domain.mo``
|
||||||
|
|
||||||
|
|
||||||
|
3. Extract translatable strings and translate them
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
There are many ways to extract translatable strings and create
|
||||||
|
``gettext`` compatible translations. You can create the ``*.po`` and
|
||||||
|
``*.mo`` message catalog files yourself, or you can use some helper
|
||||||
|
tool as described in `the Python gettext library tutorial
|
||||||
|
<http://docs.python.org/library/gettext.html#internationalizing-your-programs-and-modules>`_.
|
||||||
|
|
||||||
|
You of course don't need to provide a translation for the language in
|
||||||
|
which the templates are written which is assumed to be the original
|
||||||
|
``DEFAULT_LANG``. This can be overridden in the
|
||||||
|
``I18N_TEMPLATES_LANG`` variable.
|
||||||
|
|
||||||
|
Recommended tool: babel
|
||||||
|
.......................
|
||||||
|
|
||||||
|
`Babel <http://babel.pocoo.org/>`_ makes it easy to extract
|
||||||
|
translatable strings from the localized Jinja2 templates and assists
|
||||||
|
with creating translations as documented in this `Jinja2-Babel
|
||||||
|
tutorial
|
||||||
|
<http://pythonhosted.org/Flask-Babel/#translating-applications>`_
|
||||||
|
[#flask]_ on which the following is based.
|
||||||
|
|
||||||
|
1. Add babel mapping
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Let's assume that you are localizing a theme in ``themes/my_theme/``
|
||||||
|
and that you use the default settings, i.e. the default domain
|
||||||
|
``messages`` and will put the translations in the ``translations``
|
||||||
|
subdirectory of the theme directory as
|
||||||
|
``themes/my_theme/translations/``.
|
||||||
|
|
||||||
|
It is up to you where to store babel mappings and translation files
|
||||||
|
templates (``*.pot``), but a convenient place is to put them in
|
||||||
|
``themes/my_theme/`` and work in that directory. From now on let's
|
||||||
|
assume that it will be our current working directory (CWD).
|
||||||
|
|
||||||
|
To tell babel to extract translatable strings from the templates
|
||||||
|
create a mapping file ``babel.cfg`` with the following line
|
||||||
|
|
||||||
|
.. code-block:: cfg
|
||||||
|
|
||||||
|
[jinja2: templates/**.html]
|
||||||
|
|
||||||
|
|
||||||
|
2. Extract translatable strings from templates
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Run the following command to create a ``messages.pot`` message catalog
|
||||||
|
template file from extracted translatable strings
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pybabel extract --mapping babel.cfg --output messages.pot ./
|
||||||
|
|
||||||
|
|
||||||
|
3. Initialize message catalogs
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
If you want to translate the template to language ``lang``, run the
|
||||||
|
following command to create a message catalog
|
||||||
|
``translations/lang/LC_MESSAGES/messages.po`` using the template
|
||||||
|
``messages.pot``
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pybabel init --input-file messages.pot --output-dir translations/ --locale lang --domain messages
|
||||||
|
|
||||||
|
babel expects ``lang`` to be a valid locale identifier, so if e.g. you
|
||||||
|
are translating for language ``cz`` but the corresponding locale is
|
||||||
|
``cs``, you have to use the locale identifier. Nevertheless, the
|
||||||
|
gettext infrastructure should later correctly find the locale for a
|
||||||
|
given language.
|
||||||
|
|
||||||
|
4. Fill the message catalogs
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The message catalog files format is quite intuitive, it is fully
|
||||||
|
documented in the `GNU gettext manual
|
||||||
|
<http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files>`_. Essentially,
|
||||||
|
you fill in the ``msgstr`` strings
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: po
|
||||||
|
|
||||||
|
msgid "just a simple string"
|
||||||
|
msgstr "jenom jednoduchý řetězec"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"some multiline string"
|
||||||
|
"looks like this"
|
||||||
|
msgstr ""
|
||||||
|
"nějaký více řádkový řetězec"
|
||||||
|
"vypadá takto"
|
||||||
|
|
||||||
|
You might also want to remove ``#,fuzzy`` flags once the translation
|
||||||
|
is complete and reviewed to show that it can be compiled.
|
||||||
|
|
||||||
|
5. Compile the message catalogs
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The message catalogs must be compiled into binary format using this
|
||||||
|
command
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pybabel compile --directory translations/ --domain messages
|
||||||
|
|
||||||
|
This command might complain about "fuzzy" translations, which means
|
||||||
|
you should review the translations and once done, remove the fuzzy
|
||||||
|
flag line.
|
||||||
|
|
||||||
|
(6.) Update the catalogs when templates change
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
If you add any translatable patterns into your templates, you have to
|
||||||
|
update your message catalogs too. First you extract a new message
|
||||||
|
catalog template as described in the 2. step. Then you run the
|
||||||
|
following command [#pybabel_error]_
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pybabel update --input-file messages.pot --output-dir translations/ --domain messages
|
||||||
|
|
||||||
|
This will merge the new patterns with the old ones. Once you review
|
||||||
|
and fill them, you have to recompile them as described in the 5. step.
|
||||||
|
|
||||||
|
.. [#flask] Although the tutorial is focused on Flask-based web
|
||||||
|
applications, the linked translation tutorial is not
|
||||||
|
Flask-specific.
|
||||||
|
.. [#pybabel_error] If you get an error ``TypeError: must be str, not
|
||||||
|
bytes`` with Python 3.3, it is likely you are
|
||||||
|
suffering from this `bug
|
||||||
|
<https://github.com/mitsuhiko/flask-babel/issues/43>`_.
|
||||||
|
Until the fix is released, you can use babel with
|
||||||
|
Python 2.7.
|
0
i18n_subsites/test_data/content/images/img.png
Normal file
0
i18n_subsites/test_data/content/images/img.png
Normal file
7
i18n_subsites/test_data/content/pages/hidden-page-cz.rst
Normal file
7
i18n_subsites/test_data/content/pages/hidden-page-cz.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
404 stránka
|
||||||
|
===========
|
||||||
|
:slug: 404
|
||||||
|
:lang: cz
|
||||||
|
:status: hidden
|
||||||
|
|
||||||
|
Jednoduchá 404 stránka.
|
7
i18n_subsites/test_data/content/pages/hidden-page-de.rst
Normal file
7
i18n_subsites/test_data/content/pages/hidden-page-de.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Eine 404 Seite
|
||||||
|
==============
|
||||||
|
:slug: 404
|
||||||
|
:lang: de
|
||||||
|
:status: hidden
|
||||||
|
|
||||||
|
Eine einfache 404 Seite.
|
7
i18n_subsites/test_data/content/pages/hidden-page-en.rst
Normal file
7
i18n_subsites/test_data/content/pages/hidden-page-en.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
A 404 page
|
||||||
|
==========
|
||||||
|
:slug: 404
|
||||||
|
:lang: en
|
||||||
|
:status: hidden
|
||||||
|
|
||||||
|
A simple 404 page.
|
@ -0,0 +1,5 @@
|
|||||||
|
Untranslated page
|
||||||
|
=================
|
||||||
|
:lang: en
|
||||||
|
|
||||||
|
This page has no translation.
|
@ -0,0 +1,8 @@
|
|||||||
|
Přeložený článek
|
||||||
|
================
|
||||||
|
:slug: translated-article
|
||||||
|
:lang: cz
|
||||||
|
:date: 2014-09-15
|
||||||
|
|
||||||
|
Jednoduchý článek s překlady.
|
||||||
|
Zde je odkaz na `nějaký obrázek <{filename}/images/img.png>`_.
|
@ -0,0 +1,8 @@
|
|||||||
|
Ein übersetzter Artikel
|
||||||
|
=======================
|
||||||
|
:slug: translated-article
|
||||||
|
:lang: de
|
||||||
|
:date: 2014-09-14
|
||||||
|
|
||||||
|
Ein einfacher Artikel mit einer Übersetzung.
|
||||||
|
Hier ist ein Link zur `einigem Bild <{filename}/images/img.png>`_.
|
@ -0,0 +1,8 @@
|
|||||||
|
A translated article
|
||||||
|
====================
|
||||||
|
:slug: translated-article
|
||||||
|
:lang: en
|
||||||
|
:date: 2014-09-13
|
||||||
|
|
||||||
|
A simple article with a translation.
|
||||||
|
Here is a link to `some image <{filename}/images/img.png>`_.
|
@ -0,0 +1,9 @@
|
|||||||
|
An untranslated article
|
||||||
|
=======================
|
||||||
|
:date: 2014-07-14
|
||||||
|
:lang: en
|
||||||
|
|
||||||
|
An article without a translation.
|
||||||
|
Here is a link to an `untranslated page`_
|
||||||
|
|
||||||
|
.. _`untranslated page`: {filename}/pages/untranslated-page.rst
|
2
i18n_subsites/test_data/localized_theme/babel.cfg
Normal file
2
i18n_subsites/test_data/localized_theme/babel.cfg
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[jinja2: templates/**.html]
|
||||||
|
|
23
i18n_subsites/test_data/localized_theme/messages.pot
Normal file
23
i18n_subsites/test_data/localized_theme/messages.pot
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Translations template for PROJECT.
|
||||||
|
# Copyright (C) 2014 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2014-07-13 12:25+0200\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 1.3\n"
|
||||||
|
|
||||||
|
#: templates/base.html:3
|
||||||
|
msgid "Welcome to our"
|
||||||
|
msgstr ""
|
||||||
|
|
@ -0,0 +1,7 @@
|
|||||||
|
{% extends "!simple/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{% trans %}Welcome to our{% endtrans %} {{ SITENAME }}{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/style.css" />
|
||||||
|
{% endblock %}
|
Binary file not shown.
@ -0,0 +1,23 @@
|
|||||||
|
# German translations for PROJECT.
|
||||||
|
# Copyright (C) 2014 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2014-07-13 12:25+0200\n"
|
||||||
|
"PO-Revision-Date: 2014-07-13 12:26+0200\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: de <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 1.3\n"
|
||||||
|
|
||||||
|
#: templates/base.html:3
|
||||||
|
msgid "Welcome to our"
|
||||||
|
msgstr "Willkommen Sie zur unserer"
|
||||||
|
|
56
i18n_subsites/test_data/output/an-untranslated-article.html
Normal file
56
i18n_subsites/test_data/output/an-untranslated-article.html
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Testing site - An untranslated article</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testing site Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/theme/style.css" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/">Testing site</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li><a href="http://example.com/test/pages/untranslated-page.html">Untranslated page</a></li>
|
||||||
|
<li class="active"><a href="http://example.com/test/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<section id="content" class="body">
|
||||||
|
<header>
|
||||||
|
<h2 class="entry-title">
|
||||||
|
<a href="http://example.com/test/an-untranslated-article.html" rel="bookmark"
|
||||||
|
title="Permalink to An untranslated article">An untranslated article</a></h2>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
<footer class="post-info">
|
||||||
|
<time class="published" datetime="2014-07-14T00:00:00+00:00">
|
||||||
|
Mon 14 July 2014
|
||||||
|
</time>
|
||||||
|
<address class="vcard author">
|
||||||
|
By <a class="url fn" href="http://example.com/test/author/the-tester.html">The Tester</a>
|
||||||
|
</address>
|
||||||
|
<div class="category">
|
||||||
|
Category: <a href="http://example.com/test/category/misc.html">misc</a>
|
||||||
|
</div>
|
||||||
|
</footer><!-- /.post-info -->
|
||||||
|
<div class="entry-content">
|
||||||
|
<p>An article without a translation.
|
||||||
|
Here is a link to an <a class="reference external" href="http://example.com/test/pages/untranslated-page.html">untranslated page</a></p>
|
||||||
|
|
||||||
|
</div><!-- /.entry-content -->
|
||||||
|
</section>
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Testovací stránka - An untranslated article</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testovací stránka Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/cz/../theme/style.css" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/cz/">Testovací stránka</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li class="active"><a href="http://example.com/test/cz/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<section id="content" class="body">
|
||||||
|
<header>
|
||||||
|
<h2 class="entry-title">
|
||||||
|
<a href="http://example.com/test/cz/an-untranslated-article-en.html" rel="bookmark"
|
||||||
|
title="Permalink to An untranslated article">An untranslated article</a></h2>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
<footer class="post-info">
|
||||||
|
<time class="published" datetime="2014-07-14T00:00:00+00:00">
|
||||||
|
Mon 14 July 2014
|
||||||
|
</time>
|
||||||
|
<address class="vcard author">
|
||||||
|
By <a class="url fn" href="http://example.com/test/cz/author/test-testovic.html">Test Testovič</a>
|
||||||
|
</address>
|
||||||
|
<div class="category">
|
||||||
|
Category: <a href="http://example.com/test/cz/category/misc.html">misc</a>
|
||||||
|
</div>
|
||||||
|
</footer><!-- /.post-info -->
|
||||||
|
<div class="entry-content">
|
||||||
|
<p>An article without a translation.
|
||||||
|
Here is a link to an <a class="reference external" href="http://example.com/test/pages/untranslated-page.html">untranslated page</a></p>
|
||||||
|
|
||||||
|
</div><!-- /.entry-content -->
|
||||||
|
</section>
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
10
i18n_subsites/test_data/output/cz/feeds_all.atom.xml
Normal file
10
i18n_subsites/test_data/output/cz/feeds_all.atom.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom"><title>Testovací stránka</title><link href="http://example.com/test/cz/" rel="alternate"></link><link href="http://example.com/test/feeds_all.atom.xml" rel="self"></link><id>http://example.com/test/cz/</id><updated>2014-09-15T00:00:00+00:00</updated><entry><title>Přeložený článek</title><link href="http://example.com/test/cz/translated-article.html" rel="alternate"></link><published>2014-09-15T00:00:00+00:00</published><updated>2014-09-15T00:00:00+00:00</updated><author><name>Test Testovič</name></author><id>tag:example.com,2014-09-15:/test/cz/translated-article.html</id><content type="html"><p>Jednoduchý článek s překlady.
|
||||||
|
Zde je odkaz na <a class="reference external" href="http://example.com/test/images/img.png">nějaký obrázek</a>.</p>
|
||||||
|
</content><category term="misc"></category></entry><entry><title>Ein übersetzter Artikel</title><link href="http://example.com/test/de/translated-article.html" rel="alternate"></link><published>2014-09-14T00:00:00+00:00</published><updated>2014-09-14T00:00:00+00:00</updated><author><name>Test Testovič</name></author><id>tag:example.com,2014-09-14:/test/de/translated-article.html</id><content type="html"><p>Ein einfacher Artikel mit einer Übersetzung.
|
||||||
|
Hier ist ein Link zur <a class="reference external" href="http://example.com/test/images/img.png">einigem Bild</a>.</p>
|
||||||
|
</content><category term="misc"></category></entry><entry><title>A translated article</title><link href="http://example.com/test/translated-article.html" rel="alternate"></link><published>2014-09-13T00:00:00+00:00</published><updated>2014-09-13T00:00:00+00:00</updated><author><name>Test Testovič</name></author><id>tag:example.com,2014-09-13:/test/translated-article.html</id><content type="html"><p>A simple article with a translation.
|
||||||
|
Here is a link to <a class="reference external" href="http://example.com/test/images/img.png">some image</a>.</p>
|
||||||
|
</content><category term="misc"></category></entry><entry><title>An untranslated article</title><link href="http://example.com/test/cz/an-untranslated-article-en.html" rel="alternate"></link><published>2014-07-14T00:00:00+00:00</published><updated>2014-07-14T00:00:00+00:00</updated><author><name>Test Testovič</name></author><id>tag:example.com,2014-07-14:/test/cz/an-untranslated-article-en.html</id><content type="html"><p>An article without a translation.
|
||||||
|
Here is a link to an <a class="reference external" href="http://example.com/test/pages/untranslated-page.html">untranslated page</a></p>
|
||||||
|
</content><category term="misc"></category></entry></feed>
|
56
i18n_subsites/test_data/output/cz/index.html
Normal file
56
i18n_subsites/test_data/output/cz/index.html
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="cz">
|
||||||
|
<head>
|
||||||
|
<title>Welcome to our Testovací stránka</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testovací stránka Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/cz/../theme/style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/cz/">Testovací stránka</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li><a href="http://example.com/test/cz/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<section id="content">
|
||||||
|
<h2>All articles</h2>
|
||||||
|
|
||||||
|
<ol id="post-list">
|
||||||
|
<li><article class="hentry">
|
||||||
|
<header> <h2 class="entry-title"><a href="http://example.com/test/cz/translated-article.html" rel="bookmark" title="Permalink to Přeložený článek">Přeložený článek</a></h2> </header>
|
||||||
|
<footer class="post-info">
|
||||||
|
<time class="published" datetime="2014-09-15T00:00:00+00:00"> Mon 15 September 2014 </time>
|
||||||
|
<address class="vcard author">By
|
||||||
|
<a class="url fn" href="http://example.com/test/cz/author/test-testovic.html">Test Testovič</a>
|
||||||
|
</address>
|
||||||
|
</footer><!-- /.post-info -->
|
||||||
|
<div class="entry-content"> <p>Jednoduchý článek s překlady.
|
||||||
|
Zde je odkaz na <a class="reference external" href="http://example.com/test/images/img.png">nějaký obrázek</a>.</p>
|
||||||
|
</div><!-- /.entry-content -->
|
||||||
|
</article></li>
|
||||||
|
<li><article class="hentry">
|
||||||
|
<header> <h2 class="entry-title"><a href="http://example.com/test/cz/an-untranslated-article-en.html" rel="bookmark" title="Permalink to An untranslated article">An untranslated article</a></h2> </header>
|
||||||
|
<footer class="post-info">
|
||||||
|
<time class="published" datetime="2014-07-14T00:00:00+00:00"> Mon 14 July 2014 </time>
|
||||||
|
<address class="vcard author">By
|
||||||
|
<a class="url fn" href="http://example.com/test/cz/author/test-testovic.html">Test Testovič</a>
|
||||||
|
</address>
|
||||||
|
</footer><!-- /.post-info -->
|
||||||
|
<div class="entry-content"> <p>An article without a translation.
|
||||||
|
Here is a link to an <a class="reference external" href="http://example.com/test/pages/untranslated-page.html">untranslated page</a></p>
|
||||||
|
</div><!-- /.entry-content -->
|
||||||
|
</article></li>
|
||||||
|
</ol><!-- /#posts-list -->
|
||||||
|
</section><!-- /#content -->
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
40
i18n_subsites/test_data/output/cz/pages/404.html
Normal file
40
i18n_subsites/test_data/output/cz/pages/404.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="cz">
|
||||||
|
<head>
|
||||||
|
<title>Testovací stránka - 404 stránka</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testovací stránka Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/cz/../theme/style.css" />
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="de" href="http://example.com/test/cz/../de/pages/404.html">
|
||||||
|
<link rel="alternate" hreflang="en" href="http://example.com/test/cz/../pages/404.html">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/cz/">Testovací stránka</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li><a href="http://example.com/test/cz/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<h1>404 stránka</h1>
|
||||||
|
Translations:
|
||||||
|
<a href="http://example.com/test/cz/../de/pages/404.html" hreflang="de">de</a>
|
||||||
|
<a href="http://example.com/test/cz/../pages/404.html" hreflang="en">en</a>
|
||||||
|
|
||||||
|
|
||||||
|
<p>Jednoduchá 404 stránka.</p>
|
||||||
|
|
||||||
|
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
61
i18n_subsites/test_data/output/cz/translated-article.html
Normal file
61
i18n_subsites/test_data/output/cz/translated-article.html
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="cz">
|
||||||
|
<head>
|
||||||
|
<title>Testovací stránka - Přeložený článek</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testovací stránka Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/cz/../theme/style.css" />
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="de" href="http://example.com/test/cz/../de/translated-article.html">
|
||||||
|
<link rel="alternate" hreflang="en" href="http://example.com/test/cz/../translated-article.html">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/cz/">Testovací stránka</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li class="active"><a href="http://example.com/test/cz/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<section id="content" class="body">
|
||||||
|
<header>
|
||||||
|
<h2 class="entry-title">
|
||||||
|
<a href="http://example.com/test/cz/translated-article.html" rel="bookmark"
|
||||||
|
title="Permalink to Přeložený článek">Přeložený článek</a></h2>
|
||||||
|
Translations:
|
||||||
|
<a href="http://example.com/test/cz/../de/translated-article.html" hreflang="de">de</a>
|
||||||
|
<a href="http://example.com/test/cz/../translated-article.html" hreflang="en">en</a>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
<footer class="post-info">
|
||||||
|
<time class="published" datetime="2014-09-15T00:00:00+00:00">
|
||||||
|
Mon 15 September 2014
|
||||||
|
</time>
|
||||||
|
<address class="vcard author">
|
||||||
|
By <a class="url fn" href="http://example.com/test/cz/author/test-testovic.html">Test Testovič</a>
|
||||||
|
</address>
|
||||||
|
<div class="category">
|
||||||
|
Category: <a href="http://example.com/test/cz/category/misc.html">misc</a>
|
||||||
|
</div>
|
||||||
|
</footer><!-- /.post-info -->
|
||||||
|
<div class="entry-content">
|
||||||
|
<p>Jednoduchý článek s překlady.
|
||||||
|
Zde je odkaz na <a class="reference external" href="http://example.com/test/images/img.png">nějaký obrázek</a>.</p>
|
||||||
|
|
||||||
|
</div><!-- /.entry-content -->
|
||||||
|
</section>
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Testseite - An untranslated article</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testseite Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/de/../theme/style.css" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/de/">Testseite</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li class="active"><a href="http://example.com/test/de/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<section id="content" class="body">
|
||||||
|
<header>
|
||||||
|
<h2 class="entry-title">
|
||||||
|
<a href="http://example.com/test/de/drafts/an-untranslated-article-en.html" rel="bookmark"
|
||||||
|
title="Permalink to An untranslated article">An untranslated article</a></h2>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
<footer class="post-info">
|
||||||
|
<time class="published" datetime="2014-07-14T00:00:00+00:00">
|
||||||
|
Mo 14 Juli 2014
|
||||||
|
</time>
|
||||||
|
<address class="vcard author">
|
||||||
|
By <a class="url fn" href="http://example.com/test/de/author/der-tester.html">Der Tester</a>
|
||||||
|
</address>
|
||||||
|
<div class="category">
|
||||||
|
Category: <a href="http://example.com/test/de/category/misc.html">misc</a>
|
||||||
|
</div>
|
||||||
|
</footer><!-- /.post-info -->
|
||||||
|
<div class="entry-content">
|
||||||
|
<p>An article without a translation.
|
||||||
|
Here is a link to an <a class="reference external" href="http://example.com/test/de/pages/untranslated-page-en.html">untranslated page</a></p>
|
||||||
|
|
||||||
|
</div><!-- /.entry-content -->
|
||||||
|
</section>
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
8
i18n_subsites/test_data/output/de/feeds_all.atom.xml
Normal file
8
i18n_subsites/test_data/output/de/feeds_all.atom.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom"><title>Testseite</title><link href="http://example.com/test/de/" rel="alternate"></link><link href="http://example.com/test/feeds_all.atom.xml" rel="self"></link><id>http://example.com/test/de/</id><updated>2014-09-15T00:00:00+00:00</updated><entry><title>Přeložený článek</title><link href="http://example.com/test/cz/translated-article.html" rel="alternate"></link><published>2014-09-15T00:00:00+00:00</published><updated>2014-09-15T00:00:00+00:00</updated><author><name>Der Tester</name></author><id>tag:example.com,2014-09-15:/test/cz/translated-article.html</id><content type="html"><p>Jednoduchý článek s překlady.
|
||||||
|
Zde je odkaz na <a class="reference external" href="http://example.com/test/images/img.png">nějaký obrázek</a>.</p>
|
||||||
|
</content><category term="misc"></category></entry><entry><title>Ein übersetzter Artikel</title><link href="http://example.com/test/de/translated-article.html" rel="alternate"></link><published>2014-09-14T00:00:00+00:00</published><updated>2014-09-14T00:00:00+00:00</updated><author><name>Der Tester</name></author><id>tag:example.com,2014-09-14:/test/de/translated-article.html</id><content type="html"><p>Ein einfacher Artikel mit einer Übersetzung.
|
||||||
|
Hier ist ein Link zur <a class="reference external" href="http://example.com/test/images/img.png">einigem Bild</a>.</p>
|
||||||
|
</content><category term="misc"></category></entry><entry><title>A translated article</title><link href="http://example.com/test/translated-article.html" rel="alternate"></link><published>2014-09-13T00:00:00+00:00</published><updated>2014-09-13T00:00:00+00:00</updated><author><name>Der Tester</name></author><id>tag:example.com,2014-09-13:/test/translated-article.html</id><content type="html"><p>A simple article with a translation.
|
||||||
|
Here is a link to <a class="reference external" href="http://example.com/test/images/img.png">some image</a>.</p>
|
||||||
|
</content><category term="misc"></category></entry></feed>
|
44
i18n_subsites/test_data/output/de/index.html
Normal file
44
i18n_subsites/test_data/output/de/index.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<title>Willkommen Sie zur unserer Testseite</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testseite Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/de/../theme/style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/de/">Testseite</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li><a href="http://example.com/test/de/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<section id="content">
|
||||||
|
<h2>All articles</h2>
|
||||||
|
|
||||||
|
<ol id="post-list">
|
||||||
|
<li><article class="hentry">
|
||||||
|
<header> <h2 class="entry-title"><a href="http://example.com/test/de/translated-article.html" rel="bookmark" title="Permalink to Ein übersetzter Artikel">Ein übersetzter Artikel</a></h2> </header>
|
||||||
|
<footer class="post-info">
|
||||||
|
<time class="published" datetime="2014-09-14T00:00:00+00:00"> So 14 September 2014 </time>
|
||||||
|
<address class="vcard author">By
|
||||||
|
<a class="url fn" href="http://example.com/test/de/author/der-tester.html">Der Tester</a>
|
||||||
|
</address>
|
||||||
|
</footer><!-- /.post-info -->
|
||||||
|
<div class="entry-content"> <p>Ein einfacher Artikel mit einer Übersetzung.
|
||||||
|
Hier ist ein Link zur <a class="reference external" href="http://example.com/test/images/img.png">einigem Bild</a>.</p>
|
||||||
|
</div><!-- /.entry-content -->
|
||||||
|
</article></li>
|
||||||
|
</ol><!-- /#posts-list -->
|
||||||
|
</section><!-- /#content -->
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
40
i18n_subsites/test_data/output/de/pages/404.html
Normal file
40
i18n_subsites/test_data/output/de/pages/404.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<title>Testseite - Eine 404 Seite</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testseite Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/de/../theme/style.css" />
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="cz" href="http://example.com/test/de/../cz/pages/404.html">
|
||||||
|
<link rel="alternate" hreflang="en" href="http://example.com/test/de/../pages/404.html">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/de/">Testseite</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li><a href="http://example.com/test/de/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<h1>Eine 404 Seite</h1>
|
||||||
|
Translations:
|
||||||
|
<a href="http://example.com/test/de/../cz/pages/404.html" hreflang="cz">cz</a>
|
||||||
|
<a href="http://example.com/test/de/../pages/404.html" hreflang="en">en</a>
|
||||||
|
|
||||||
|
|
||||||
|
<p>Eine einfache 404 Seite.</p>
|
||||||
|
|
||||||
|
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Testseite - Untranslated page</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testseite Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/de/../theme/style.css" />
|
||||||
|
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/de/">Testseite</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li><a href="http://example.com/test/de/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<h1>Untranslated page</h1>
|
||||||
|
|
||||||
|
|
||||||
|
<p>This page has no translation.</p>
|
||||||
|
|
||||||
|
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
61
i18n_subsites/test_data/output/de/translated-article.html
Normal file
61
i18n_subsites/test_data/output/de/translated-article.html
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<title>Testseite - Ein übersetzter Artikel</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testseite Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/de/../theme/style.css" />
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="cz" href="http://example.com/test/de/../cz/translated-article.html">
|
||||||
|
<link rel="alternate" hreflang="en" href="http://example.com/test/de/../translated-article.html">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/de/">Testseite</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li class="active"><a href="http://example.com/test/de/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<section id="content" class="body">
|
||||||
|
<header>
|
||||||
|
<h2 class="entry-title">
|
||||||
|
<a href="http://example.com/test/de/translated-article.html" rel="bookmark"
|
||||||
|
title="Permalink to Ein übersetzter Artikel">Ein übersetzter Artikel</a></h2>
|
||||||
|
Translations:
|
||||||
|
<a href="http://example.com/test/de/../cz/translated-article.html" hreflang="cz">cz</a>
|
||||||
|
<a href="http://example.com/test/de/../translated-article.html" hreflang="en">en</a>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
<footer class="post-info">
|
||||||
|
<time class="published" datetime="2014-09-14T00:00:00+00:00">
|
||||||
|
So 14 September 2014
|
||||||
|
</time>
|
||||||
|
<address class="vcard author">
|
||||||
|
By <a class="url fn" href="http://example.com/test/de/author/der-tester.html">Der Tester</a>
|
||||||
|
</address>
|
||||||
|
<div class="category">
|
||||||
|
Category: <a href="http://example.com/test/de/category/misc.html">misc</a>
|
||||||
|
</div>
|
||||||
|
</footer><!-- /.post-info -->
|
||||||
|
<div class="entry-content">
|
||||||
|
<p>Ein einfacher Artikel mit einer Übersetzung.
|
||||||
|
Hier ist ein Link zur <a class="reference external" href="http://example.com/test/images/img.png">einigem Bild</a>.</p>
|
||||||
|
|
||||||
|
</div><!-- /.entry-content -->
|
||||||
|
</section>
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
10
i18n_subsites/test_data/output/feeds_all.atom.xml
Normal file
10
i18n_subsites/test_data/output/feeds_all.atom.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom"><title>Testing site</title><link href="http://example.com/test/" rel="alternate"></link><link href="http://example.com/test/feeds_all.atom.xml" rel="self"></link><id>http://example.com/test/</id><updated>2014-09-15T00:00:00+00:00</updated><entry><title>Přeložený článek</title><link href="http://example.com/test/cz/translated-article.html" rel="alternate"></link><published>2014-09-15T00:00:00+00:00</published><updated>2014-09-15T00:00:00+00:00</updated><author><name>The Tester</name></author><id>tag:example.com,2014-09-15:/test/cz/translated-article.html</id><content type="html"><p>Jednoduchý článek s překlady.
|
||||||
|
Zde je odkaz na <a class="reference external" href="http://example.com/test/images/img.png">nějaký obrázek</a>.</p>
|
||||||
|
</content><category term="misc"></category></entry><entry><title>Ein übersetzter Artikel</title><link href="http://example.com/test/de/translated-article.html" rel="alternate"></link><published>2014-09-14T00:00:00+00:00</published><updated>2014-09-14T00:00:00+00:00</updated><author><name>The Tester</name></author><id>tag:example.com,2014-09-14:/test/de/translated-article.html</id><content type="html"><p>Ein einfacher Artikel mit einer Übersetzung.
|
||||||
|
Hier ist ein Link zur <a class="reference external" href="http://example.com/test/images/img.png">einigem Bild</a>.</p>
|
||||||
|
</content><category term="misc"></category></entry><entry><title>A translated article</title><link href="http://example.com/test/translated-article.html" rel="alternate"></link><published>2014-09-13T00:00:00+00:00</published><updated>2014-09-13T00:00:00+00:00</updated><author><name>The Tester</name></author><id>tag:example.com,2014-09-13:/test/translated-article.html</id><content type="html"><p>A simple article with a translation.
|
||||||
|
Here is a link to <a class="reference external" href="http://example.com/test/images/img.png">some image</a>.</p>
|
||||||
|
</content><category term="misc"></category></entry><entry><title>An untranslated article</title><link href="http://example.com/test/an-untranslated-article.html" rel="alternate"></link><published>2014-07-14T00:00:00+00:00</published><updated>2014-07-14T00:00:00+00:00</updated><author><name>The Tester</name></author><id>tag:example.com,2014-07-14:/test/an-untranslated-article.html</id><content type="html"><p>An article without a translation.
|
||||||
|
Here is a link to an <a class="reference external" href="http://example.com/test/pages/untranslated-page.html">untranslated page</a></p>
|
||||||
|
</content><category term="misc"></category></entry></feed>
|
0
i18n_subsites/test_data/output/images/img.png
Normal file
0
i18n_subsites/test_data/output/images/img.png
Normal file
57
i18n_subsites/test_data/output/index.html
Normal file
57
i18n_subsites/test_data/output/index.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Welcome to our Testing site</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testing site Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/theme/style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/">Testing site</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li><a href="http://example.com/test/pages/untranslated-page.html">Untranslated page</a></li>
|
||||||
|
<li><a href="http://example.com/test/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<section id="content">
|
||||||
|
<h2>All articles</h2>
|
||||||
|
|
||||||
|
<ol id="post-list">
|
||||||
|
<li><article class="hentry">
|
||||||
|
<header> <h2 class="entry-title"><a href="http://example.com/test/translated-article.html" rel="bookmark" title="Permalink to A translated article">A translated article</a></h2> </header>
|
||||||
|
<footer class="post-info">
|
||||||
|
<time class="published" datetime="2014-09-13T00:00:00+00:00"> Sat 13 September 2014 </time>
|
||||||
|
<address class="vcard author">By
|
||||||
|
<a class="url fn" href="http://example.com/test/author/the-tester.html">The Tester</a>
|
||||||
|
</address>
|
||||||
|
</footer><!-- /.post-info -->
|
||||||
|
<div class="entry-content"> <p>A simple article with a translation.
|
||||||
|
Here is a link to <a class="reference external" href="http://example.com/test/images/img.png">some image</a>.</p>
|
||||||
|
</div><!-- /.entry-content -->
|
||||||
|
</article></li>
|
||||||
|
<li><article class="hentry">
|
||||||
|
<header> <h2 class="entry-title"><a href="http://example.com/test/an-untranslated-article.html" rel="bookmark" title="Permalink to An untranslated article">An untranslated article</a></h2> </header>
|
||||||
|
<footer class="post-info">
|
||||||
|
<time class="published" datetime="2014-07-14T00:00:00+00:00"> Mon 14 July 2014 </time>
|
||||||
|
<address class="vcard author">By
|
||||||
|
<a class="url fn" href="http://example.com/test/author/the-tester.html">The Tester</a>
|
||||||
|
</address>
|
||||||
|
</footer><!-- /.post-info -->
|
||||||
|
<div class="entry-content"> <p>An article without a translation.
|
||||||
|
Here is a link to an <a class="reference external" href="http://example.com/test/pages/untranslated-page.html">untranslated page</a></p>
|
||||||
|
</div><!-- /.entry-content -->
|
||||||
|
</article></li>
|
||||||
|
</ol><!-- /#posts-list -->
|
||||||
|
</section><!-- /#content -->
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
41
i18n_subsites/test_data/output/pages/404.html
Normal file
41
i18n_subsites/test_data/output/pages/404.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Testing site - A 404 page</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testing site Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/theme/style.css" />
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="cz" href="http://example.com/test/cz/pages/404.html">
|
||||||
|
<link rel="alternate" hreflang="de" href="http://example.com/test/de/pages/404.html">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/">Testing site</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li><a href="http://example.com/test/pages/untranslated-page.html">Untranslated page</a></li>
|
||||||
|
<li><a href="http://example.com/test/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<h1>A 404 page</h1>
|
||||||
|
Translations:
|
||||||
|
<a href="http://example.com/test/cz/pages/404.html" hreflang="cz">cz</a>
|
||||||
|
<a href="http://example.com/test/de/pages/404.html" hreflang="de">de</a>
|
||||||
|
|
||||||
|
|
||||||
|
<p>A simple 404 page.</p>
|
||||||
|
|
||||||
|
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
35
i18n_subsites/test_data/output/pages/untranslated-page.html
Normal file
35
i18n_subsites/test_data/output/pages/untranslated-page.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Testing site - Untranslated page</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testing site Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/theme/style.css" />
|
||||||
|
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/">Testing site</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li class="active"><a href="http://example.com/test/pages/untranslated-page.html">Untranslated page</a></li>
|
||||||
|
<li><a href="http://example.com/test/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<h1>Untranslated page</h1>
|
||||||
|
|
||||||
|
|
||||||
|
<p>This page has no translation.</p>
|
||||||
|
|
||||||
|
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
0
i18n_subsites/test_data/output/theme/style.css
Normal file
0
i18n_subsites/test_data/output/theme/style.css
Normal file
62
i18n_subsites/test_data/output/translated-article.html
Normal file
62
i18n_subsites/test_data/output/translated-article.html
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Testing site - A translated article</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="Pelican" />
|
||||||
|
<link href="http://example.com/test/feeds_all.atom.xml" type="application/atom+xml" rel="alternate" title="Testing site Full Atom Feed" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="http://example.com/test/theme/style.css" />
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="cz" href="http://example.com/test/cz/translated-article.html">
|
||||||
|
<link rel="alternate" hreflang="de" href="http://example.com/test/de/translated-article.html">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="index" class="home">
|
||||||
|
<header id="banner" class="body">
|
||||||
|
<h1><a href="http://example.com/test/">Testing site</a></h1>
|
||||||
|
</header><!-- /#banner -->
|
||||||
|
<nav id="menu"><ul>
|
||||||
|
<li><a href="http://example.com/test/pages/untranslated-page.html">Untranslated page</a></li>
|
||||||
|
<li class="active"><a href="http://example.com/test/category/misc.html">misc</a></li>
|
||||||
|
</ul></nav><!-- /#menu -->
|
||||||
|
<section id="content" class="body">
|
||||||
|
<header>
|
||||||
|
<h2 class="entry-title">
|
||||||
|
<a href="http://example.com/test/translated-article.html" rel="bookmark"
|
||||||
|
title="Permalink to A translated article">A translated article</a></h2>
|
||||||
|
Translations:
|
||||||
|
<a href="http://example.com/test/cz/translated-article.html" hreflang="cz">cz</a>
|
||||||
|
<a href="http://example.com/test/de/translated-article.html" hreflang="de">de</a>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
<footer class="post-info">
|
||||||
|
<time class="published" datetime="2014-09-13T00:00:00+00:00">
|
||||||
|
Sat 13 September 2014
|
||||||
|
</time>
|
||||||
|
<address class="vcard author">
|
||||||
|
By <a class="url fn" href="http://example.com/test/author/the-tester.html">The Tester</a>
|
||||||
|
</address>
|
||||||
|
<div class="category">
|
||||||
|
Category: <a href="http://example.com/test/category/misc.html">misc</a>
|
||||||
|
</div>
|
||||||
|
</footer><!-- /.post-info -->
|
||||||
|
<div class="entry-content">
|
||||||
|
<p>A simple article with a translation.
|
||||||
|
Here is a link to <a class="reference external" href="http://example.com/test/images/img.png">some image</a>.</p>
|
||||||
|
|
||||||
|
</div><!-- /.entry-content -->
|
||||||
|
</section>
|
||||||
|
<footer id="contentinfo" class="body">
|
||||||
|
<address id="about" class="vcard body">
|
||||||
|
Proudly powered by <a href="https://getpelican.com/">Pelican</a>,
|
||||||
|
which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||||
|
</address><!-- /#about -->
|
||||||
|
</footer><!-- /#contentinfo -->
|
||||||
|
</body>
|
||||||
|
</html>
|
53
i18n_subsites/test_data/pelicanconf.py
Normal file
53
i18n_subsites/test_data/pelicanconf.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*- #
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
AUTHOR = 'The Tester'
|
||||||
|
SITENAME = 'Testing site'
|
||||||
|
SITEURL = 'http://example.com/test'
|
||||||
|
|
||||||
|
# to make the test suite portable
|
||||||
|
TIMEZONE = 'UTC'
|
||||||
|
|
||||||
|
DEFAULT_LANG = 'en'
|
||||||
|
LOCALE = 'en_US.UTF-8'
|
||||||
|
|
||||||
|
# Generate only one feed
|
||||||
|
FEED_ALL_ATOM = 'feeds_all.atom.xml'
|
||||||
|
CATEGORY_FEED_ATOM = None
|
||||||
|
TRANSLATION_FEED_ATOM = None
|
||||||
|
AUTHOR_FEED_ATOM = None
|
||||||
|
AUTHOR_FEED_RSS = None
|
||||||
|
|
||||||
|
# Disable unnecessary pages
|
||||||
|
CATEGORY_SAVE_AS = ''
|
||||||
|
TAG_SAVE_AS = ''
|
||||||
|
AUTHOR_SAVE_AS = ''
|
||||||
|
ARCHIVES_SAVE_AS = ''
|
||||||
|
AUTHORS_SAVE_AS = ''
|
||||||
|
CATEGORIES_SAVE_AS = ''
|
||||||
|
TAGS_SAVE_AS = ''
|
||||||
|
|
||||||
|
PLUGIN_PATHS = ['../../']
|
||||||
|
PLUGINS = ['i18n_subsites']
|
||||||
|
|
||||||
|
THEME = 'localized_theme'
|
||||||
|
JINJA_ENVIRONMENT = {'extensions': ['jinja2.ext.i18n']}
|
||||||
|
|
||||||
|
from blinker import signal
|
||||||
|
tmpsig = signal('tmpsig')
|
||||||
|
I18N_FILTER_SIGNALS = [tmpsig]
|
||||||
|
|
||||||
|
I18N_SUBSITES = {
|
||||||
|
'de': {
|
||||||
|
'SITENAME': 'Testseite',
|
||||||
|
'AUTHOR': 'Der Tester',
|
||||||
|
'LOCALE': 'de_DE.UTF-8',
|
||||||
|
},
|
||||||
|
'cz': {
|
||||||
|
'SITENAME': 'Testovací stránka',
|
||||||
|
'AUTHOR': 'Test Testovič',
|
||||||
|
'I18N_UNTRANSLATED_PAGES': 'remove',
|
||||||
|
'I18N_UNTRANSLATED_ARTICLES': 'keep',
|
||||||
|
},
|
||||||
|
}
|
139
i18n_subsites/test_i18n_subsites.py
Normal file
139
i18n_subsites/test_i18n_subsites.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
'''Unit tests for the i18n_subsites plugin'''
|
||||||
|
|
||||||
|
import os
|
||||||
|
import locale
|
||||||
|
import unittest
|
||||||
|
import subprocess
|
||||||
|
from tempfile import mkdtemp
|
||||||
|
from shutil import rmtree
|
||||||
|
|
||||||
|
from . import i18n_subsites as i18ns
|
||||||
|
from pelican import Pelican
|
||||||
|
from pelican.tests.support import get_settings
|
||||||
|
from pelican.settings import read_settings
|
||||||
|
|
||||||
|
|
||||||
|
class TestTemporaryLocale(unittest.TestCase):
|
||||||
|
'''Test the temporary locale context manager'''
|
||||||
|
|
||||||
|
def test_locale_restored(self):
|
||||||
|
'''Test that the locale is restored after exiting context'''
|
||||||
|
orig_locale = locale.setlocale(locale.LC_ALL)
|
||||||
|
with i18ns.temporary_locale():
|
||||||
|
locale.setlocale(locale.LC_ALL, 'C')
|
||||||
|
self.assertEqual(locale.setlocale(locale.LC_ALL), 'C')
|
||||||
|
self.assertEqual(locale.setlocale(locale.LC_ALL), orig_locale)
|
||||||
|
|
||||||
|
def test_temp_locale_set(self):
|
||||||
|
'''Test that the temporary locale is set'''
|
||||||
|
with i18ns.temporary_locale('C'):
|
||||||
|
self.assertEqual(locale.setlocale(locale.LC_ALL), 'C')
|
||||||
|
|
||||||
|
|
||||||
|
class TestSettingsManipulation(unittest.TestCase):
|
||||||
|
'''Test operations on settings dict'''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
'''Prepare default settings'''
|
||||||
|
self.settings = get_settings()
|
||||||
|
|
||||||
|
def test_get_pelican_cls_class(self):
|
||||||
|
'''Test that we get class given as an object'''
|
||||||
|
self.settings['PELICAN_CLASS'] = object
|
||||||
|
cls = i18ns.get_pelican_cls(self.settings)
|
||||||
|
self.assertIs(cls, object)
|
||||||
|
|
||||||
|
def test_get_pelican_cls_str(self):
|
||||||
|
'''Test that we get correct class given by string'''
|
||||||
|
cls = i18ns.get_pelican_cls(self.settings)
|
||||||
|
self.assertIs(cls, Pelican)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSitesRelpath(unittest.TestCase):
|
||||||
|
'''Test relative path between sites generation'''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
'''Generate some sample siteurls'''
|
||||||
|
self.siteurl = 'http://example.com'
|
||||||
|
i18ns._SITE_DB['en'] = self.siteurl
|
||||||
|
i18ns._SITE_DB['de'] = self.siteurl + '/de'
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
'''Remove sites from db'''
|
||||||
|
i18ns._SITE_DB.clear()
|
||||||
|
|
||||||
|
def test_get_site_path(self):
|
||||||
|
'''Test getting the path within a site'''
|
||||||
|
self.assertEqual(i18ns.get_site_path(self.siteurl), '/')
|
||||||
|
self.assertEqual(i18ns.get_site_path(self.siteurl + '/de'), '/de')
|
||||||
|
|
||||||
|
def test_relpath_to_site(self):
|
||||||
|
'''Test getting relative paths between sites'''
|
||||||
|
self.assertEqual(i18ns.relpath_to_site('en', 'de'), 'de')
|
||||||
|
self.assertEqual(i18ns.relpath_to_site('de', 'en'), '..')
|
||||||
|
|
||||||
|
|
||||||
|
class TestRegistration(unittest.TestCase):
|
||||||
|
'''Test plugin registration'''
|
||||||
|
|
||||||
|
def test_return_on_missing_signal(self):
|
||||||
|
'''Test return on missing required signal'''
|
||||||
|
i18ns._SIGNAL_HANDLERS_DB['tmp_sig'] = None
|
||||||
|
i18ns.register()
|
||||||
|
self.assertNotIn(id(i18ns.save_generator),
|
||||||
|
i18ns.signals.generator_init.receivers)
|
||||||
|
|
||||||
|
def test_registration(self):
|
||||||
|
'''Test registration of all signal handlers'''
|
||||||
|
i18ns.register()
|
||||||
|
for sig_name, handler in i18ns._SIGNAL_HANDLERS_DB.items():
|
||||||
|
sig = getattr(i18ns.signals, sig_name)
|
||||||
|
self.assertIn(id(handler), sig.receivers)
|
||||||
|
# clean up
|
||||||
|
sig.disconnect(handler)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFullRun(unittest.TestCase):
|
||||||
|
'''Test running Pelican with the Plugin'''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
'''Create temporary output and cache folders'''
|
||||||
|
self.temp_path = mkdtemp(prefix='pelicantests.')
|
||||||
|
self.temp_cache = mkdtemp(prefix='pelican_cache.')
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
'''Remove output and cache folders'''
|
||||||
|
rmtree(self.temp_path)
|
||||||
|
rmtree(self.temp_cache)
|
||||||
|
|
||||||
|
def test_sites_generation(self):
|
||||||
|
'''Test generation of sites with the plugin
|
||||||
|
|
||||||
|
Compare with recorded output via ``git diff``.
|
||||||
|
To generate output for comparison run the command
|
||||||
|
``pelican -o test_data/output -s test_data/pelicanconf.py \
|
||||||
|
test_data/content``
|
||||||
|
Remember to remove the output/ folder before that.
|
||||||
|
'''
|
||||||
|
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
base_path = os.path.join(base_path, 'test_data')
|
||||||
|
content_path = os.path.join(base_path, 'content')
|
||||||
|
output_path = os.path.join(base_path, 'output')
|
||||||
|
settings_path = os.path.join(base_path, 'pelicanconf.py')
|
||||||
|
settings = read_settings(path=settings_path, override={
|
||||||
|
'PATH': content_path,
|
||||||
|
'OUTPUT_PATH': self.temp_path,
|
||||||
|
'CACHE_PATH': self.temp_cache,
|
||||||
|
'PLUGINS': [i18ns],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pelican = Pelican(settings)
|
||||||
|
pelican.run()
|
||||||
|
|
||||||
|
# compare output
|
||||||
|
out, err = subprocess.Popen(
|
||||||
|
['git', 'diff', '--no-ext-diff', '--exit-code', '-w', output_path,
|
||||||
|
self.temp_path], env={'PAGER': ''},
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||||
|
self.assertFalse(out, 'non-empty `diff` stdout:\n{}'.format(out))
|
||||||
|
self.assertFalse(err, 'non-empty `diff` stderr:\n{}'.format(err))
|
48
series/Readme.md
Normal file
48
series/Readme.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
Series plugin
|
||||||
|
-------------
|
||||||
|
|
||||||
|
**NOTE:** [This plugin has been moved to its own repository](https://github.com/pelican-plugins/series). Please file any issues/PRs there. Once all plugins have been migrated to the [new Pelican Plugins organization](https://github.com/pelican-plugins), this monolithic repository will be archived.
|
||||||
|
|
||||||
|
The series plugin allows you to join different posts into a series.
|
||||||
|
|
||||||
|
In order to mark posts as part of a series, use the `:series:` metadata:
|
||||||
|
|
||||||
|
:series: NAME_OF_THIS_SERIES
|
||||||
|
|
||||||
|
or, in Markdown syntax
|
||||||
|
|
||||||
|
Series: NAME_OF_THIS_SERIES
|
||||||
|
|
||||||
|
The plugin collects all articles belonging to the same series and provides
|
||||||
|
series-related variables that you can use in your template.
|
||||||
|
|
||||||
|
#### Indexing
|
||||||
|
By default articles in a series are ordered by date and then automatically numbered.
|
||||||
|
|
||||||
|
If you want to force a given order just specify the `:series_index:` metadata or in Markdown `series_index:`,
|
||||||
|
starting from 1. All articles with this enforced index are put at the beginning of
|
||||||
|
the series and ordered according to the index itself. All the remaining articles
|
||||||
|
come after them, ordered by date.
|
||||||
|
|
||||||
|
The plugin provides the following variables to your templates
|
||||||
|
|
||||||
|
* `article.series.name` is the name of the series as specified in the article metadata
|
||||||
|
* `article.series.index` is the index of the current article inside the series
|
||||||
|
* `article.series.all` is an ordered list of all articles in the series (including the current one)
|
||||||
|
* `article.series.all_previous` is an ordered list of the articles published before the current one
|
||||||
|
* `article.series.all_next` is an ordered list of the articles published after the current one
|
||||||
|
* `article.series.previous` is the previous article in the series (a shortcut to `article.series.all_previous[-1]`)
|
||||||
|
* `article.series.next` is the next article in the series (a shortcut to `article.series.all_next[0]`)
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
{% if article.series %}
|
||||||
|
<p>This post is part {{ article.series.index }} of the "{{ article.series.name }}" series:</p>
|
||||||
|
<ol class="parts">
|
||||||
|
{% for part_article in article.series.all %}
|
||||||
|
<li {% if part_article == article %}class="active"{% endif %}>
|
||||||
|
<a href='{{ SITEURL }}/{{ part_article.url }}'>{{ part_article.title }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
{% endif %}
|
1
series/__init__.py
Normal file
1
series/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .series import *
|
72
series/series.py
Normal file
72
series/series.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This plugin extends the original series plugin
|
||||||
|
by FELD Boris <lothiraldan@gmail.com>
|
||||||
|
|
||||||
|
Copyright (c) Leonardo Giordani <giordani.leonardo@gmail.com>
|
||||||
|
|
||||||
|
Joins articles in a series and provides variables to
|
||||||
|
manage the series in the template.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
|
from pelican import signals
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_series(generator):
|
||||||
|
series = defaultdict(list)
|
||||||
|
|
||||||
|
# This cycles through all articles in the given generator
|
||||||
|
# and collects the 'series' metadata, if present.
|
||||||
|
# The 'series_index' metadata is also stored, if specified
|
||||||
|
for article in generator.articles:
|
||||||
|
if 'series' in article.metadata:
|
||||||
|
article_entry = (
|
||||||
|
article.metadata.get('series_index', None),
|
||||||
|
article.metadata['date'],
|
||||||
|
article
|
||||||
|
)
|
||||||
|
|
||||||
|
series[article.metadata['series']].append(article_entry)
|
||||||
|
|
||||||
|
# This uses items() which on Python2 is not a generator
|
||||||
|
# but we are dealing with a small amount of data so
|
||||||
|
# there shouldn't be performance issues =)
|
||||||
|
for series_name, series_articles in series.items():
|
||||||
|
# This is not DRY but very simple to understand
|
||||||
|
forced_order_articles = [
|
||||||
|
art_tup for art_tup in series_articles if art_tup[0] is not None]
|
||||||
|
|
||||||
|
date_order_articles = [
|
||||||
|
art_tup for art_tup in series_articles if art_tup[0] is None]
|
||||||
|
|
||||||
|
forced_order_articles.sort(key=itemgetter(0))
|
||||||
|
date_order_articles.sort(key=itemgetter(1))
|
||||||
|
|
||||||
|
all_articles = forced_order_articles + date_order_articles
|
||||||
|
ordered_articles = [art_tup[2] for art_tup in all_articles]
|
||||||
|
enumerated_articles = enumerate(ordered_articles)
|
||||||
|
|
||||||
|
for index, article in enumerated_articles:
|
||||||
|
article.series = dict()
|
||||||
|
article.series['name'] = series_name
|
||||||
|
article.series['index'] = index + 1
|
||||||
|
article.series['all'] = ordered_articles
|
||||||
|
article.series['all_previous'] = ordered_articles[0: index]
|
||||||
|
article.series['all_next'] = ordered_articles[index + 1:]
|
||||||
|
|
||||||
|
if index > 0:
|
||||||
|
article.series['previous'] = ordered_articles[index - 1]
|
||||||
|
else:
|
||||||
|
article.series['previous'] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
article.series['next'] = ordered_articles[index + 1]
|
||||||
|
except IndexError:
|
||||||
|
article.series['next'] = None
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
signals.article_generator_finalized.connect(aggregate_series)
|
100
subcategory/README.md
Normal file
100
subcategory/README.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# Subcategory Plugin
|
||||||
|
|
||||||
|
This plugin adds support for subcategories in addition to article categories.
|
||||||
|
|
||||||
|
Subcategories are hierarchical. Each subcategory has a parent, which is either a regular category or another subcategory.
|
||||||
|
|
||||||
|
Feeds can be generated for each subcategory, just like categories and tags.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Metadata
|
||||||
|
|
||||||
|
Subcategories are an extension to categories. Add subcategories to an article's category metadata using a `/` like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
Category: Regular Category/Sub-Category/Sub-Sub-category
|
||||||
|
```
|
||||||
|
|
||||||
|
Then create a `subcategory.html` template in your theme, similar to the `category.html` or `tag.html` templates.
|
||||||
|
|
||||||
|
In your templates, `article.category` continues to act the same way. Your subcategories are stored in the `articles.subcategories` list. To create breadcrumb-style navigation you might try something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
<nav class="breadcrumb">
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<a href="{{ SITEURL }}/{{ article.category.url }}">{{ article.category}}</a>
|
||||||
|
</li>
|
||||||
|
{% for subcategory in article.subcategories %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ SITEURL }}/{{ subcategory.url }}">{{ subcategory.shortname }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subcategory folders
|
||||||
|
|
||||||
|
To specify subcategories using folders you can configure `PATH_METADATA`<br>
|
||||||
|
to extract the article path (containing all category and subcategory folders) into the `subcategory_path` metadata. The following settings would use all available subcategories for the hierarchy:
|
||||||
|
|
||||||
|
```
|
||||||
|
PATH_METADATA= '(?P<subcategory_path>.*)/.*'
|
||||||
|
```
|
||||||
|
|
||||||
|
You can limit the depth of generated subcategories by adjusting the regular expression to only include a specific number of path separators (`/`). For example, the following would generate only a single level of subcategories regardless of the folder tree depth:
|
||||||
|
|
||||||
|
```
|
||||||
|
PATH_METADATA= '(?P<subcategory_path>[^/]*/[^/]*)/.*'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subcategory Names
|
||||||
|
|
||||||
|
Each subcategory's full name is a `/`-separated list of it parents and itself. This is necessary to keep each subcategory unique. It means you can have `Category 1/Foo` and `Category 2/Foo` and they won't interfere with each other. Each subcategory has an attribute `shortname` which is just the name without its parents associated. For example if you had...
|
||||||
|
|
||||||
|
```
|
||||||
|
Category/Sub Category1/Sub Category2
|
||||||
|
```
|
||||||
|
|
||||||
|
... the full name for Sub Category2 would be `Category/Sub Category1/Sub Category2` and the "short name" would be `Sub Category2`.
|
||||||
|
|
||||||
|
If you need to use the slug, it is generated from the short name -- not the full name.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
Consistent with the default settings for Tags and Categories, the default settings for subcategories are:
|
||||||
|
|
||||||
|
```
|
||||||
|
'SUBCATEGORY_SAVE_AS' = os.path.join('subcategory', '{savepath}.html')
|
||||||
|
'SUBCATEGORY_URL' = 'subcategory/(fullurl).html'
|
||||||
|
```
|
||||||
|
|
||||||
|
`savepath` and `fullurl` are generated recursively, using slugs. So the full URL would be:
|
||||||
|
|
||||||
|
```
|
||||||
|
category-slug/sub-category-slug/sub-sub-category-slug
|
||||||
|
```
|
||||||
|
|
||||||
|
... with `savepath` being similar but joined using `os.path.join`.
|
||||||
|
|
||||||
|
Similarly, you can save subcategory feeds by adding one of the following to your Pelican configuration file:
|
||||||
|
|
||||||
|
```
|
||||||
|
SUBCATEGORY_FEED_ATOM = 'feeds/%s.atom.xml'
|
||||||
|
SUBCATEGORY_FEED_RSS = 'feeds/%s.rss.xml'
|
||||||
|
```
|
||||||
|
|
||||||
|
... and this will create a feed with `fullurl` of the subcategory. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
feeds/category/subcategory.atom.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
Article urls can also use the values of `subpath` and `suburl` in their definitions. These are equivalent to the `fullurl` and `savepath` of the most specific subcategory. If you have articles that don't have subcategories these values are set to the category slug.
|
||||||
|
|
||||||
|
```
|
||||||
|
ARTICLE_SAVE_AS = os.path.join('{subpath}' 'articles' '{slug}.html')
|
||||||
|
ARTICLE_URL = '{suburl}/articles/{slug}.html'
|
||||||
|
```
|
1
subcategory/__init__.py
Normal file
1
subcategory/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .subcategory import *
|
135
subcategory/subcategory.py
Normal file
135
subcategory/subcategory.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
@Author: Alistair Magee
|
||||||
|
|
||||||
|
Adds support for subcategories on pelican articles
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from operator import attrgetter
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from pelican import signals
|
||||||
|
from pelican.urlwrappers import URLWrapper, Category
|
||||||
|
from pelican.utils import slugify
|
||||||
|
|
||||||
|
from six import text_type
|
||||||
|
|
||||||
|
class SubCategory(URLWrapper):
|
||||||
|
def __init__(self, name, parent, settings):
|
||||||
|
super(SubCategory, self).__init__(name, settings)
|
||||||
|
self.parent = parent
|
||||||
|
self.shortname = name.split('/')
|
||||||
|
self.shortname = self.shortname.pop()
|
||||||
|
self.slug = slugify(self.shortname, settings.get('SLUG_SUBSTITUIONS', ()))
|
||||||
|
if isinstance(self.parent, SubCategory):
|
||||||
|
self.savepath = os.path.join(self.parent.savepath, self.slug)
|
||||||
|
self.fullurl = '{}/{}'.format(self.parent.fullurl, self.slug)
|
||||||
|
else: #parent is a category
|
||||||
|
self.savepath = os.path.join(self.parent.slug, self.slug)
|
||||||
|
self.fullurl = '{}/{}'.format(self.parent.slug, self.slug)
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
d = self.__dict__
|
||||||
|
d['shortname'] = self.shortname
|
||||||
|
d['savepath'] = self.savepath
|
||||||
|
d['fullurl'] = self.fullurl
|
||||||
|
d['parent'] = self.parent
|
||||||
|
return d
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.fullurl)
|
||||||
|
|
||||||
|
def _key(self):
|
||||||
|
return self.fullurl
|
||||||
|
|
||||||
|
def get_subcategories(generator, metadata):
|
||||||
|
if 'SUBCATEGORY_SAVE_AS' not in generator.settings:
|
||||||
|
generator.settings['SUBCATEGORY_SAVE_AS'] = os.path.join(
|
||||||
|
'subcategory', '{savepath}.html')
|
||||||
|
if 'SUBCATEGORY_URL' not in generator.settings:
|
||||||
|
generator.settings['SUBCATEGORY_URL'] = 'subcategory/{fullurl}.html'
|
||||||
|
if ('PAGINATED_TEMPLATES' in generator.settings and
|
||||||
|
'subcategory' not in generator.settings['PAGINATED_TEMPLATES']):
|
||||||
|
generator.settings['PAGINATED_TEMPLATES']['subcategory'] = None
|
||||||
|
|
||||||
|
if 'subcategory_path' in metadata:
|
||||||
|
category_list = text_type(metadata.get('subcategory_path')).split('/')
|
||||||
|
else:
|
||||||
|
category_list = text_type(metadata.get('category')).split('/')
|
||||||
|
|
||||||
|
category = (category_list.pop(0)).strip()
|
||||||
|
category = Category(category, generator.settings)
|
||||||
|
metadata['category'] = category
|
||||||
|
#generate a list of subcategories with their parents
|
||||||
|
sub_list = []
|
||||||
|
parent = category.name
|
||||||
|
for subcategory in category_list:
|
||||||
|
subcategory = parent + '/' + subcategory.strip()
|
||||||
|
sub_list.append(subcategory)
|
||||||
|
parent = subcategory
|
||||||
|
metadata['subcategories'] = sub_list
|
||||||
|
|
||||||
|
def create_subcategories(generator):
|
||||||
|
generator.subcategories = []
|
||||||
|
for article in generator.articles:
|
||||||
|
parent = article.category
|
||||||
|
actual_subcategories = []
|
||||||
|
for subcategory in article.subcategories:
|
||||||
|
#following line returns a list of items, tuples in this case
|
||||||
|
sub_cat = [item for item in generator.subcategories
|
||||||
|
if item[0].name == subcategory]
|
||||||
|
if sub_cat:
|
||||||
|
sub_cat[0][1].append(article)
|
||||||
|
parent = sub_cat[0][0]
|
||||||
|
actual_subcategories.append(parent)
|
||||||
|
else:
|
||||||
|
new_sub = SubCategory(subcategory, parent, generator.settings)
|
||||||
|
generator.subcategories.append((new_sub, [article,]))
|
||||||
|
parent = new_sub
|
||||||
|
actual_subcategories.append(parent)
|
||||||
|
article.subcategories = actual_subcategories
|
||||||
|
"""Add subpath and suburl to the article metadata. This allows the
|
||||||
|
the last subcategory's fullurl and savepath to be used when definining
|
||||||
|
Article URL's. If an article has no subcategories, the Category slug
|
||||||
|
is used instead
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
last_subcat = article.subcategories[-1]
|
||||||
|
article.metadata['subpath'] = last_subcat.savepath
|
||||||
|
article.metadata['suburl'] = last_subcat.fullurl
|
||||||
|
except IndexError: #No Subcategory
|
||||||
|
article.metadata['subpath'] = article.category.slug
|
||||||
|
article.metadata['suburl'] = article.category.slug
|
||||||
|
|
||||||
|
def generate_subcategories(generator, writer):
|
||||||
|
write = partial(writer.write_file,
|
||||||
|
relative_urls=generator.settings['RELATIVE_URLS'])
|
||||||
|
subcategory_template = generator.get_template('subcategory')
|
||||||
|
for subcat, articles in generator.subcategories:
|
||||||
|
articles.sort(key=attrgetter('date'), reverse=True)
|
||||||
|
dates = [article for article in generator.dates if article in articles]
|
||||||
|
write(subcat.save_as, subcategory_template, generator.context,
|
||||||
|
subcategory=subcat, articles=articles, dates=dates,
|
||||||
|
template_name='subcategory', page_name=subcat.page_name,
|
||||||
|
all_articles=generator.articles)
|
||||||
|
|
||||||
|
def generate_subcategory_feeds(generator, writer):
|
||||||
|
for subcat, articles in generator.subcategories:
|
||||||
|
articles.sort(key=attrgetter('date'), reverse=True)
|
||||||
|
if generator.settings.get('SUBCATEGORY_FEED_ATOM'):
|
||||||
|
writer.write_feed(articles, generator.context,
|
||||||
|
generator.settings['SUBCATEGORY_FEED_ATOM']
|
||||||
|
% subcat.fullurl)
|
||||||
|
if generator.settings.get('SUBCATEGORY_FEED_RSS'):
|
||||||
|
writer.write_feed(articles, generator.context,
|
||||||
|
generator.settings['SUBCATEGORY_FEED_RSS']
|
||||||
|
% subcat.fullurl, feed_type='rss')
|
||||||
|
|
||||||
|
def generate(generator, writer):
|
||||||
|
generate_subcategory_feeds(generator, writer)
|
||||||
|
generate_subcategories(generator, writer)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
signals.article_generator_context.connect(get_subcategories)
|
||||||
|
signals.article_generator_finalized.connect(create_subcategories)
|
||||||
|
signals.article_writer_finalized.connect(generate)
|
100
tag_cloud/README.rst
Normal file
100
tag_cloud/README.rst
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
tag_cloud
|
||||||
|
=========
|
||||||
|
|
||||||
|
**NOTE:** `This plugin has been moved to its own repository <https://github.com/pelican-plugins/tag-cloud>`_. Please file any issues/PRs there. Once all plugins have been migrated to the `new Pelican Plugins organization <https://github.com/pelican-plugins>`_, this monolithic repository will be archived.
|
||||||
|
|
||||||
|
This plugin generates a tag-cloud.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
In order to use to use this plugin, you have to edit(*) or create(+) the following files::
|
||||||
|
|
||||||
|
blog/
|
||||||
|
├── pelicanconf.py *
|
||||||
|
├── content
|
||||||
|
├── plugins +
|
||||||
|
│ └── tag_cloud.py +
|
||||||
|
└── themes
|
||||||
|
└── mytheme
|
||||||
|
├── templates
|
||||||
|
│ └── base.html *
|
||||||
|
└── static
|
||||||
|
└── css
|
||||||
|
└── style.css *
|
||||||
|
|
||||||
|
In **pelicanconf.py** you have to activate the plugin::
|
||||||
|
|
||||||
|
PLUGIN_PATHS = ["plugins"]
|
||||||
|
PLUGINS = ["tag_cloud"]
|
||||||
|
|
||||||
|
Into your **plugins** folder, you should add tag_cloud.py (from this repository).
|
||||||
|
|
||||||
|
In your theme files, you should change **base.html** to apply formats (and sizes) defined in **style.css**, as specified in "Settings", below.
|
||||||
|
|
||||||
|
Settings
|
||||||
|
--------
|
||||||
|
|
||||||
|
================================================ =====================================================
|
||||||
|
Setting name (followed by default value) What does it do?
|
||||||
|
================================================ =====================================================
|
||||||
|
``TAG_CLOUD_STEPS = 4`` Count of different font sizes in the tag
|
||||||
|
cloud.
|
||||||
|
``TAG_CLOUD_MAX_ITEMS = 100`` Maximum number of tags in the cloud.
|
||||||
|
``TAG_CLOUD_SORTING = 'random'`` The tag cloud ordering scheme. Valid values:
|
||||||
|
random, alphabetically, alphabetically-rev, size and
|
||||||
|
size-rev
|
||||||
|
``TAG_CLOUD_BADGE = True`` Optionnal setting : can bring **badges**, which mean
|
||||||
|
say : display the number of each tags present
|
||||||
|
on all articles.
|
||||||
|
================================================ =====================================================
|
||||||
|
|
||||||
|
The default theme does not include a tag cloud, but it is pretty easy to add one::
|
||||||
|
|
||||||
|
<ul class="tagcloud">
|
||||||
|
{% for tag in tag_cloud %}
|
||||||
|
<li class="tag-{{ tag.1 }}">
|
||||||
|
<a href="{{ SITEURL }}/{{ tag.0.url }}">
|
||||||
|
{{ tag.0 }}
|
||||||
|
{% if TAG_CLOUD_BADGE %}
|
||||||
|
<span class="badge">{{ tag.2 }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
You should then also define CSS styles with appropriate classes (tag-1 to tag-N,
|
||||||
|
where N matches ``TAG_CLOUD_STEPS``), tag-1 being the most frequent, and
|
||||||
|
define a ``ul.tagcloud`` class with appropriate list-style to create the cloud.
|
||||||
|
You should copy/paste this **badge** CSS rule ``ul.tagcloud .list-group-item <span>.badge``
|
||||||
|
if you're using ``TAG_CLOUD_BADGE`` setting. (this rule, potentially long , is suggested to avoid
|
||||||
|
conflicts with CSS libs as twitter Bootstrap)
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
|
ul.tagcloud {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.tagcloud li {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.tag-1 {
|
||||||
|
font-size: 150%;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.tag-2 {
|
||||||
|
font-size: 120%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ... add li.tag-3 etc, as much as needed */
|
||||||
|
|
||||||
|
ul.tagcloud .list-group-item span.badge {
|
||||||
|
background-color: grey;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
By default the tags in the cloud are sorted randomly, but if you prefers to have it alphabetically use the `alphabetically` (ascending) and `alphabetically-rev` (descending). Also is possible to sort the tags by it's size (number of articles with this specific tag) using the values `size` (ascending) and `size-rev` (descending).
|
2
tag_cloud/__init__.py
Normal file
2
tag_cloud/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .tag_cloud import *
|
||||||
|
|
90
tag_cloud/tag_cloud.py
Normal file
90
tag_cloud/tag_cloud.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
'''
|
||||||
|
tag_cloud
|
||||||
|
===================================
|
||||||
|
|
||||||
|
This plugin generates a tag cloud from available tags
|
||||||
|
'''
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
|
||||||
|
from pelican import signals
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def set_default_settings(settings):
|
||||||
|
settings.setdefault('TAG_CLOUD_STEPS', 4)
|
||||||
|
settings.setdefault('TAG_CLOUD_MAX_ITEMS', 100)
|
||||||
|
settings.setdefault('TAG_CLOUD_SORTING', 'random')
|
||||||
|
settings.setdefault('TAG_CLOUD_BADGE', False)
|
||||||
|
|
||||||
|
|
||||||
|
def init_default_config(pelican):
|
||||||
|
from pelican.settings import DEFAULT_CONFIG
|
||||||
|
set_default_settings(DEFAULT_CONFIG)
|
||||||
|
if(pelican):
|
||||||
|
set_default_settings(pelican.settings)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_tag_cloud(generator):
|
||||||
|
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 = tags[0]
|
||||||
|
min_count = tags[-1]
|
||||||
|
steps = generator.settings.get('TAG_CLOUD_STEPS')
|
||||||
|
|
||||||
|
# calculate word sizes
|
||||||
|
def generate_tag(tag, count):
|
||||||
|
tag = (
|
||||||
|
tag,
|
||||||
|
int(math.floor(steps - (steps - 1) * math.log(count - min_count + 1)
|
||||||
|
/ (math.log(max_count - min_count + 1) or 1)))
|
||||||
|
)
|
||||||
|
if generator.settings.get('TAG_CLOUD_BADGE'):
|
||||||
|
tag += (count,)
|
||||||
|
return tag
|
||||||
|
|
||||||
|
tag_cloud = [
|
||||||
|
generate_tag(tag, count)
|
||||||
|
for tag, count in tag_cloud
|
||||||
|
]
|
||||||
|
|
||||||
|
sorting = generator.settings.get('TAG_CLOUD_SORTING')
|
||||||
|
|
||||||
|
if sorting == 'alphabetically':
|
||||||
|
tag_cloud.sort(key=lambda elem: elem[0].name)
|
||||||
|
elif sorting == 'alphabetically-rev':
|
||||||
|
tag_cloud.sort(key=lambda elem: elem[0].name, reverse=True)
|
||||||
|
elif sorting == 'size':
|
||||||
|
tag_cloud.sort(key=lambda elem: elem[1])
|
||||||
|
elif sorting == 'size-rev':
|
||||||
|
tag_cloud.sort(key=lambda elem: elem[1], reverse=True)
|
||||||
|
elif sorting == 'random':
|
||||||
|
random.shuffle(tag_cloud)
|
||||||
|
else:
|
||||||
|
logger.warning("setting for TAG_CLOUD_SORTING not recognized: %s, "
|
||||||
|
"falling back to 'random'", sorting)
|
||||||
|
random.shuffle(tag_cloud)
|
||||||
|
|
||||||
|
# make available in context
|
||||||
|
generator.tag_cloud = tag_cloud
|
||||||
|
generator._update_context(['tag_cloud'])
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
signals.initialized.connect(init_default_config)
|
||||||
|
signals.article_generator_finalized.connect(generate_tag_cloud)
|
4
tag_cloud/test_data/article_1.md
Normal file
4
tag_cloud/test_data/article_1.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Title: Article1
|
||||||
|
tags: fun, pelican, plugins
|
||||||
|
|
||||||
|
content, yeah!
|
5
tag_cloud/test_data/article_2.md
Normal file
5
tag_cloud/test_data/article_2.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Title: Article2
|
||||||
|
tags: pelican, plugins, python
|
||||||
|
|
||||||
|
content2, yeah!
|
||||||
|
|
5
tag_cloud/test_data/article_3.md
Normal file
5
tag_cloud/test_data/article_3.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Title: Article3
|
||||||
|
tags: pelican, plugins
|
||||||
|
|
||||||
|
content3, yeah!
|
||||||
|
|
5
tag_cloud/test_data/article_4.md
Normal file
5
tag_cloud/test_data/article_4.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Title: Article4
|
||||||
|
tags: pelican, fun
|
||||||
|
|
||||||
|
content4, yeah!
|
||||||
|
|
5
tag_cloud/test_data/article_5.md
Normal file
5
tag_cloud/test_data/article_5.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Title: Article5
|
||||||
|
tags: plugins, pelican, fun
|
||||||
|
|
||||||
|
content5, yeah!
|
||||||
|
|
114
tag_cloud/test_tag_cloud.py
Normal file
114
tag_cloud/test_tag_cloud.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import unittest
|
||||||
|
import os
|
||||||
|
import six
|
||||||
|
import tag_cloud
|
||||||
|
from shutil import rmtree
|
||||||
|
from tempfile import mkdtemp
|
||||||
|
|
||||||
|
from pelican.generators import ArticlesGenerator
|
||||||
|
from pelican.tests.support import get_settings
|
||||||
|
from pelican.urlwrappers import Tag
|
||||||
|
|
||||||
|
CUR_DIR = os.path.dirname(__file__)
|
||||||
|
CONTENT_DIR = os.path.join(CUR_DIR, 'test_data')
|
||||||
|
|
||||||
|
|
||||||
|
class TestTagCloudGeneration(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.temp_path = mkdtemp(prefix='pelicantests.')
|
||||||
|
|
||||||
|
cls._settings = get_settings(filenames={})
|
||||||
|
cls._settings['DEFAULT_CATEGORY'] = 'Default'
|
||||||
|
cls._settings['DEFAULT_DATE'] = (1970, 1, 1)
|
||||||
|
cls._settings['READERS'] = {'asc': None}
|
||||||
|
cls._settings['CACHE_CONTENT'] = False
|
||||||
|
tag_cloud.set_default_settings(cls._settings)
|
||||||
|
|
||||||
|
context = cls._settings.copy()
|
||||||
|
context['generated_content'] = dict()
|
||||||
|
context['static_links'] = set()
|
||||||
|
cls.generator = ArticlesGenerator(
|
||||||
|
context=context, settings=cls._settings,
|
||||||
|
path=CONTENT_DIR, theme=cls._settings['THEME'], output_path=cls.temp_path)
|
||||||
|
cls.generator.generate_context()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
rmtree(cls.temp_path)
|
||||||
|
|
||||||
|
def test_tag_cloud_random(self):
|
||||||
|
self.generator.settings['TAG_CLOUD_STEPS'] = 10
|
||||||
|
self.generator.settings['TAG_CLOUD_BADGE'] = False
|
||||||
|
tag_cloud.generate_tag_cloud(self.generator)
|
||||||
|
expected = [
|
||||||
|
(Tag('pelican', self._settings), 1),
|
||||||
|
(Tag('plugins', self._settings), 2),
|
||||||
|
(Tag('fun', self._settings), 3),
|
||||||
|
(Tag('python', self._settings), 10)
|
||||||
|
]
|
||||||
|
six.assertCountEqual(self, self.generator.tag_cloud, expected)
|
||||||
|
|
||||||
|
def test_tag_cloud_badge(self):
|
||||||
|
self.generator.settings['TAG_CLOUD_STEPS'] = 10
|
||||||
|
self.generator.settings['TAG_CLOUD_BADGE'] = True
|
||||||
|
tag_cloud.generate_tag_cloud(self.generator)
|
||||||
|
expected = [
|
||||||
|
(Tag('pelican', self._settings), 1, 5),
|
||||||
|
(Tag('plugins', self._settings), 2, 4),
|
||||||
|
(Tag('fun', self._settings), 3, 3),
|
||||||
|
(Tag('python', self._settings), 10, 1)
|
||||||
|
]
|
||||||
|
six.assertCountEqual(self, self.generator.tag_cloud, expected)
|
||||||
|
|
||||||
|
def test_tag_cloud_alphabetical(self):
|
||||||
|
self.generator.settings['TAG_CLOUD_STEPS'] = 10
|
||||||
|
self.generator.settings['TAG_CLOUD_SORTING'] = 'alphabetically'
|
||||||
|
tag_cloud.generate_tag_cloud(self.generator)
|
||||||
|
expected = [
|
||||||
|
(Tag('fun', self._settings), 3),
|
||||||
|
(Tag('pelican', self._settings), 1),
|
||||||
|
(Tag('plugins', self._settings), 2),
|
||||||
|
(Tag('python', self._settings), 10)
|
||||||
|
]
|
||||||
|
self.assertEqual(self.generator.tag_cloud, expected)
|
||||||
|
|
||||||
|
def test_tag_cloud_alphabetical_rev(self):
|
||||||
|
self.generator.settings['TAG_CLOUD_STEPS'] = 10
|
||||||
|
self.generator.settings['TAG_CLOUD_SORTING'] = 'alphabetically-rev'
|
||||||
|
tag_cloud.generate_tag_cloud(self.generator)
|
||||||
|
expected = [
|
||||||
|
(Tag('python', self._settings), 10),
|
||||||
|
(Tag('plugins', self._settings), 2),
|
||||||
|
(Tag('pelican', self._settings), 1),
|
||||||
|
(Tag('fun', self._settings), 3)
|
||||||
|
]
|
||||||
|
self.assertEqual(self.generator.tag_cloud, expected)
|
||||||
|
|
||||||
|
def test_tag_cloud_size(self):
|
||||||
|
self.generator.settings['TAG_CLOUD_STEPS'] = 10
|
||||||
|
self.generator.settings['TAG_CLOUD_SORTING'] = 'size'
|
||||||
|
tag_cloud.generate_tag_cloud(self.generator)
|
||||||
|
expected = [
|
||||||
|
(Tag('pelican', self._settings), 1),
|
||||||
|
(Tag('plugins', self._settings), 2),
|
||||||
|
(Tag('fun', self._settings), 3),
|
||||||
|
(Tag('python', self._settings), 10)
|
||||||
|
]
|
||||||
|
self.assertEqual(self.generator.tag_cloud, expected)
|
||||||
|
|
||||||
|
def test_tag_cloud_size_rev(self):
|
||||||
|
self.generator.settings['TAG_CLOUD_STEPS'] = 10
|
||||||
|
self.generator.settings['TAG_CLOUD_SORTING'] = 'size-rev'
|
||||||
|
tag_cloud.generate_tag_cloud(self.generator)
|
||||||
|
expected = [
|
||||||
|
(Tag('python', self._settings), 10),
|
||||||
|
(Tag('fun', self._settings), 3),
|
||||||
|
(Tag('plugins', self._settings), 2),
|
||||||
|
(Tag('pelican', self._settings), 1)
|
||||||
|
]
|
||||||
|
self.assertEqual(self.generator.tag_cloud, expected)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
1
tipue_search/__init__.py
Normal file
1
tipue_search/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .tipue_search import *
|
111
tipue_search/tipue_search.py
Normal file
111
tipue_search/tipue_search.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Tipue Search
|
||||||
|
============
|
||||||
|
|
||||||
|
A Pelican plugin to serialize generated HTML to JSON
|
||||||
|
that can be used by jQuery plugin - Tipue Search.
|
||||||
|
|
||||||
|
Copyright (c) Talha Mansoor
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
import json
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from codecs import open
|
||||||
|
try:
|
||||||
|
from urlparse import urljoin
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
from pelican import signals
|
||||||
|
|
||||||
|
|
||||||
|
class Tipue_Search_JSON_Generator(object):
|
||||||
|
|
||||||
|
def __init__(self, context, settings, path, theme, output_path, *null):
|
||||||
|
|
||||||
|
self.output_path = output_path
|
||||||
|
self.context = context
|
||||||
|
self.siteurl = settings.get('SITEURL')
|
||||||
|
self.relative_urls = settings.get('RELATIVE_URLS')
|
||||||
|
self.tpages = settings.get('TEMPLATE_PAGES')
|
||||||
|
self.output_path = output_path
|
||||||
|
self.json_nodes = []
|
||||||
|
|
||||||
|
|
||||||
|
def create_json_node(self, page):
|
||||||
|
|
||||||
|
if getattr(page, 'status', 'published') != 'published':
|
||||||
|
return
|
||||||
|
|
||||||
|
soup_title = BeautifulSoup(page.title.replace(' ', ' '), 'html.parser')
|
||||||
|
page_title = soup_title.get_text(' ', strip=True).replace('“', '"').replace('”', '"').replace('’', "'").replace('^', '^')
|
||||||
|
|
||||||
|
soup_text = BeautifulSoup(page.content, 'html.parser')
|
||||||
|
page_text = soup_text.get_text(' ', strip=True).replace('“', '"').replace('”', '"').replace('’', "'").replace('¶', ' ').replace('^', '^')
|
||||||
|
page_text = ' '.join(page_text.split())
|
||||||
|
|
||||||
|
page_category = page.category.name if getattr(page, 'category', 'None') != 'None' else ''
|
||||||
|
|
||||||
|
page_url = '.'
|
||||||
|
if page.url:
|
||||||
|
page_url = page.url if self.relative_urls else (self.siteurl + '/' + page.url)
|
||||||
|
|
||||||
|
node = {'title': page_title,
|
||||||
|
'text': page_text,
|
||||||
|
'tags': page_category,
|
||||||
|
'url': page_url,
|
||||||
|
'loc': page_url} # changed from 'url' following http://blog.siphos.be/2015/08/updates-on-my-pelican-adventure/ (an update to Pelican made it not work, because the update (e.g., in the theme folder, static/tipuesearch/tipuesearch.js is looking for the 'loc' attribute.
|
||||||
|
|
||||||
|
self.json_nodes.append(node)
|
||||||
|
|
||||||
|
|
||||||
|
def create_tpage_node(self, srclink):
|
||||||
|
|
||||||
|
srcfile = open(os.path.join(self.output_path, self.tpages[srclink]), encoding='utf-8')
|
||||||
|
soup = BeautifulSoup(srcfile, 'html.parser')
|
||||||
|
page_title = soup.title.string if soup.title is not None else ''
|
||||||
|
page_text = soup.get_text()
|
||||||
|
|
||||||
|
# Should set default category?
|
||||||
|
page_category = ''
|
||||||
|
page_url = urljoin(self.siteurl, self.tpages[srclink])
|
||||||
|
|
||||||
|
node = {'title': page_title,
|
||||||
|
'text': page_text,
|
||||||
|
'tags': page_category,
|
||||||
|
'url': page_url}
|
||||||
|
|
||||||
|
self.json_nodes.append(node)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_output(self, writer):
|
||||||
|
path = os.path.join(self.output_path, 'tipuesearch_content.js')
|
||||||
|
|
||||||
|
pages = self.context['pages'] + self.context['articles']
|
||||||
|
|
||||||
|
for article in self.context['articles']:
|
||||||
|
pages += article.translations
|
||||||
|
|
||||||
|
for srclink in self.tpages:
|
||||||
|
self.create_tpage_node(srclink)
|
||||||
|
|
||||||
|
for page in pages:
|
||||||
|
self.create_json_node(page)
|
||||||
|
root_node = {'pages': self.json_nodes}
|
||||||
|
|
||||||
|
root_node_js = 'var tipuesearch = ' + json.dumps(root_node, separators=(',', ':'), ensure_ascii=False) + ';'
|
||||||
|
|
||||||
|
with open(path, 'w', encoding='utf-8') as fd:
|
||||||
|
fd.write(root_node_js)
|
||||||
|
|
||||||
|
|
||||||
|
def get_generators(generators):
|
||||||
|
return Tipue_Search_JSON_Generator
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
signals.get_generators.connect(get_generators)
|
Loading…
Reference in New Issue
Block a user