jekyll-polyglot 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +161 -3
- data/lib/jekyll/polyglot/hooks/redirects.rb +79 -0
- data/lib/jekyll/polyglot/hooks.rb +1 -0
- data/lib/jekyll/polyglot/liquid/tags/i18n_headers.rb +62 -37
- data/lib/jekyll/polyglot/patches/jekyll/site.rb +116 -26
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c41fff580d3b7b67c65668954a77168ecf5017f5c0ca7b3747bdcc67ddb7d8ed
|
|
4
|
+
data.tar.gz: 6666fb42d87a589c695229be0386cdd601511aee2115c6da472f2de3b829c8ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 446131b28c107601f59f17e4d630c800c6c6b4c64290f00ecf417c407cfd1e3bb960f36a1994d84d7b438cd862d358216090556f198b89ab31a1f6043f842bc9
|
|
7
|
+
data.tar.gz: 4902c0e1a8de5060c667eade6806a3e3932eae52475c1a0ad1f4cb5fcb984086080e4c08bf7551eb923098c152e6a46ffe7d63475ec27f9acbe948bee48d8adb
|
data/README.md
CHANGED
|
@@ -28,7 +28,7 @@ In your `_config.yml` file, add the following preferences
|
|
|
28
28
|
```YAML
|
|
29
29
|
languages: ["en", "sv", "de", "fr"]
|
|
30
30
|
default_lang: "en"
|
|
31
|
-
exclude_from_localization: ["javascript", "images", "css", "public", "sitemap"]
|
|
31
|
+
exclude_from_localization: ["javascript", "images", "css", "public", "sitemap", "CNAME"]
|
|
32
32
|
parallel_localization: true
|
|
33
33
|
url: https://polyglot.untra.io
|
|
34
34
|
```
|
|
@@ -41,6 +41,16 @@ These configuration preferences indicate
|
|
|
41
41
|
|
|
42
42
|
The optional `lang_from_path: true` option enables getting the page language from a filepath segment seperated by `/` or `.`, e.g `de/first-one.md`, or `_posts/zh_HK/use-second-segment.md` , if the lang frontmatter isn't defined.
|
|
43
43
|
|
|
44
|
+
#### Netlify _redirects localization
|
|
45
|
+
If you are deploying to Netlify and use a `_redirects` file, you can enable automatic localization of redirects:
|
|
46
|
+
```yaml
|
|
47
|
+
localize_redirects: true
|
|
48
|
+
exclude_from_redirect_localization:
|
|
49
|
+
- /signin
|
|
50
|
+
- /app
|
|
51
|
+
```
|
|
52
|
+
See [Localizing Netlify _redirects](#localizing-netlify-_redirects) for more details.
|
|
53
|
+
|
|
44
54
|
## How To Use It
|
|
45
55
|
When adding new posts and pages, add to the YAML front matter:
|
|
46
56
|
```
|
|
@@ -59,8 +69,7 @@ _posts/2010-03-01-salad-recipes-sv.md
|
|
|
59
69
|
_posts/2010-03-01-salad-recipes-fr.md
|
|
60
70
|
```
|
|
61
71
|
|
|
62
|
-
Organized names will generate consistent permalinks when the post is rendered, and Polyglot will know to build separate language versions of
|
|
63
|
-
the website using only the files with the correct `lang` variable in the front matter.
|
|
72
|
+
Organized names will generate consistent permalinks when the post is rendered, and Polyglot will know to build separate language versions of the website using only the files with the correct `lang` variable in the front matter.
|
|
64
73
|
|
|
65
74
|
In short:
|
|
66
75
|
* Be consistent with how you name and place your *posts* files
|
|
@@ -83,6 +92,19 @@ Sample code for meta link generation:
|
|
|
83
92
|
```
|
|
84
93
|
|
|
85
94
|
|
|
95
|
+
#### Available and missing translations
|
|
96
|
+
_New in 1.13.0_
|
|
97
|
+
|
|
98
|
+
Polyglot exposes two arrays on every page describing its translation status:
|
|
99
|
+
|
|
100
|
+
- `page.available_languages` — language codes that have an actual translation of this page.
|
|
101
|
+
- `page.missing_languages` — languages that don't yet have a translation of this page.
|
|
102
|
+
|
|
103
|
+
`missing_languages` is intentionally empty for pages that have no per-language translations at all: a single-source page falls back to identical content for every visitor, so there is nothing missing to flag. Only pages that already have *at least one* translations report the gaps.
|
|
104
|
+
|
|
105
|
+
Combine with [`page.rendered_lang`](#detecting-fallback-pages-with-pagerendered_lang) to also flag fallback content on the page itself.
|
|
106
|
+
|
|
107
|
+
|
|
86
108
|
#### Using different permalinks per language
|
|
87
109
|
_New in 1.7.0_
|
|
88
110
|
|
|
@@ -114,12 +136,33 @@ Estos somos nosotros!
|
|
|
114
136
|
Additionally, if you are also using the `jekyll-redirect-from` plugin, pages coordinated this way will automatically have redirects created between pages.
|
|
115
137
|
So `/es/about` will automatically redirect to `/es/acerca-de` and `/acerca-de` can redirect to `/about`. If you use this approach, be sure to also employ a customized [redirect.html](https://github.com/untra/polyglot/blob/main/site/_layouts/redirect.html).
|
|
116
138
|
|
|
139
|
+
As of version 1.12, Polyglot also properly supports `redirect_from` frontmatter across sublanguages. When you add redirect paths to pages in non-default languages, Polyglot will correctly scope those redirects to each language's prefix, preventing duplicate redirects and ensuring proper routing.
|
|
140
|
+
|
|
117
141
|
#### Fallback Language Support
|
|
118
142
|
Lets say you are building your website. You have an `/about/` page written in *english*, *german* and
|
|
119
143
|
*swedish*. You are also supporting a *french* website, but you never designed a *french* version of your `/about/` page!
|
|
120
144
|
|
|
121
145
|
No worries. Polyglot ensures the sitemap of your *english* site matches your *french* site, matches your *swedish* and *german* sites too. In this case, because you specified a `default_lang` variable in your `_config.yml`, all sites missing their languages' counterparts will fallback to your `default_lang`, so content is preserved across different languages of your site.
|
|
122
146
|
|
|
147
|
+
#### Smart hreflang Generation
|
|
148
|
+
|
|
149
|
+
Polyglot only generates `hreflang` tags for languages that have actual translations. This improves SEO correctness by not advertising language alternatives that don't actually exist.
|
|
150
|
+
|
|
151
|
+
For example, if you have `/about.html` in English and Spanish but not French:
|
|
152
|
+
- The English page gets `hreflang="en"`, `hreflang="es"`, and `hreflang="x-default"`
|
|
153
|
+
- The Spanish page gets the same hreflang tags
|
|
154
|
+
- No `hreflang="fr"` is generated, even though a French fallback page exists
|
|
155
|
+
|
|
156
|
+
This behavior:
|
|
157
|
+
- Generates pages for all languages (fallback content is still served)
|
|
158
|
+
- Only advertises translations that actually exist via `hreflang` tags
|
|
159
|
+
- Always includes `hreflang` for the default language and `x-default`
|
|
160
|
+
|
|
161
|
+
Translation detection works via:
|
|
162
|
+
1. **page_id matching**: Documents with the same `page_id` frontmatter are considered translations
|
|
163
|
+
2. **permalink matching**: Documents with matching permalinks (and different `lang`) are considered translations
|
|
164
|
+
3. **Searches both collections and standalone pages**: The `{% I18n_Headers %}` tag searches `site.collections` and `site.pages`
|
|
165
|
+
|
|
123
166
|
### Relativized Local Urls
|
|
124
167
|
No need to meticulously manage anchor tags to link to your correct language. Polyglot modifies how pages get written to the site so your *french* links keep visitors on your *french* blog.
|
|
125
168
|
```md
|
|
@@ -158,6 +201,73 @@ becomes
|
|
|
158
201
|
<p>Cliquez <a href="https://mywebsite.com/fr/">ici</a> pour aller à l'entrée du site.
|
|
159
202
|
```
|
|
160
203
|
|
|
204
|
+
#### Canonical URL Handling
|
|
205
|
+
|
|
206
|
+
For proper canonical URL handling on multilingual sites, we recommend using Polyglot's `{% I18n_Headers %}` tag for canonical URLs instead of jekyll-seo-tag's default canonical output. This provides intelligent canonical URL generation that:
|
|
207
|
+
|
|
208
|
+
- Points to the translated URL for pages with actual translations
|
|
209
|
+
- Points to the default language URL for fallback pages (pages without translations)
|
|
210
|
+
- Properly handles the `page_id` and permalink matching for translation detection
|
|
211
|
+
|
|
212
|
+
**Setup with jekyll-seo-tag:**
|
|
213
|
+
|
|
214
|
+
If you're using [jekyll-seo-tag](https://github.com/jekyll/jekyll-seo-tag), you can disable its canonical output and let Polyglot handle it:
|
|
215
|
+
|
|
216
|
+
```liquid
|
|
217
|
+
{% seo canonical=false %}
|
|
218
|
+
{% I18n_Headers %}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The `canonical=false` option is available in jekyll-seo-tag v2.9.0+
|
|
222
|
+
|
|
223
|
+
**Fallback Canonical Behavior:**
|
|
224
|
+
|
|
225
|
+
To have fallback pages (pages without translations) point their canonical URL to the default language version, add to your `_config.yml`:
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
fallback_canonical_to_default_lang: true
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
With this option enabled:
|
|
232
|
+
- Pages with actual translations: canonical points to the translated URL (e.g., `/es/sobre-nosotros/`)
|
|
233
|
+
- Fallback pages (no translation): canonical points to the default language URL (e.g., `/about/` instead of `/es/about/`)
|
|
234
|
+
|
|
235
|
+
This improves SEO by:
|
|
236
|
+
- Preventing search engines from indexing duplicate fallback content under multiple language URLs
|
|
237
|
+
- Consolidating SEO authority to the original content
|
|
238
|
+
- Signaling to search engines which version is the authoritative source
|
|
239
|
+
|
|
240
|
+
Note: `hreflang` URLs pointing to the default language or `x-default` are intentionally NOT relativized, as they should always point to the canonical language-specific URLs.
|
|
241
|
+
|
|
242
|
+
### Localizing Netlify _redirects
|
|
243
|
+
_New in 1.13.0_
|
|
244
|
+
|
|
245
|
+
When using Polyglot with [Netlify](https://www.netlify.com/), redirect rules defined in a [Netlify `_redirects` file](https://docs.netlify.com/manage/routing/redirects/overview/#syntax-for-the-_redirects-file) will get relativized (e.g., `/github` becomes `/fr/github` on French pages). However the Netlify `_redirects` file only contains the redirect base paths, which causes 404 errors for localized URLs.
|
|
246
|
+
|
|
247
|
+
Polyglot can automatically generate language-prefixed versions of your redirects. Enable this feature in your `_config.yml`:
|
|
248
|
+
|
|
249
|
+
```yaml
|
|
250
|
+
localize_redirects: true
|
|
251
|
+
exclude_from_redirect_localization:
|
|
252
|
+
- /signin
|
|
253
|
+
- /app
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
With this configuration, a redirect like:
|
|
257
|
+
```
|
|
258
|
+
/github https://github.com/org/repo 302
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Will automatically generate localized versions for all your configured languages:
|
|
262
|
+
```
|
|
263
|
+
/github https://github.com/org/repo 302
|
|
264
|
+
/fr/github https://github.com/org/repo 302
|
|
265
|
+
/de/github https://github.com/org/repo 302
|
|
266
|
+
/sv/github https://github.com/org/repo 302
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Paths listed in `exclude_from_redirect_localization` will not be localized, which is useful for authentication endpoints or app URLs that should only exist at the root level.
|
|
270
|
+
|
|
161
271
|
### Disabling Url Relativizing
|
|
162
272
|
_New in 1.4.0_
|
|
163
273
|
If you dont want a href attribute to be relativized (such as for making [a language switcher](https://github.com/untra/polyglot/blob/main/site/_includes/sidebar.html#L40)), you can use the block tag:
|
|
@@ -260,11 +370,50 @@ This plugin stands out from other I18n Jekyll plugins.
|
|
|
260
370
|
- provides the liquid tag `{{ site.languages }}` to get an array of your I18n strings.
|
|
261
371
|
- provides the liquid tag `{{ site.default_lang }}` to get the default_lang I18n string.
|
|
262
372
|
- provides the liquid tag `{{ site.active_lang }}` to get the I18n language string the website was built for. Alternative names for `active_lang` can be configured via `config.lang_vars`.
|
|
373
|
+
- provides the liquid tag `{{ page.rendered_lang }}` to get the language the page content is actually rendered in (useful for detecting fallback pages).
|
|
374
|
+
- provides the liquid tag `{{ page.available_languages }}` to get the array of language codes a page has been translated into.
|
|
375
|
+
- provides the liquid tag `{{ page.missing_languages }}` to get the array of configured languages a page has not been translated into (empty when the page has no real translations and falls back identically everywhere).
|
|
263
376
|
- provides the liquid tag `{{ I18n_Headers }}` to append SEO bonuses to your website.
|
|
264
377
|
- provides the liquid tag `{{ Unrelativized_Link href="/hello" }}` to make urls that do not get influenced by url correction regexes.
|
|
265
378
|
- provides `site.data` localization for efficient rich text replacement.
|
|
266
379
|
- a creator that will answer all of your questions and issues.
|
|
267
380
|
|
|
381
|
+
### Detecting Fallback Pages with `page.rendered_lang`
|
|
382
|
+
|
|
383
|
+
The `page.rendered_lang` variable indicates the actual language of a page's content. This is different from `site.active_lang`, which indicates the language version of the site currently being built.
|
|
384
|
+
|
|
385
|
+
- `site.active_lang`: The language the site is being built for (e.g., `es` for the Spanish site)
|
|
386
|
+
- `page.rendered_lang`: The language of the page's actual content (e.g., `en` if no Spanish translation exists)
|
|
387
|
+
|
|
388
|
+
When `page.rendered_lang != site.active_lang`, the page is a **fallback page** - it's being served in the default language because no translation exists.
|
|
389
|
+
|
|
390
|
+
**Example: Showing a "not translated" notice:**
|
|
391
|
+
```liquid
|
|
392
|
+
{% if page.rendered_lang != site.active_lang %}
|
|
393
|
+
<div class="translation-notice">
|
|
394
|
+
This page is not yet available in {{ site.active_lang }}.
|
|
395
|
+
Showing {{ page.rendered_lang }} version.
|
|
396
|
+
</div>
|
|
397
|
+
{% endif %}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Example: Conditional content based on translation status:**
|
|
401
|
+
```liquid
|
|
402
|
+
{% if page.rendered_lang == site.active_lang %}
|
|
403
|
+
<!-- This is an actual translation -->
|
|
404
|
+
<p>Welcome to our {{ site.active_lang }} content!</p>
|
|
405
|
+
{% else %}
|
|
406
|
+
<!-- This is fallback content -->
|
|
407
|
+
<p>Content available in {{ page.rendered_lang }} only.</p>
|
|
408
|
+
{% endif %}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
This is useful for:
|
|
412
|
+
- Displaying notices when content hasn't been translated
|
|
413
|
+
- Tracking translation coverage
|
|
414
|
+
- Applying different styling to fallback pages
|
|
415
|
+
- Building translation status dashboards
|
|
416
|
+
|
|
268
417
|
## SEO Recipes
|
|
269
418
|
Jekyll-polyglot has a few spectacular [Search Engine Optimization techniques](https://untra.github.io/polyglot/seo) to ensure your Jekyll blog gets the most out of its multilingual audience. Check them out!
|
|
270
419
|
|
|
@@ -310,6 +459,10 @@ These are talented and considerate software developers across the world that hav
|
|
|
310
459
|
* [@obfusk](https://github.com/obfusk) [1.5.0](https://polyglot.untra.io/2021/07/17/polyglot-1.5.0/)
|
|
311
460
|
* [@eighthave](https://github.com/eighthave) [1.5.0](https://polyglot.untra.io/2021/07/17/polyglot-1.5.0/)
|
|
312
461
|
* [@george-gca](https://github.com/george-gca) [pt-BR support](https://polyglot.untra.io/pt-BR/2024/02/29/localized-variables.md)
|
|
462
|
+
* [@PanderMusubi](https://github.com/PanderMusubi) - 1.12 / jekyll-minimal-mistakes-polyglot demo
|
|
463
|
+
* [@GruberMarkus](https://github.com/GruberMarkus) - redirect anchor support
|
|
464
|
+
* [@rathboma](https://github.com/rathboma) - page.rendered_lang / sublanguage redirects
|
|
465
|
+
* [@manabu-nakamura](https://github.com/manabu-nakamura) - Japanese strings
|
|
313
466
|
|
|
314
467
|
### Other Websites Built with Polyglot
|
|
315
468
|
Feel free to open a PR and list your multilingual blog here you may want to share:
|
|
@@ -328,6 +481,7 @@ Feel free to open a PR and list your multilingual blog here you may want to shar
|
|
|
328
481
|
* [AnotherTurret just another study note blog](https://aturret.space/)
|
|
329
482
|
* [Diciotech is a collaborative online tech dictionary](https://diciotech.netlify.app/)
|
|
330
483
|
* [Yunseo Kim's Study Notes](https://www.yunseo.kim/)
|
|
484
|
+
* [Beekeeper Studio](https://www.beekeeperstudio.io/)
|
|
331
485
|
|
|
332
486
|
## 2.0 Roadmap
|
|
333
487
|
* [x] - **site language**: portuguese Brazil `pt-BR`
|
|
@@ -338,6 +492,10 @@ Feel free to open a PR and list your multilingual blog here you may want to shar
|
|
|
338
492
|
* [x] - **site language**: korean `ko`
|
|
339
493
|
* [x] - **site language**: hebrew `he`
|
|
340
494
|
* [x] - **site language**: chinese China `zh-CN`
|
|
495
|
+
* [x] - **site language**: italian `it`
|
|
496
|
+
* [x] - **site language**: turkish `tk`
|
|
497
|
+
* [x] - **site language**: ukrainian `uk`
|
|
498
|
+
* [x] - **site language**: hindi `hi`
|
|
341
499
|
* [ ] - **site language**: chinese Taiwan `zh-TW`
|
|
342
500
|
* [ ] - **site language**: portuguese Portugal `pt-PT`
|
|
343
501
|
* [ ] - get whitelisted as an official github-pages jekyll plugin
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Hook to localize Netlify _redirects file for multilingual sites.
|
|
4
|
+
# When enabled, generates language-prefixed versions of each redirect.
|
|
5
|
+
#
|
|
6
|
+
# Configuration:
|
|
7
|
+
# localize_redirects: true # Enable the feature
|
|
8
|
+
# exclude_from_redirect_localization: # Optional: paths to skip
|
|
9
|
+
# - /signin
|
|
10
|
+
# - /app
|
|
11
|
+
#
|
|
12
|
+
# Example:
|
|
13
|
+
# Input: /github https://github.com/org/repo 302
|
|
14
|
+
# Output: /github https://github.com/org/repo 302
|
|
15
|
+
# /es/github https://github.com/org/repo 302
|
|
16
|
+
# /de/github https://github.com/org/repo 302
|
|
17
|
+
# ...
|
|
18
|
+
|
|
19
|
+
Jekyll::Hooks.register :polyglot, :post_write do |site|
|
|
20
|
+
hook_redirects(site)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def hook_redirects(site)
|
|
24
|
+
return unless site.config.fetch('localize_redirects', false)
|
|
25
|
+
|
|
26
|
+
redirects_path = File.join(site.source, '_redirects')
|
|
27
|
+
return unless File.exist?(redirects_path)
|
|
28
|
+
|
|
29
|
+
exclusions = site.config.fetch('exclude_from_redirect_localization', [])
|
|
30
|
+
lines = File.readlines(redirects_path)
|
|
31
|
+
localized_lines = []
|
|
32
|
+
|
|
33
|
+
lines.each do |line|
|
|
34
|
+
# Always include the original line
|
|
35
|
+
localized_lines << line
|
|
36
|
+
|
|
37
|
+
# Skip comments and empty lines
|
|
38
|
+
stripped = line.strip
|
|
39
|
+
next if stripped.empty? || stripped.start_with?('#')
|
|
40
|
+
|
|
41
|
+
# Parse the redirect line: /source /target [status_code]
|
|
42
|
+
parts = stripped.split(/\s+/)
|
|
43
|
+
next if parts.length < 2
|
|
44
|
+
|
|
45
|
+
source = parts[0]
|
|
46
|
+
|
|
47
|
+
# Skip if source is in exclusion list
|
|
48
|
+
next if exclusions.include?(source)
|
|
49
|
+
|
|
50
|
+
# Only process paths that start with /
|
|
51
|
+
next unless source.start_with?('/')
|
|
52
|
+
|
|
53
|
+
# Skip if source already has a language prefix
|
|
54
|
+
next if site.languages.any? { |lang| source.start_with?("/#{lang}/") || source == "/#{lang}" }
|
|
55
|
+
|
|
56
|
+
# Add localized versions for non-default languages
|
|
57
|
+
site.languages.each do |lang|
|
|
58
|
+
next if lang == site.default_lang
|
|
59
|
+
|
|
60
|
+
localized_source = "/#{lang}#{source}"
|
|
61
|
+
destination = parts[1]
|
|
62
|
+
|
|
63
|
+
# Localize destination if it's an internal path (starts with /)
|
|
64
|
+
# but not if it's an external URL (contains ://)
|
|
65
|
+
localized_destination = if destination.start_with?('/') && !destination.include?('://')
|
|
66
|
+
"/#{lang}#{destination}"
|
|
67
|
+
else
|
|
68
|
+
destination
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
rest = parts.length > 2 ? " #{parts[2..].join(' ')}" : ''
|
|
72
|
+
localized_lines << "#{localized_source} #{localized_destination}#{rest}\n"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Write to destination
|
|
77
|
+
dest_path = File.join(site.dest, '_redirects')
|
|
78
|
+
File.write(dest_path, localized_lines.join)
|
|
79
|
+
end
|
|
@@ -12,53 +12,78 @@ module Jekyll
|
|
|
12
12
|
def render(context)
|
|
13
13
|
site = context.registers[:site]
|
|
14
14
|
page = context.registers[:page]
|
|
15
|
-
permalink = page['permalink'] || page['url'] || ''
|
|
16
|
-
|
|
17
|
-
page_id = page['page_id']
|
|
15
|
+
permalink = normalize_permalink(page['permalink'] || page['url'] || '')
|
|
16
|
+
normalized_permalink = strip_lang_prefix(permalink, site.active_lang)
|
|
18
17
|
permalink_lang = page['permalink_lang']
|
|
18
|
+
site_url = resolve_site_url(site)
|
|
19
|
+
|
|
20
|
+
lang_to_permalink = build_lang_to_permalink(site, page['page_id'], normalized_permalink)
|
|
21
|
+
|
|
22
|
+
canonical_tag(site, site_url, lang_to_permalink, permalink_lang, normalized_permalink) +
|
|
23
|
+
hreflang_tags(site, site_url, lang_to_permalink, permalink_lang, normalized_permalink)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def normalize_permalink(permalink)
|
|
29
|
+
permalink.start_with?('/') ? permalink : "/#{permalink}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def strip_lang_prefix(permalink, active_lang)
|
|
33
|
+
stripped = permalink.delete_prefix("/#{active_lang}/")
|
|
34
|
+
stripped.start_with?('/') ? stripped : "/#{stripped}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def resolve_site_url(site)
|
|
38
|
+
return @url unless @url.empty?
|
|
39
|
+
|
|
19
40
|
baseurl = site.config['baseurl'] || ''
|
|
20
|
-
|
|
21
|
-
|
|
41
|
+
site.config['url'] + baseurl
|
|
42
|
+
end
|
|
22
43
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.filter { |doc| !doc.data['page_id'].nil? }
|
|
27
|
-
.select { |doc| doc.data['page_id'] == page_id }
|
|
44
|
+
def build_lang_to_permalink(site, page_id, normalized_permalink)
|
|
45
|
+
site.find_translations(page_id, normalized_permalink)
|
|
46
|
+
end
|
|
28
47
|
|
|
29
|
-
|
|
30
|
-
lang_to_permalink
|
|
48
|
+
def lookup_permalink(lang_to_permalink, permalink_lang, lang)
|
|
49
|
+
lang_to_permalink[lang] || (permalink_lang && permalink_lang[lang])
|
|
50
|
+
end
|
|
31
51
|
|
|
32
|
-
|
|
52
|
+
def with_lang_prefix(permalink, lang)
|
|
53
|
+
permalink.start_with?("/#{lang}/") ? permalink : "/#{lang}#{permalink}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def canonical_tag(site, site_url, lang_to_permalink, permalink_lang, normalized_permalink)
|
|
33
57
|
current_lang = site.active_lang
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
58
|
+
has_translation = lookup_permalink(lang_to_permalink, permalink_lang, current_lang)
|
|
59
|
+
use_default = site.fallback_canonical_to_default_lang && !has_translation && current_lang != site.default_lang
|
|
60
|
+
|
|
61
|
+
canonical = if use_default
|
|
62
|
+
normalize_permalink(lookup_permalink(lang_to_permalink, permalink_lang, site.default_lang) || normalized_permalink)
|
|
63
|
+
elsif current_lang == site.default_lang
|
|
64
|
+
normalize_permalink(lookup_permalink(lang_to_permalink, permalink_lang, current_lang) || normalized_permalink)
|
|
39
65
|
else
|
|
40
|
-
|
|
66
|
+
current = normalize_permalink(lookup_permalink(lang_to_permalink, permalink_lang, current_lang) || normalized_permalink)
|
|
67
|
+
with_lang_prefix(current, current_lang)
|
|
41
68
|
end
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
site.languages.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
"<link rel=\"canonical\" href=\"#{site_url}#{canonical}\"/>\n"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def hreflang_tags(site, site_url, lang_to_permalink, permalink_lang, normalized_permalink)
|
|
73
|
+
default_permalink = normalize_permalink(lookup_permalink(lang_to_permalink, permalink_lang, site.default_lang) || normalized_permalink)
|
|
74
|
+
|
|
75
|
+
site.languages.map do |lang|
|
|
76
|
+
has_translation = lookup_permalink(lang_to_permalink, permalink_lang, lang)
|
|
77
|
+
next nil if !has_translation && lang != site.default_lang
|
|
78
|
+
|
|
79
|
+
alt = normalize_permalink(lookup_permalink(lang_to_permalink, permalink_lang, lang) || normalized_permalink)
|
|
80
|
+
if lang == site.default_lang
|
|
81
|
+
"<link rel=\"alternate\" hreflang=\"#{lang}\" href=\"#{site_url}#{alt}\"/>\n" \
|
|
82
|
+
"<link rel=\"alternate\" hreflang=\"x-default\" href=\"#{site_url}#{default_permalink}\"/>\n"
|
|
54
83
|
else
|
|
55
|
-
|
|
56
|
-
# Don't add the language prefix if it's already in the permalink
|
|
57
|
-
lang_permalink = alt_permalink.start_with?("/#{lang}/") ? alt_permalink : "/#{lang}#{alt_permalink}"
|
|
58
|
-
"<link rel=\"alternate\" hreflang=\"#{lang}\" href=\"#{site_url}#{lang_permalink}\"/>\n"
|
|
84
|
+
"<link rel=\"alternate\" hreflang=\"#{lang}\" href=\"#{site_url}#{with_lang_prefix(alt, lang)}\"/>\n"
|
|
59
85
|
end
|
|
60
|
-
end
|
|
61
|
-
i18n
|
|
86
|
+
end.compact.join
|
|
62
87
|
end
|
|
63
88
|
end
|
|
64
89
|
end
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
require 'English'
|
|
2
1
|
require 'etc'
|
|
3
2
|
|
|
4
3
|
include Process
|
|
5
4
|
module Jekyll
|
|
6
5
|
class Site
|
|
7
|
-
attr_reader :default_lang, :languages, :exclude_from_localization, :lang_vars, :lang_from_path
|
|
6
|
+
attr_reader :default_lang, :languages, :exclude_from_localization, :lang_vars, :lang_from_path, :fallback_canonical_to_default_lang
|
|
8
7
|
attr_accessor :file_langs, :active_lang
|
|
9
8
|
|
|
10
9
|
def prepare
|
|
@@ -12,6 +11,7 @@ module Jekyll
|
|
|
12
11
|
fetch_languages
|
|
13
12
|
@parallel_localization = config.fetch('parallel_localization', true)
|
|
14
13
|
@lang_from_path = config.fetch('lang_from_path', false)
|
|
14
|
+
@fallback_canonical_to_default_lang = config.fetch('fallback_canonical_to_default_lang', false)
|
|
15
15
|
@exclude_from_localization = config.fetch('exclude_from_localization', []).map do |e|
|
|
16
16
|
if File.directory?(e) && e[-1] != '/'
|
|
17
17
|
"#{e}/"
|
|
@@ -47,7 +47,7 @@ module Jekyll
|
|
|
47
47
|
next unless waitpid pid, Process::WNOHANG
|
|
48
48
|
|
|
49
49
|
pids.delete pid_lang
|
|
50
|
-
raise "Polyglot subprocess #{pid} (#{
|
|
50
|
+
raise "Polyglot subprocess #{pid} (#{pid_lang}) failed (#{$?.exitstatus})" unless $?.success?
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
end
|
|
@@ -131,11 +131,9 @@ module Jekyll
|
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
segments = split_on_multiple_delimiters(doc.path)
|
|
134
|
-
# loop through all segments and check if they match the language regex
|
|
135
134
|
segments.each do |segment|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
end
|
|
135
|
+
match = @languages.find { |lang| lang.downcase == segment.downcase }
|
|
136
|
+
return match if match
|
|
139
137
|
end
|
|
140
138
|
|
|
141
139
|
nil
|
|
@@ -148,12 +146,38 @@ module Jekyll
|
|
|
148
146
|
def coordinate_documents(docs)
|
|
149
147
|
regex = document_url_regex
|
|
150
148
|
approved = {}
|
|
149
|
+
# Build set of valid languages (default + configured)
|
|
150
|
+
valid_languages = ([@default_lang] + @languages).uniq
|
|
151
|
+
|
|
151
152
|
docs.each do |doc|
|
|
152
|
-
|
|
153
|
+
# Get the explicitly declared language (frontmatter or path-derived)
|
|
154
|
+
explicit_lang = doc.data['lang'] || derive_lang_from_path(doc)
|
|
155
|
+
lang = explicit_lang || @default_lang
|
|
156
|
+
|
|
157
|
+
# FILTER: Skip documents whose explicit lang is not in configured languages.
|
|
158
|
+
# Check the explicit value (not the fallback) so that documents with an
|
|
159
|
+
# unconfigured lang like 'de' are excluded even if normalization would
|
|
160
|
+
# map them to default_lang. Compare case-insensitively so case-mismatched
|
|
161
|
+
# frontmatter (e.g. 'pt-br' vs configured 'pt-BR') is normalized below
|
|
162
|
+
# rather than rejected here.
|
|
163
|
+
if explicit_lang && valid_languages.none? { |l| l.downcase == explicit_lang.downcase }
|
|
164
|
+
Jekyll.logger.warn "Polyglot:", "Skipping #{doc.relative_path} - lang '#{explicit_lang}' not in configured languages #{valid_languages.inspect}"
|
|
165
|
+
next
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# If the doc lang matches a config language case-insensitively, use the config case
|
|
169
|
+
config_lang = @languages.find { |l| l.downcase == lang.downcase }
|
|
170
|
+
lang = config_lang if config_lang
|
|
171
|
+
doc.data['lang'] = lang if doc.data['lang'] && config_lang
|
|
172
|
+
|
|
153
173
|
lang_exclusive = doc.data['lang-exclusive'] || []
|
|
174
|
+
|
|
154
175
|
url = doc.url.gsub(regex, '/')
|
|
155
176
|
page_id = doc.data['page_id'] || url
|
|
156
177
|
doc.data['permalink'] = url if doc.data['permalink'].to_s.empty? && !doc.data['lang'].to_s.empty?
|
|
178
|
+
# Set rendered_lang to indicate what language this page is actually rendered in
|
|
179
|
+
# This allows templates to detect fallback pages (rendered_lang != active_lang)
|
|
180
|
+
doc.data['rendered_lang'] = lang
|
|
157
181
|
|
|
158
182
|
# skip entirely if nothing to check
|
|
159
183
|
next if @file_langs.nil?
|
|
@@ -175,38 +199,102 @@ module Jekyll
|
|
|
175
199
|
end
|
|
176
200
|
|
|
177
201
|
def assignPageRedirects(doc, docs)
|
|
202
|
+
# Preserve and normalize user-defined redirect_from
|
|
203
|
+
user_redirects = doc.data['redirect_from'] || []
|
|
204
|
+
user_redirects = [user_redirects] unless user_redirects.is_a?(Array)
|
|
205
|
+
|
|
206
|
+
# Determine document language
|
|
207
|
+
doc_lang = doc.data['lang'] || derive_lang_from_path(doc) || @default_lang
|
|
208
|
+
|
|
209
|
+
# Scope user-defined redirects to document's language if non-default
|
|
210
|
+
if doc_lang != @default_lang && !user_redirects.empty?
|
|
211
|
+
user_redirects = user_redirects.map do |redirect_path|
|
|
212
|
+
# Normalize path to start with /
|
|
213
|
+
redirect_path = "/#{redirect_path}" unless redirect_path.start_with?('/')
|
|
214
|
+
# Only prefix if not already prefixed with this language
|
|
215
|
+
if redirect_path.start_with?("/#{doc_lang}/")
|
|
216
|
+
redirect_path
|
|
217
|
+
else
|
|
218
|
+
"/#{doc_lang}#{redirect_path}"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Compute page_id based redirects (cross-language)
|
|
224
|
+
computed_redirects = []
|
|
178
225
|
pageId = doc.data['page_id']
|
|
179
226
|
if !pageId.nil? && !pageId.empty?
|
|
180
|
-
redirects = []
|
|
181
|
-
|
|
182
227
|
docs_with_same_id = docs.select { |dd| dd.data['page_id'] == pageId }
|
|
183
|
-
|
|
184
|
-
# For each document with the same page_id
|
|
185
228
|
docs_with_same_id.each do |dd|
|
|
186
|
-
# Add redirect if it's a different permalink
|
|
187
229
|
if dd.data['permalink'] != doc.data['permalink']
|
|
188
|
-
|
|
230
|
+
computed_redirects << dd.data['permalink']
|
|
189
231
|
end
|
|
190
232
|
end
|
|
191
|
-
|
|
192
|
-
doc.data['redirect_from'] = redirects
|
|
193
233
|
end
|
|
234
|
+
|
|
235
|
+
# Merge user-defined and computed redirects, removing duplicates
|
|
236
|
+
all_redirects = (user_redirects + computed_redirects).uniq
|
|
237
|
+
doc.data['redirect_from'] = all_redirects unless all_redirects.empty?
|
|
194
238
|
end
|
|
195
239
|
|
|
196
240
|
def assignPageLanguagePermalinks(doc, docs)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
241
|
+
page_id = doc.data['page_id']
|
|
242
|
+
normalized_permalink = normalized_permalink_for_doc(doc)
|
|
243
|
+
translations = find_translations(page_id, normalized_permalink, docs)
|
|
244
|
+
|
|
245
|
+
doc.data['permalink_lang'] = translations
|
|
246
|
+
configured = ([@default_lang] + @languages).uniq
|
|
247
|
+
doc.data['available_languages'] = translations.keys
|
|
248
|
+
# missing_languages signals "a visitor in this lang would see different
|
|
249
|
+
# content than another lang's visitor". A single-source page falls back
|
|
250
|
+
# identically everywhere, so nothing is missing in that case.
|
|
251
|
+
doc.data['missing_languages'] =
|
|
252
|
+
translations.size > 1 ? (configured - translations.keys) : []
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Returns a hash of { lang => permalink } for all docs that are translations
|
|
256
|
+
# of the given page. Matches by page_id when present, otherwise by normalized
|
|
257
|
+
# permalink. Filters out languages not in the configured languages list.
|
|
258
|
+
# candidate_docs defaults to site.collections + site.pages so the helper can
|
|
259
|
+
# be called from Liquid render contexts where the caller doesn't already
|
|
260
|
+
# hold a docs array.
|
|
261
|
+
def find_translations(page_id, normalized_permalink, candidate_docs = nil)
|
|
262
|
+
candidate_docs ||= collections.values.flat_map(&:docs) + pages
|
|
263
|
+
valid_languages = ([@default_lang] + @languages).uniq
|
|
264
|
+
|
|
265
|
+
matching =
|
|
266
|
+
if !page_id.to_s.empty?
|
|
267
|
+
candidate_docs.select { |d| d.data['page_id'] == page_id }
|
|
268
|
+
elsif !normalized_permalink.to_s.empty?
|
|
269
|
+
candidate_docs.select { |d| normalized_permalink_for_doc(d) == normalized_permalink }
|
|
270
|
+
else
|
|
271
|
+
[]
|
|
206
272
|
end
|
|
273
|
+
|
|
274
|
+
matching.each_with_object({}) do |d, h|
|
|
275
|
+
explicit_lang = d.data['lang'] || derive_lang_from_path(d)
|
|
276
|
+
doclang = explicit_lang || @default_lang
|
|
277
|
+
next if explicit_lang && !valid_languages.include?(explicit_lang)
|
|
278
|
+
|
|
279
|
+
h[doclang] = d.data['permalink']
|
|
207
280
|
end
|
|
208
281
|
end
|
|
209
282
|
|
|
283
|
+
# Returns the doc's permalink with its own language prefix stripped, so it
|
|
284
|
+
# can be matched against sibling docs that share the same un-prefixed
|
|
285
|
+
# permalink. Returns nil when no usable permalink is present.
|
|
286
|
+
def normalized_permalink_for_doc(doc)
|
|
287
|
+
permalink = doc.data['permalink'] || (doc.respond_to?(:url) ? doc.url : nil)
|
|
288
|
+
return nil if permalink.to_s.empty?
|
|
289
|
+
|
|
290
|
+
permalink = "/#{permalink}" unless permalink.start_with?('/')
|
|
291
|
+
lang = doc.data['lang']
|
|
292
|
+
return permalink if lang.to_s.empty?
|
|
293
|
+
|
|
294
|
+
stripped = permalink.delete_prefix("/#{lang}/")
|
|
295
|
+
stripped.start_with?('/') ? stripped : "/#{stripped}"
|
|
296
|
+
end
|
|
297
|
+
|
|
210
298
|
# performs any necessary operations on the documents before rendering them
|
|
211
299
|
def process_documents(docs)
|
|
212
300
|
# return if @active_lang == @default_lang
|
|
@@ -275,7 +363,9 @@ module Jekyll
|
|
|
275
363
|
end
|
|
276
364
|
end
|
|
277
365
|
start = disabled ? 'ferh' : 'href'
|
|
278
|
-
|
|
366
|
+
# Build negative lookbehind to exclude hreflang URLs from relativization
|
|
367
|
+
# hreflang tags for default language and x-default should not be relativized
|
|
368
|
+
neglookbehind = disabled ? "" : "(?<!hreflang=\"#{@default_lang}\" |hreflang=\"x-default\" )"
|
|
279
369
|
%r{#{neglookbehind}#{start}="?#{url}#{@baseurl}/((?:#{regex}[^,'"\s/?.]+\.?)*(?:/[^\]\[)("'\s]*)?)"}
|
|
280
370
|
end
|
|
281
371
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-polyglot
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Volin
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-05-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jekyll
|
|
@@ -44,6 +44,7 @@ files:
|
|
|
44
44
|
- lib/jekyll/polyglot/hooks/assets-toggle.rb
|
|
45
45
|
- lib/jekyll/polyglot/hooks/coordinate.rb
|
|
46
46
|
- lib/jekyll/polyglot/hooks/process.rb
|
|
47
|
+
- lib/jekyll/polyglot/hooks/redirects.rb
|
|
47
48
|
- lib/jekyll/polyglot/liquid.rb
|
|
48
49
|
- lib/jekyll/polyglot/liquid/tags/i18n_headers.rb
|
|
49
50
|
- lib/jekyll/polyglot/liquid/tags/static_href.rb
|
|
@@ -71,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
71
72
|
- !ruby/object:Gem::Version
|
|
72
73
|
version: 3.1.0
|
|
73
74
|
requirements: []
|
|
74
|
-
rubygems_version: 3.3.
|
|
75
|
+
rubygems_version: 3.3.27
|
|
75
76
|
signing_key:
|
|
76
77
|
specification_version: 4
|
|
77
78
|
summary: I18n plugin for Jekyll Blogs
|