ultrabug / mkdocs-static-i18n

MkDocs i18n plugin using static translation markdown files
https://ultrabug.github.io/mkdocs-static-i18n/
MIT License
229 stars 38 forks source link

Custom language selector (non-mkdocs-material) #297

Open tombreit opened 6 months ago

tombreit commented 6 months ago

I would like to build a language selector in a custom (non-mkdocs-material) theme, but I do not know how to access the alternate language versions of a given page:

<!-- theme/base.html -->

i18n_page_locale: {{ i18n_page_locale }} 
i18n_languages:   {{ i18n_languages }}

i18n_page_locale gives me the expected language code for the current language version.

But the variable i18n_languages seem to be undefined.

How do I access the alternate language versions of a given page in a custom template?

ultrabug commented 6 months ago

Hello @tombreit I think you can take inspiration from : https://github.com/ultrabug/mkdocs-static-i18n/blob/main/mkdocs_static_i18n/custom_i18n_sitemap/sitemap.xml

tombreit commented 6 months ago

Hi @ultrabug, thanks for your prompt reply and the sitemap-hint! But in my case, there must be something wrong with my project setup - even i18n_alternates is not defined:

# requirements.txt

mkdocs                 1.5.3
mkdocs-static-i18n     1.2.2
# mkdocs.yml

site_name: Testsite
strict: true
plugins:
  - i18n:
      docs_structure: suffix
      languages:
        - locale: en
          default: true
          name: English
          build: true
        - locale: de
          name: Deutsch
          build: true
theme:
  name: null
  custom_dir: 'theme/'
# project layout
.
├── docs
│   ├── page1.de.md
│   └── page1.en.md
├── mkdocs.yml
├── theme
│   ├── base.html
│   ├── main.html
│   └── mkdocs_theme.yml
└── upload.sh
<!-- theme/base.html -->

i18n_page_locale:  {{ i18n_page_locale }} 
i18n_languages:    {{ i18n_languages }}
i18n_alternates:   {{ i18n_alternates.items() }}

mkdocs serve

  File "project/theme_csl/main.html", line 1, in top-level template code
    {% extends "base.html" %}
  File "project/theme_csl/base.html", line 86, in top-level template code
    {{ i18n_alternates.items() }}
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/jinja2/environment.py", line 485, in getattr
    return getattr(obj, attribute)
           ^^^^^^^^^^^^^^^^^^^^^^^
jinja2.exceptions.UndefinedError: 'i18n_alternates' is undefined

Do you have any other tips or ideas?

kamilkrzyskow commented 6 months ago

The i18n_alternates are set only in the template context: https://github.com/ultrabug/mkdocs-static-i18n/blob/0b492e11fa0cd8c716bb2e570a8fb4cc93765cf1/mkdocs_static_i18n/plugin.py#L145 For sitemap.xml and perhaps 404.html as well, as it's included in static_templates.

The page context has only those values: https://github.com/ultrabug/mkdocs-static-i18n/blob/0b492e11fa0cd8c716bb2e570a8fb4cc93765cf1/mkdocs_static_i18n/plugin.py#L182-L188

kamilkrzyskow commented 6 months ago

Based on this excerpt from the material theme, the templates can access the config: https://github.com/squidfunk/mkdocs-material/blob/cc78979185dfca30ad6657192174733f702d86f5/src/templates/base.html#L63-L65

So try:

{% if "i18n" in config.plugins %}
{% set i18n_alternates = config.plugins["i18n"].i18n_files_per_language  %}
{% else %}
{% set i18n_alternates = {} %}
{% endif %}

{{ i18n_alternates.items() }}

I haven't tested it ✌️

tombreit commented 6 months ago

Sorry, but I don't get it. You put me on the right track - but

<!-- mytheme/base.html -->

{% for locale, i18n_files in config.plugins["i18n"].i18n_files_per_language.items() %}
- locale:     {{ locale }}
  i18n_files: {{ i18n_files }}
{% endfor %}

only works for my secondary language pages - it does not list alternate de pages for en pages - but lists en and de pages for de pages:

# page: docs/index.en.md

- locale:     en
  i18n_files: [File(src_uri='index.en.md', dest_uri='index.html', name='index', url='./')]
# page: docs/index.de.md

- locale:     en
  i18n_files: [File(src_uri='index.en.md', dest_uri='index.html', name='index', url='./')]

- locale:     de
  i18n_files: [File(src_uri='index.de.md', dest_uri='de/index.html', name='index', url='de/')]

Hard to explain, so I have put together a demo page:

kamilkrzyskow commented 6 months ago

Didn't run the demo, your comment explained the situation quite well: https://github.com/ultrabug/mkdocs-static-i18n/blob/0b492e11fa0cd8c716bb2e570a8fb4cc93765cf1/mkdocs_static_i18n/plugin.py#L76-L78 It turned out that the i18n_files_per_language is a mapping that gets updated after each language build, and the sitemap uses the data from the last build to save the information, once all languages are built.


Going back to your OP:

I would like to build a language selector in a custom (non-mkdocs-material) theme, but I do not know how to access the alternate language versions of a given page:

As far as I know, the i18n plugin requires or assumes that the file tree is the same in each language, so every file path is the same too only the .en.md file suffix or /en/... folder prefix changes.

Given this information you don't need to access the files from other languages, you only need the language identifier. https://github.com/ultrabug/mkdocs-static-i18n/blob/0b492e11fa0cd8c716bb2e570a8fb4cc93765cf1/mkdocs_static_i18n/reconfigure.py#L85 https://github.com/ultrabug/mkdocs-static-i18n/blob/0b492e11fa0cd8c716bb2e570a8fb4cc93765cf1/mkdocs_static_i18n/reconfigure.py#L110-L114

You should be able to access the data with: config.plugins["i18n"].current_language get current language you have access to the current locale already:

context["i18n_file_locale"] = page.file.locale 
context["i18n_page_locale"] = self.current_language 

config.plugins["i18n"].build_languages loop over that to get alternates page.url | replace(current_language, alternate, 1) replace the current with the alternate once inside the url 🤔

accessing page might crash on 404.html, so add a safety check if that happens if page

tombreit commented 6 months ago

That would work if all pages exists in all configured languages. Looping over config.plugins["i18n"].all_languages seems to get same resuts.

But neither config.plugins["i18n"].build_languages nor config.plugins["i18n"].all_languages seem to return only the actually existing alternates. Instead these return all possible alternates according to the languages config key.

Given the following docs layout:

docs/
├── enonly.en.md
├── index.de.md
└── index.en.md

this would work for index.en.mdindex.de.md - but not for enonly.en.md.

Perhaps I implement this language switcher in Javascript;-) And thank you very much for taking the time to look at my issue.

kamilkrzyskow commented 6 months ago

Well, both build_languages and all_languages are global containers for the whole build, and aren't "resolved" for each page separately. I always use the https://ultrabug.github.io/mkdocs-static-i18n/setup/controlling-your-builds/#fallbacking-to-default setting, so I never even considered a non-translated page alternate as an issue 😅

Perhaps I implement this language switcher in Javascript;-)

So that after a page load, the JavaScript fetches the urls to check if the page is available and returns a status 200? I advise against this, as this would just create unnecessary requests.


This is the code that reconfigures the context and configures the alternates: https://github.com/ultrabug/mkdocs-static-i18n/blob/0b492e11fa0cd8c716bb2e570a8fb4cc93765cf1/mkdocs_static_i18n/reconfigure.py#L424-L442 There is the page.file.alternates list, but I don't think it will limit the alternates to only existent ones, still worth a try.


I would like to build a language selector in a custom (non-mkdocs-material) theme,

Also the Material theme uses the extra.alternate config, which later is modified by the plugin. So why not copy the logic from the material theme? The licence is MIT, so you can reuse it, just provide attribution.

tombreit commented 6 months ago

I already tried to hijack the language switcher from mkdocs-material. But, again, I'm unable to use the provided "integration" via reconfigure_material_theme. To me it looks like this functionality is strongly coupled with/for the material theme and it is not clear to me how I could use this functionality with my theme:

https://github.com/ultrabug/mkdocs-static-i18n/blob/0b492e11fa0cd8c716bb2e570a8fb4cc93765cf1/mkdocs_static_i18n/reconfigure.py#L126-L127

This integration actually sounds perfect and I would love to use it - but the static-i18n plugin only seems to provide/expose this for "mkdocs-material"?

kamilkrzyskow commented 6 months ago

True, my bad, I forgot about the config.theme.name validation. The i18n plugin makes the assumption that extra.alternate is only used by the mkdocs-material theme, which for now it was, and has merged the alternate handling with the reconfiguration logic.

The code responsible for the alternate deepcopy could be moved out into another function reconfigure_extra_alternate: https://github.com/ultrabug/mkdocs-static-i18n/blob/0b492e11fa0cd8c716bb2e570a8fb4cc93765cf1/mkdocs_static_i18n/reconfigure.py#L306-L356 and later it can be called inside the reconfigure_material_theme like before, and in an elif "alternate" in config.extra statement after that whole material validation: https://github.com/ultrabug/mkdocs-static-i18n/blob/0b492e11fa0cd8c716bb2e570a8fb4cc93765cf1/mkdocs_static_i18n/reconfigure.py#L127

I think it's a valid proposition to support that feature in other themes, one issue might be that other themes use extra.alternate in a different way from the material theme. You could do some market research with the more popular themes, and built-in themes, and make sure there won't be any conflict. if there is a conflict additional validation would be required for that extra.alternate to make sure it has valid values.

IMO that logic is not that coupled with material, that it can't be taken out 🤔However, I won't be developing anything for at least a week+ so can't make a PR myself. Once the market research is done ping ultrabug, maybe he'll have the time if you don't want to dabble with Python directly.