Compare commits
	
		
			12 Commits
		
	
	
		
			886221e821
			...
			a7a297d99d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						a7a297d99d
	
				 | 
					
					
						|||
| 
						
						
							
						
						a03f31e237
	
				 | 
					
					
						|||
| 
						
						
							
						
						65dce1b060
	
				 | 
					
					
						|||
| 
						
						
							
						
						5b2683b3a4
	
				 | 
					
					
						|||
| 
						
						
							
						
						43de32edd2
	
				 | 
					
					
						|||
| 
						
						
							
						
						263e1c5f20
	
				 | 
					
					
						|||
| 
						
						
							
						
						f1eba8b28a
	
				 | 
					
					
						|||
| 
						
						
							
						
						b9c070b6c8
	
				 | 
					
					
						|||
| 
						
						
							
						
						957c8e2ed1
	
				 | 
					
					
						|||
| 
						
						
							
						
						1eed4d7ca3
	
				 | 
					
					
						|||
| 
						
						
							
						
						ee6946373c
	
				 | 
					
					
						|||
| 
						
						
							
						
						30b1ce923f
	
				 | 
					
					
						
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,5 +1,4 @@
 | 
			
		||||
output
 | 
			
		||||
__pycache__
 | 
			
		||||
*.pid
 | 
			
		||||
plugins
 | 
			
		||||
cache
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							@@ -1,3 +1,9 @@
 | 
			
		||||
[submodule "themes/clean-blog"]
 | 
			
		||||
	path = themes/clean-blog
 | 
			
		||||
	url = ssh://gitea@git.epheme.re:2222/fmouhart/pelican-clean-blog.git
 | 
			
		||||
	url = https://git.epheme.re/fmouhart/pelican-clean-blog.git
 | 
			
		||||
[submodule "plugins/autopages"]
 | 
			
		||||
	path = plugins/autopages
 | 
			
		||||
	url = https://git.epheme.re/fmouhart/pelican-autopages.git
 | 
			
		||||
[submodule "plugins/i18n_subsites"]
 | 
			
		||||
	path = plugins/i18n_subsites
 | 
			
		||||
	url = https://git.epheme.re/fmouhart/pelican-i18n_subsites.git
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								Makefile
									
									
									
									
									
								
							@@ -12,7 +12,10 @@ publish:
 | 
			
		||||
clean:
 | 
			
		||||
	uv run invoke clean
 | 
			
		||||
 | 
			
		||||
init:
 | 
			
		||||
%.mo: %.po
 | 
			
		||||
	msgfmt "$^" -o "$@"
 | 
			
		||||
 | 
			
		||||
init: themes/clean-blog/translations/fr/LC_MESSAGES/messages.mo
 | 
			
		||||
	uv sync
 | 
			
		||||
 | 
			
		||||
.PHONY: clean build publish dev init
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
Title: wget/curl
 | 
			
		||||
Date: 2022-07-25 13:45 CEST
 | 
			
		||||
Author: Fabrice
 | 
			
		||||
Category: cheat sheets
 | 
			
		||||
Category: antisèches
 | 
			
		||||
Tags: wget, curl, cli
 | 
			
		||||
Slug: wget-curl
 | 
			
		||||
Header_Cover: ../images/covers/speedboat.jpg
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
---
 | 
			
		||||
Title: Setup Neovim with kickstart.nvim
 | 
			
		||||
Date: 2023-12-25 17:15
 | 
			
		||||
Modified: 2024-02-06 10:30
 | 
			
		||||
Modified: 2025-02-12 13:00
 | 
			
		||||
Lang: en
 | 
			
		||||
Author: Fabrice
 | 
			
		||||
Category: software
 | 
			
		||||
@@ -22,38 +22,38 @@ incrementally before it starts getting too big for me to find anything inside
 | 
			
		||||
it and not using even half of the plugins I installed. That goes without saying,
 | 
			
		||||
there were quite a bit on conflicting keymaps as well as I'm using
 | 
			
		||||
[bépo](http://bepo.fr/) as my keyboard layout with [partial remaps
 | 
			
		||||
(fr)](https://cdn.bepo.fr/Vim-bepo-066.png).
 | 
			
		||||
(fr)](https://cdn.bepo.fr/images/Vim-bepo-066.png).
 | 
			
		||||
 | 
			
		||||
Obviously, it slowly became quite a mess. To address this issue, I
 | 
			
		||||
decided to reorganise my `$HOME/.config/vim` directory using the [vim directory
 | 
			
		||||
structure](www.panozzaj.com/blog/2011/09/09/vim-directory-structure/) and did
 | 
			
		||||
some cleanup at this point of time. I think it was also around this period that
 | 
			
		||||
I discovered that Vim8 added a native package manager that I started to use.
 | 
			
		||||
Thus, at this point, I started organising my configuration with semantic files,
 | 
			
		||||
such as `$VIMHOME/plugin/spelling.vim` to manage my spelling configuration for
 | 
			
		||||
instance. This approach makes debugging easier, and also checking custom
 | 
			
		||||
keyboard shortcuts easier, as I just have to check
 | 
			
		||||
Obviously, it slowly became quite a mess. To address this issue, I decided to
 | 
			
		||||
reorganise my `$HOME/.config/vim` directory using the [vim directory
 | 
			
		||||
structure](https://www.panozzaj.com/blog/2011/09/09/vim-directory-structure/)
 | 
			
		||||
and did some cleanup at this point of time. I think it was also around this
 | 
			
		||||
period that I discovered that Vim8 added a native package manager that I started
 | 
			
		||||
to use. Thus, at this point, I started organising my configuration with semantic
 | 
			
		||||
files, such as `$VIMHOME/plugin/spelling.vim` to manage my spelling
 | 
			
		||||
configuration for instance. This approach makes debugging easier, and also
 | 
			
		||||
checking custom keyboard shortcuts easier, as I just have to check
 | 
			
		||||
`$VIMHOME/plugin/omnicomplete.vim` for instance to know which shortcuts I set up
 | 
			
		||||
when I'm still getting the habits of using them.
 | 
			
		||||
 | 
			
		||||
At some point of time, I moved to Neovim, and I simply moved my configuration
 | 
			
		||||
from Vim to Neovim and continue on adding more and more plugins on top of each
 | 
			
		||||
other depending on my hype, especially because the world of Neovim plugins
 | 
			
		||||
At some point of time, I moved to Neovim, and simply moved my configuration from
 | 
			
		||||
Vim to Neovim. All the while continuing adding more and more plugins on top of
 | 
			
		||||
each other depending on my hype, especially because the world of Neovim plugins
 | 
			
		||||
opened up to me. Needless to say that less than half of these plugins were put
 | 
			
		||||
into good use. Which leads to my first configuration big cleanup.
 | 
			
		||||
 | 
			
		||||
Six months ago, I wiped my frankenconfig, and started back from scratch in
 | 
			
		||||
Six months ago, I wiped my _frankenconfig_, and started back from scratch in
 | 
			
		||||
[lua](https://lua.org/about.html), with the same structural approach as
 | 
			
		||||
previously, but now wondering if the plugin would be useful or not. Since my
 | 
			
		||||
first time using Vim, there were some big changes in the vim ecosystem,
 | 
			
		||||
especially in language management with
 | 
			
		||||
[tree-sitter](https://tree-sitter.github.io/tree-sitter/) and
 | 
			
		||||
[lsp](https://en.wikipedia.org/wiki/Language_Server_Protocol). These two bring
 | 
			
		||||
into the environment a unified way to manage languages without having to depend
 | 
			
		||||
on language-specific plugins, henceforth I didn't need specific plugins to have
 | 
			
		||||
into the environment a unified way to manage languages without depending on
 | 
			
		||||
language-specific plugins. Henceforth, I didn't need specific plugins to have
 | 
			
		||||
nice syntax coloration for obscure languages anymore, or get frustrated with
 | 
			
		||||
[omnicomplete](https://vim.fandom.com/wiki/Omni_completion) which decided not to
 | 
			
		||||
work for some languages… While it's not an absolute rule (for instance, I'm
 | 
			
		||||
work only for some languages… While it's not an absolute rule (for instance, I'm
 | 
			
		||||
using [vimtex]({filename}./nvim-latex.md) for latex, which includes a more
 | 
			
		||||
accurate syntax coloring than tree-sitter). I also moved from the native vim way
 | 
			
		||||
of managing plugins to use [`Lazy`](https://github.com/folke/lazy.nvim) as a
 | 
			
		||||
@@ -70,10 +70,10 @@ However, I was unhappy with some of my configurations, and if I managed to have
 | 
			
		||||
something functional, there were many details that annoy me that stemmed for
 | 
			
		||||
some configuration I wrote some times ago and of course didn't document. This
 | 
			
		||||
leads us to today, where I just decided to use
 | 
			
		||||
[`kickstart.nvim`](https://github.com/nvim-lua/kickstart.nvim), which is a
 | 
			
		||||
well-documented vim starting configuration (it's not a distribution, it still
 | 
			
		||||
requires your input to obtain something that fits your needs), which was exactly
 | 
			
		||||
what I needed to start anew… but not fully from scratch.
 | 
			
		||||
[`kickstart.nvim`](https://github.com/nvim-lua/kickstart.nvim). It is a well
 | 
			
		||||
documented vim starting configuration (it's not a distribution, it still
 | 
			
		||||
requires your inputs to obtain something that fits your needs), which was
 | 
			
		||||
exactly what I needed to start anew… but not fully from scratch.
 | 
			
		||||
 | 
			
		||||
## The migration
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
---
 | 
			
		||||
Title: Typesetting with Typst 
 | 
			
		||||
Date: 2024-10-19 18:00
 | 
			
		||||
Modified: 2024-10-19 21:00
 | 
			
		||||
Modified: 2025-02-14 12:45
 | 
			
		||||
Lang: en
 | 
			
		||||
Author: Fabrice
 | 
			
		||||
Category: software
 | 
			
		||||
@@ -126,7 +126,7 @@ at the outset of the file.
 | 
			
		||||
```typst
 | 
			
		||||
#import "lettre.typ": *
 | 
			
		||||
 | 
			
		||||
#show: doc => lettre(
 | 
			
		||||
#show: doc => lettre.with(
 | 
			
		||||
  de: [
 | 
			
		||||
    Sender\
 | 
			
		||||
    Address
 | 
			
		||||
@@ -144,7 +144,6 @@ at the outset of the file.
 | 
			
		||||
  post: [
 | 
			
		||||
    post-letter (e.g., post-scriptum)
 | 
			
		||||
  ],
 | 
			
		||||
  doc
 | 
			
		||||
)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								plugins/autopages/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								plugins/autopages/LICENSE
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
Copyright (c) 2015, Magnetic Media Online, Inc.
 | 
			
		||||
All rights reserved.
 | 
			
		||||
 | 
			
		||||
Redistribution and use in source and binary forms, with or without
 | 
			
		||||
modification, are permitted provided that the following conditions are met:
 | 
			
		||||
 | 
			
		||||
1. Redistributions of source code must retain the above copyright notice,
 | 
			
		||||
   this list of conditions and the following disclaimer.
 | 
			
		||||
 | 
			
		||||
2. Redistributions in binary form must reproduce the above copyright notice,
 | 
			
		||||
   this list of conditions and the following disclaimer in the documentation
 | 
			
		||||
   and/or other materials provided with the distribution.
 | 
			
		||||
 | 
			
		||||
3. Neither the name of the copyright holder nor the names of its
 | 
			
		||||
   contributors may be used to endorse or promote products derived from this
 | 
			
		||||
   software without specific prior written permission.
 | 
			
		||||
 | 
			
		||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 | 
			
		||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 | 
			
		||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 | 
			
		||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 | 
			
		||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 | 
			
		||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 | 
			
		||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 | 
			
		||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 | 
			
		||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 | 
			
		||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 | 
			
		||||
POSSIBILITY OF SUCH DAMAGE.
 | 
			
		||||
							
								
								
									
										21
									
								
								plugins/autopages/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								plugins/autopages/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
# Auto Pages
 | 
			
		||||
 | 
			
		||||
This plugin adds an attribute `page` to the author, category, and tag
 | 
			
		||||
objects which can be used in templates by themes. The page is processed as
 | 
			
		||||
an ordinary Pelican page, so it can be Markdown, reStructuredText, etc.
 | 
			
		||||
 | 
			
		||||
## Configuration
 | 
			
		||||
 | 
			
		||||
| Setting              | Default      | Notes                                                                                                                                                               |
 | 
			
		||||
|----------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | 
			
		||||
| `AUTHOR_PAGE_PATH`   | `authors`    | The location, relative to the project root where author pages can be found. The filename of the author page minus the extension must match the Author's slug.       |
 | 
			
		||||
| `CATEGORY_PAGE_PATH` | `categories` | The location, relative to the project root where category pages can be found. The filename of the category page minus the extension must match the Category's slug. |
 | 
			
		||||
| `TAG_PAGE_PATH`      | `tags`       | The location, relative to the project root where tag pages can be found. The filename of the tag page minus the extension must match the Tag's slug.                |
 | 
			
		||||
 | 
			
		||||
## Template Variables
 | 
			
		||||
 | 
			
		||||
| Variable        | Notes                                |
 | 
			
		||||
|-----------------|--------------------------------------|
 | 
			
		||||
| `author.page`   | `Page` object for the author page.   |
 | 
			
		||||
| `category.page` | `Page` object for the category page. |
 | 
			
		||||
| `tag.page`      | `Page` object for the tag page.      |
 | 
			
		||||
							
								
								
									
										1
									
								
								plugins/autopages/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								plugins/autopages/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
from .autopages import *
 | 
			
		||||
							
								
								
									
										64
									
								
								plugins/autopages/autopages.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								plugins/autopages/autopages.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import os.path
 | 
			
		||||
 | 
			
		||||
from pelican import signals
 | 
			
		||||
from pelican.contents import Page
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger("autopages")
 | 
			
		||||
 | 
			
		||||
def yield_files(root):
 | 
			
		||||
    root = os.path.realpath(os.path.abspath(root))
 | 
			
		||||
    for dirpath, dirnames, filenames in os.walk(root):
 | 
			
		||||
        for dirname in list(dirnames):
 | 
			
		||||
            try:
 | 
			
		||||
                if dirname.startswith("."):
 | 
			
		||||
                    dirnames.remove(dirname)
 | 
			
		||||
            except IndexError:
 | 
			
		||||
                # duplicate already removed?
 | 
			
		||||
                pass
 | 
			
		||||
        for filename in filenames:
 | 
			
		||||
            if filename.startswith("."):
 | 
			
		||||
                continue
 | 
			
		||||
            yield os.path.join(dirpath, filename)
 | 
			
		||||
 | 
			
		||||
def make_page(readers, context, filename):
 | 
			
		||||
    base_path, filename = os.path.split(filename)
 | 
			
		||||
    page = readers.read_file(base_path, filename, Page, None, context)
 | 
			
		||||
    slug, _ = os.path.splitext(filename)
 | 
			
		||||
    return slug, page
 | 
			
		||||
 | 
			
		||||
def make_pages(readers, context, path):
 | 
			
		||||
    pages = {}
 | 
			
		||||
    for filename in yield_files(path):
 | 
			
		||||
        try:
 | 
			
		||||
            slug, page = make_page(readers, context, filename)
 | 
			
		||||
        except Exception:
 | 
			
		||||
            logger.exception("Could not make autopage for %r", filename)
 | 
			
		||||
            continue
 | 
			
		||||
        pages[slug] = page
 | 
			
		||||
    return pages
 | 
			
		||||
 | 
			
		||||
def create_autopages(article_generator):
 | 
			
		||||
    settings = article_generator.settings
 | 
			
		||||
    readers = article_generator.readers
 | 
			
		||||
    context = article_generator.context
 | 
			
		||||
 | 
			
		||||
    authors_path = settings.get("AUTHOR_PAGE_PATH", "authors")
 | 
			
		||||
    categories_path = settings.get("CATEGORY_PAGE_PATH", "categories")
 | 
			
		||||
    tags_path = settings.get("TAG_PAGE_PATH", "tags")
 | 
			
		||||
 | 
			
		||||
    author_pages = make_pages(readers, context, authors_path)
 | 
			
		||||
    category_pages = make_pages(readers, context, categories_path)
 | 
			
		||||
    tag_pages = make_pages(readers, context, tags_path)
 | 
			
		||||
 | 
			
		||||
    for author, _ in article_generator.authors:
 | 
			
		||||
        author.page = author_pages.get(author.slug, "")
 | 
			
		||||
    for category, _ in article_generator.categories:
 | 
			
		||||
        category.page = category_pages.get(category.slug, "")
 | 
			
		||||
    for tag in article_generator.tags:
 | 
			
		||||
        tag.page = tag_pages.get(tag.slug, "")
 | 
			
		||||
 | 
			
		||||
def register():
 | 
			
		||||
    signals.article_generator_finalized.connect(create_autopages)
 | 
			
		||||
							
								
								
									
										165
									
								
								plugins/i18n_subsites/README.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								plugins/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
									
								
								plugins/i18n_subsites/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								plugins/i18n_subsites/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
from .i18n_subsites import *
 | 
			
		||||
							
								
								
									
										462
									
								
								plugins/i18n_subsites/i18n_subsites.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										462
									
								
								plugins/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
									
								
								plugins/i18n_subsites/implementing_language_buttons.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								plugins/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
									
								
								plugins/i18n_subsites/localizing_using_jinja2.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								plugins/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,0 +1,7 @@
 | 
			
		||||
404 stránka
 | 
			
		||||
===========
 | 
			
		||||
:slug: 404
 | 
			
		||||
:lang: cz
 | 
			
		||||
:status: hidden
 | 
			
		||||
 | 
			
		||||
Jednoduchá 404 stránka.
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
Eine 404 Seite
 | 
			
		||||
==============
 | 
			
		||||
:slug: 404
 | 
			
		||||
:lang: de
 | 
			
		||||
:status: hidden
 | 
			
		||||
 | 
			
		||||
Eine einfache 404 Seite.
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -0,0 +1,2 @@
 | 
			
		||||
[jinja2: templates/**.html]
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								plugins/i18n_subsites/test_data/localized_theme/messages.pot
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								plugins/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"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										53
									
								
								plugins/i18n_subsites/test_data/pelicanconf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								plugins/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
									
								
								plugins/i18n_subsites/test_i18n_subsites.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								plugins/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))
 | 
			
		||||
							
								
								
									
										51
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								readme.md
									
									
									
									
									
								
							@@ -1 +1,52 @@
 | 
			
		||||
This repository contains the sources necessary to build the blog at:
 | 
			
		||||
<https://blog.epheme.re>
 | 
			
		||||
 | 
			
		||||
# Dependencies
 | 
			
		||||
 | 
			
		||||
To use this repository as intended, you need, to build the blog, the following
 | 
			
		||||
software:
 | 
			
		||||
 | 
			
		||||
- `git`
 | 
			
		||||
- `make`
 | 
			
		||||
- `uv`
 | 
			
		||||
- `gettext`
 | 
			
		||||
 | 
			
		||||
To synchronise the blog remotely with its intended target, the synchronisation
 | 
			
		||||
is done using `rsync` over `ssh`.
 | 
			
		||||
 | 
			
		||||
# Install
 | 
			
		||||
 | 
			
		||||
To install a local copy to work on this blog, you also need other components,
 | 
			
		||||
such as the [theme](https://git.epheme.re/fmouhart/pelican-clean-blog) and
 | 
			
		||||
[some](https://git.epheme.re/fmouhart/pelican-autopages)
 | 
			
		||||
[plugins](https://git.epheme.re/fmouhart/pelican-clean-blog). Those are embedded
 | 
			
		||||
in the repository as a git submodule. You can thus simply run git clone with the
 | 
			
		||||
`--recurse-submodule` option:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
git clone --recurse-submodule https://git.epheme.re/fmouhart/blog.git fmouhart-blog
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This blog relies on [pelican](https://getpelican.com) as a static site
 | 
			
		||||
generator. To manage the different python dependencies of this project, we are
 | 
			
		||||
using [`uv`](https://github.com/astral-sh/uv) as a python project manager.
 | 
			
		||||
 | 
			
		||||
Moreover, translations are managed with python `gettext` which requires
 | 
			
		||||
compiling the translation file.
 | 
			
		||||
 | 
			
		||||
Those two steps are performed with the following command:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
make init
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# Development
 | 
			
		||||
 | 
			
		||||
When writing an article, you can run the blog with `livereload` enabled with the
 | 
			
		||||
command:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
make dev
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
It’ll span a local development server on port `8000`: <http://localhost:8000>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user