Начало
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