jekyll-l10n 1.6.5 → 1.7.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/lib/jekyll-l10n/constants.rb +5 -1
- data/lib/jekyll-l10n/extraction/config_loader.rb +9 -6
- data/lib/jekyll-l10n/extraction/result_saver.rb +20 -1
- data/lib/jekyll-l10n/jekyll/generator.rb +2 -1
- data/lib/jekyll-l10n/jekyll/localized_page.rb +20 -9
- data/lib/jekyll-l10n/jekyll/regeneration_checker.rb +5 -2
- data/lib/jekyll-l10n/jekyll/url_filter.rb +72 -17
- data/lib/jekyll-l10n/translation/libre_translator.rb +8 -1
- data/lib/jekyll-l10n/utils/locale_name_resolver.rb +51 -0
- data/lib/jekyll-l10n/utils/locale_url_formatter.rb +15 -0
- data/lib/jekyll-l10n/utils/page_locales_config.rb +5 -4
- data/lib/jekyll-l10n/utils/site_config_accessor.rb +23 -0
- data/lib/jekyll-l10n/utils/url_transformer.rb +20 -9
- data/lib/jekyll-l10n/version.rb +1 -1
- metadata +23 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f6934d1bf9c34ff4b1806a1b3dde6c19398ee10d261a7a3aa7925716e63f556f
|
|
4
|
+
data.tar.gz: fde2a5801b9558384d7d6b0f9854526af3d5759d567cddce5aaa94c9f0873c78
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5fd0cf1f00fdd2a0b986540f4c8aeefd2cff50fac3122b6285a92a9f45b317042b28d8f72d6eadadbdc90e1c283ad792b18edd681000799541b53a75dbdbef92
|
|
7
|
+
data.tar.gz: ec515247b53374ca812d0195815150276ce39c078356383f667a926ef313ec50fa909b316c90e03f85c3a1ede8b753f399c63542e290b3eda12035cf9b56973a
|
|
@@ -27,9 +27,13 @@ module Jekyll
|
|
|
27
27
|
# Regex fragment for matching locale codes in URL path segments.
|
|
28
28
|
# Accepts both underscore ('pt_BR') and hyphen ('zh-CN') subtag separators,
|
|
29
29
|
# unlike LOCALE_PATTERN which only accepts underscore (strict validation form).
|
|
30
|
+
# Region subtag is case-tolerant ([a-zA-Z]{2}): generated URLs are lowercase
|
|
31
|
+
# (see LocaleUrlFormatter), but this fragment also detects already-localized
|
|
32
|
+
# prefixes in hand-authored hrefs or pre-existing uppercase URLs, so it must
|
|
33
|
+
# still recognize uppercase regions.
|
|
30
34
|
# Compose into full path patterns rather than using standalone.
|
|
31
35
|
# @return [String]
|
|
32
|
-
LOCALE_CODE_SEGMENT = '[a-z]{2}(?:[_-][
|
|
36
|
+
LOCALE_CODE_SEGMENT = '[a-z]{2}(?:[_-][a-zA-Z]{2})?'
|
|
33
37
|
|
|
34
38
|
# ## Translation Fallback Modes
|
|
35
39
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../utils/page_locales_config'
|
|
4
|
+
require_relative '../utils/locale_url_formatter'
|
|
5
|
+
require_relative '../utils/site_config_accessor'
|
|
4
6
|
|
|
5
7
|
module Jekyll
|
|
6
8
|
module L10n
|
|
@@ -84,18 +86,19 @@ module Jekyll
|
|
|
84
86
|
#
|
|
85
87
|
# Localized pages are generated copies in locale subdirectories that
|
|
86
88
|
# shouldn't be re-extracted (extraction happens on original pages only).
|
|
89
|
+
# Since commit 3d2d4cd, country-subtag locales (e.g. es_ES) are written
|
|
90
|
+
# to BCP 47 hyphenated, lowercased URL directories (e.g. es-es/). Each
|
|
91
|
+
# configured locale is normalized through LocaleUrlFormatter.to_url_segment
|
|
92
|
+
# before comparison so both language-only (es) and country-subtag
|
|
93
|
+
# (es_ES → es-es) locales are correctly recognized.
|
|
87
94
|
#
|
|
88
95
|
# @param file_path [String] Path to file to check
|
|
89
96
|
# @return [Boolean] True if file is in a locale subdirectory
|
|
90
97
|
def skip_localized_page?(file_path)
|
|
91
98
|
relative_path = file_path.sub(@dest, '')
|
|
92
99
|
|
|
93
|
-
all_locales = @site
|
|
94
|
-
|
|
95
|
-
end
|
|
96
|
-
all_locales.flatten!
|
|
97
|
-
all_locales.uniq!
|
|
98
|
-
all_locales.any? { |locale| relative_path.start_with?("/#{locale}/") }
|
|
100
|
+
all_locales = SiteConfigAccessor.all_configured_locales(@site)
|
|
101
|
+
all_locales.any? { |locale| relative_path.start_with?("/#{LocaleUrlFormatter.to_url_segment(locale)}/") }
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
# Extract CSS selectors for element exclusion from configuration.
|
|
@@ -68,12 +68,17 @@ module Jekyll
|
|
|
68
68
|
# Called once per build (from Extractor.extract_site) rather than per page,
|
|
69
69
|
# reducing disk I/O and fuzzy-matching passes from O(pages) to O(1).
|
|
70
70
|
#
|
|
71
|
+
# Uses the union of all locales configured anywhere on the site (site-wide
|
|
72
|
+
# defaults plus any page-level front-matter overrides) so that page-only
|
|
73
|
+
# locales (e.g. es_ES declared only in page front matter) are included in the
|
|
74
|
+
# compendium merge, not left as orphaned per-page directories.
|
|
75
|
+
#
|
|
71
76
|
# @return [void]
|
|
72
77
|
def finalize_compendia
|
|
73
78
|
return unless @site_config.update_compendium?
|
|
74
79
|
|
|
75
80
|
po_manager = PoFileManager.new(@site, @site_config.locales_dir)
|
|
76
|
-
CompendiumMerger.new(@site).merge_compendia(po_manager,
|
|
81
|
+
CompendiumMerger.new(@site).merge_compendia(po_manager, merge_config)
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
# Translate compendia using LibreTranslate.
|
|
@@ -100,7 +105,21 @@ module Jekyll
|
|
|
100
105
|
po_files_created
|
|
101
106
|
end
|
|
102
107
|
|
|
108
|
+
# Build a PageLocalesConfig whose locale list is the union of site-wide
|
|
109
|
+
# defaults and every page-level front-matter locale override.
|
|
110
|
+
#
|
|
111
|
+
# All other site-wide settings (locales_dir, extraction, translation,
|
|
112
|
+
# logging, etc.) are preserved unchanged; only the locale list is widened.
|
|
113
|
+
#
|
|
114
|
+
# @return [PageLocalesConfig]
|
|
115
|
+
def merge_config
|
|
116
|
+
all_locales = SiteConfigAccessor.all_configured_locales(@site)
|
|
117
|
+
merged_data = (@site_config.data['with_locales_data'] || {}).merge('locales' => all_locales)
|
|
118
|
+
PageLocalesConfig.new({ 'with_locales_data' => merged_data })
|
|
119
|
+
end
|
|
120
|
+
|
|
103
121
|
private :save_po_files
|
|
122
|
+
private :merge_config
|
|
104
123
|
end
|
|
105
124
|
end
|
|
106
125
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative '../constants'
|
|
4
4
|
require_relative 'regeneration_checker'
|
|
5
|
+
require_relative '../utils/locale_url_formatter'
|
|
5
6
|
|
|
6
7
|
module Jekyll
|
|
7
8
|
module L10n
|
|
@@ -112,7 +113,7 @@ module Jekyll
|
|
|
112
113
|
end
|
|
113
114
|
|
|
114
115
|
def create_localized_page(site, page, locale)
|
|
115
|
-
new_dir = "/#{locale}#{page.dir}"
|
|
116
|
+
new_dir = "/#{LocaleUrlFormatter.to_url_segment(locale)}#{page.dir}"
|
|
116
117
|
LocalizedPage.new(site: site, base: site.source, dir: new_dir, page: page,
|
|
117
118
|
locale: locale)
|
|
118
119
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../utils/locale_url_formatter'
|
|
4
|
+
|
|
3
5
|
module Jekyll
|
|
4
6
|
module L10n
|
|
5
7
|
# Localized Page - Represents a Jekyll page in a specific locale
|
|
@@ -26,7 +28,7 @@ module Jekyll
|
|
|
26
28
|
#
|
|
27
29
|
class LocalizedPage < Jekyll::Page
|
|
28
30
|
# @!attribute [rw] locale
|
|
29
|
-
# The
|
|
31
|
+
# The canonical (POSIX/gettext) locale code for this page variant (e.g., 'es', 'pt_BR')
|
|
30
32
|
# @return [String]
|
|
31
33
|
# @!attribute [rw] lang
|
|
32
34
|
# Alias for locale, used in Liquid templates
|
|
@@ -56,7 +58,7 @@ module Jekyll
|
|
|
56
58
|
# @param dir [String] (keyword) The directory path (will be prefixed with locale,
|
|
57
59
|
# e.g., '/es/path')
|
|
58
60
|
# @param page [Jekyll::Page] (keyword) The source page to localize
|
|
59
|
-
# @param locale [String] (keyword) The
|
|
61
|
+
# @param locale [String] (keyword) The canonical locale code (e.g., 'es', 'pt_BR', 'zh_CN')
|
|
60
62
|
# @return [LocalizedPage] A new localized page instance
|
|
61
63
|
# @note All parameters are keyword arguments and must be passed by name
|
|
62
64
|
def initialize(site:, base:, dir:, page:, locale:)
|
|
@@ -82,25 +84,31 @@ module Jekyll
|
|
|
82
84
|
# Get the URL placeholder substitutions for this localized page
|
|
83
85
|
#
|
|
84
86
|
# Overrides Jekyll::Page's url_placeholders to include the locale prefix in path generation.
|
|
87
|
+
# The locale segment is converted to BCP 47 hyphenated form for use in URLs.
|
|
85
88
|
# This ensures that Jekyll's permalink logic respects the locale prefix.
|
|
86
89
|
#
|
|
87
90
|
# @return [Hash<Symbol, String>] Hash of placeholders with locale-prefixed path
|
|
88
91
|
def url_placeholders
|
|
89
92
|
placeholders = super
|
|
90
|
-
|
|
93
|
+
if placeholders[:path]
|
|
94
|
+
url_locale = LocaleUrlFormatter.to_url_segment(@locale)
|
|
95
|
+
placeholders[:path] = "/#{url_locale}#{placeholders[:path]}"
|
|
96
|
+
end
|
|
91
97
|
placeholders
|
|
92
98
|
end
|
|
93
99
|
|
|
94
100
|
# Get the full URL for this localized page
|
|
95
101
|
#
|
|
96
|
-
# Returns the page's URL with a locale prefix
|
|
97
|
-
#
|
|
102
|
+
# Returns the page's URL with a locale prefix in BCP 47 hyphenated form.
|
|
103
|
+
# For example, if the source page URL is '/path/to/page/', this returns
|
|
104
|
+
# '/es-ES/path/to/page/' for locale 'es_ES'.
|
|
98
105
|
# For HTML pages, URLs end with a trailing slash. For assets, the file extension is preserved.
|
|
99
106
|
#
|
|
100
107
|
# @return [String] The locale-prefixed URL with optional trailing slash
|
|
101
|
-
# (e.g., '/es/path/to/page/' or '/es/assets/style.css')
|
|
108
|
+
# (e.g., '/es-ES/path/to/page/' or '/es-ES/assets/style.css')
|
|
102
109
|
def url
|
|
103
|
-
|
|
110
|
+
url_locale = LocaleUrlFormatter.to_url_segment(@locale)
|
|
111
|
+
url = "/#{url_locale}#{@original_url}"
|
|
104
112
|
# Only add trailing slash for directory URLs (no file extension)
|
|
105
113
|
url += '/' unless url.end_with?('/') || url =~ /\.[a-zA-Z0-9]+$/
|
|
106
114
|
url
|
|
@@ -109,8 +117,10 @@ module Jekyll
|
|
|
109
117
|
# Get the destination file path for this localized page
|
|
110
118
|
#
|
|
111
119
|
# Computes the full file system path where this page's content will be written.
|
|
112
|
-
# For HTML pages with URL '/es/path/to/page/', this returns
|
|
113
|
-
#
|
|
120
|
+
# For HTML pages with URL '/es-ES/path/to/page/', this returns
|
|
121
|
+
# 'dest/es-ES/path/to/page/index.html'.
|
|
122
|
+
# For assets with URL '/es-ES/assets/style.css', this returns
|
|
123
|
+
# 'dest/es-ES/assets/style.css'.
|
|
114
124
|
#
|
|
115
125
|
# @param dest [String, nil] The destination directory (defaults to nil, which uses
|
|
116
126
|
# site config destination)
|
|
@@ -132,6 +142,7 @@ module Jekyll
|
|
|
132
142
|
# Convert this localized page to Liquid template data
|
|
133
143
|
#
|
|
134
144
|
# Overrides Jekyll::Page's to_liquid to include locale and lang in template context.
|
|
145
|
+
# The url key contains the BCP 47 hyphenated form; locale and lang remain canonical.
|
|
135
146
|
# This makes the page's locale available to Liquid templates for conditional rendering
|
|
136
147
|
# and URL generation.
|
|
137
148
|
#
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative '../utils/site_config_accessor'
|
|
4
4
|
require_relative '../po_file/path_builder'
|
|
5
|
+
require_relative '../utils/locale_url_formatter'
|
|
5
6
|
|
|
6
7
|
module Jekyll
|
|
7
8
|
module L10n
|
|
@@ -84,8 +85,10 @@ module Jekyll
|
|
|
84
85
|
end
|
|
85
86
|
|
|
86
87
|
def dest_path(page, locale)
|
|
87
|
-
# Replicate LocalizedPage destination logic without instantiation
|
|
88
|
-
|
|
88
|
+
# Replicate LocalizedPage destination logic without instantiation.
|
|
89
|
+
# Uses BCP 47 hyphenated locale segment to match LocalizedPage#destination output.
|
|
90
|
+
url_locale = LocaleUrlFormatter.to_url_segment(locale)
|
|
91
|
+
url = "/#{url_locale}#{page.url.chomp('/')}/"
|
|
89
92
|
"#{@destination}#{url}index.html"
|
|
90
93
|
end
|
|
91
94
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require 'liquid'
|
|
4
4
|
require_relative '../constants'
|
|
5
5
|
require_relative '../utils/error_handler'
|
|
6
|
+
require_relative '../utils/locale_name_resolver'
|
|
7
|
+
require_relative '../utils/locale_url_formatter'
|
|
6
8
|
|
|
7
9
|
module Jekyll
|
|
8
10
|
module L10n
|
|
@@ -15,25 +17,30 @@ module Jekyll
|
|
|
15
17
|
# the filters available in all Jekyll templates.
|
|
16
18
|
#
|
|
17
19
|
# Key responsibilities:
|
|
18
|
-
# - Generate locale-prefixed URLs
|
|
20
|
+
# - Generate locale-prefixed URLs in BCP 47 hyphenated form
|
|
19
21
|
# - Switch between locale variants of the same page
|
|
20
|
-
# -
|
|
22
|
+
# - Build structured locale-switcher menu data
|
|
23
|
+
# - Validate locales against site configuration (with form normalization)
|
|
21
24
|
# - Handle edge cases (external URLs, anchors, protocols)
|
|
22
25
|
#
|
|
23
26
|
# @example Usage in Jekyll Liquid templates
|
|
24
27
|
# <!-- Add locale prefix to URL -->
|
|
25
|
-
# <a href="{{ '/about/' | locale_url: '
|
|
28
|
+
# <a href="{{ '/about/' | locale_url: 'es_ES' }}">Acerca de</a>
|
|
29
|
+
# <!-- Produces: /es-ES/about/ -->
|
|
26
30
|
#
|
|
27
31
|
# <!-- Switch to different locale variant -->
|
|
28
32
|
# <a href="{{ page.url | switch_locale_url: 'fr' }}">Français</a>
|
|
29
33
|
#
|
|
34
|
+
# <!-- Build a language-switcher menu -->
|
|
35
|
+
# {% assign menu = page.url | locale_menu %}
|
|
36
|
+
#
|
|
30
37
|
# @see Jekyll::L10n for filter registration (line 218 in lib/jekyll-l10n.rb)
|
|
31
38
|
#
|
|
32
39
|
module UrlFilter
|
|
33
40
|
# Generate a locale-prefixed URL for the given URL and locale
|
|
34
41
|
#
|
|
35
|
-
# Adds a locale prefix
|
|
36
|
-
#
|
|
42
|
+
# Adds a locale prefix in BCP 47 hyphenated form to the given URL. For example,
|
|
43
|
+
# '/about/' becomes '/es-ES/about/' for locale 'es_ES'. The filter handles
|
|
37
44
|
# various edge cases and only applies the prefix when appropriate.
|
|
38
45
|
#
|
|
39
46
|
# The method performs several checks to determine if prefixing is appropriate:
|
|
@@ -42,12 +49,13 @@ module Jekyll
|
|
|
42
49
|
# - External URLs, anchors, and relative paths are not modified
|
|
43
50
|
#
|
|
44
51
|
# @param url [String] The URL to prefix (e.g., '/about/', '/path/to/page/')
|
|
45
|
-
# @param locale [String, nil] The locale code (e.g., '
|
|
52
|
+
# @param locale [String, nil] The locale code (e.g., 'es_ES', 'pt_BR'). Defaults to nil
|
|
46
53
|
# (uses current page's locale). If nil, uses page's locale from context.
|
|
47
|
-
# @return [String] The locale-prefixed URL, or original URL if
|
|
54
|
+
# @return [String] The locale-prefixed URL in BCP 47 form, or original URL if
|
|
55
|
+
# prefixing is not appropriate
|
|
48
56
|
# @example
|
|
49
|
-
# <!-- Explicit locale -->
|
|
50
|
-
# {{ '/about/' | locale_url: '
|
|
57
|
+
# <!-- Explicit underscore locale converted to hyphen in URL -->
|
|
58
|
+
# {{ '/about/' | locale_url: 'es_ES' }} # => '/es-ES/about/'
|
|
51
59
|
#
|
|
52
60
|
# <!-- Current page's locale -->
|
|
53
61
|
# {{ '/about/' | locale_url }} # => '/es/about/' (if page locale is 'es')
|
|
@@ -55,29 +63,29 @@ module Jekyll
|
|
|
55
63
|
locale ||= current_locale
|
|
56
64
|
return url if should_skip?(url, locale)
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
url_segment = LocaleUrlFormatter.to_url_segment(locale)
|
|
67
|
+
"/#{url_segment}#{url}"
|
|
59
68
|
end
|
|
60
69
|
|
|
61
70
|
# Generate a URL for the given locale variant of the current page
|
|
62
71
|
#
|
|
63
72
|
# Switches the current URL to a different locale variant. For example, if the current
|
|
64
|
-
# page is '/es/about/', calling this with 'fr' returns '/fr/about/'. This is useful
|
|
73
|
+
# page is '/es-ES/about/', calling this with 'fr' returns '/fr/about/'. This is useful
|
|
65
74
|
# for building language switcher menus.
|
|
66
75
|
#
|
|
67
76
|
# This filter first extracts the base URL (without locale prefix) from the given URL,
|
|
68
77
|
# then applies the target locale using the standard locale_url logic.
|
|
69
78
|
#
|
|
70
79
|
# @param url [String] The current URL (may or may not have a locale prefix)
|
|
71
|
-
# @param target_locale [String] The target locale code (e.g., '
|
|
80
|
+
# @param target_locale [String] The target locale code (e.g., 'es_ES', 'fr', 'pt_BR')
|
|
72
81
|
# @return [String] The URL for the target locale, or original URL if locale is invalid
|
|
73
82
|
# @example
|
|
74
|
-
# <!-- Current page is /es/about/, switch to French -->
|
|
83
|
+
# <!-- Current page is /es-ES/about/, switch to French -->
|
|
75
84
|
# {{ page.url | switch_locale_url: 'fr' }} # => '/fr/about/'
|
|
76
85
|
#
|
|
77
86
|
# <!-- Build a language switcher -->
|
|
78
|
-
# <a href="{{ page.url | switch_locale_url: '
|
|
87
|
+
# <a href="{{ page.url | switch_locale_url: 'es_ES' }}">Español (España)</a>
|
|
79
88
|
# <a href="{{ page.url | switch_locale_url: 'fr' }}">Français</a>
|
|
80
|
-
# <a href="{{ page.url | switch_locale_url: 'pt' }}">Português</a>
|
|
81
89
|
def switch_locale_url(url, target_locale)
|
|
82
90
|
# Validate target locale is configured
|
|
83
91
|
return url unless valid_target_locale?(target_locale)
|
|
@@ -89,6 +97,37 @@ module Jekyll
|
|
|
89
97
|
locale_url(base_url_value, target_locale)
|
|
90
98
|
end
|
|
91
99
|
|
|
100
|
+
# Build the full locale-switcher menu for the current page.
|
|
101
|
+
#
|
|
102
|
+
# Returns one entry per locale available to the current page (via the
|
|
103
|
+
# existing #configured_locales resolution — site-wide with_locales_data.locales
|
|
104
|
+
# config, or a page-level front-matter override — always including English
|
|
105
|
+
# even if not explicitly configured). Each entry carries everything a theme
|
|
106
|
+
# needs to render a menu item with zero locale-specific logic of its own:
|
|
107
|
+
# a display name, a switch-target URL, and whether it's the page's current
|
|
108
|
+
# locale.
|
|
109
|
+
#
|
|
110
|
+
# @param url [String] The current page's URL (e.g. page.url)
|
|
111
|
+
# @return [Array<Hash>] Array of { 'code', 'name', 'url', 'current' } hashes,
|
|
112
|
+
# one per locale. +code+ is the canonical underscore form (e.g. "es_ES"),
|
|
113
|
+
# +name+ is the ISO-resolved display name (e.g. "Spanish (Spain)"),
|
|
114
|
+
# +url+ is what {#switch_locale_url} produces for that locale, and
|
|
115
|
+
# +current+ is +true+ only for the locale matching the page's own locale.
|
|
116
|
+
# @example
|
|
117
|
+
# {{ page.url | locale_menu }}
|
|
118
|
+
# # => [{"code"=>"en","name"=>"English","url"=>"/about/","current"=>false},
|
|
119
|
+
# # {"code"=>"es_ES","name"=>"Spanish (Spain)","url"=>"/es-ES/about/","current"=>true}]
|
|
120
|
+
def locale_menu(url)
|
|
121
|
+
menu_locales.map do |locale|
|
|
122
|
+
{
|
|
123
|
+
'code' => locale,
|
|
124
|
+
'name' => LocaleNameResolver.resolve(locale),
|
|
125
|
+
'url' => switch_locale_url(url, locale),
|
|
126
|
+
'current' => locale == (current_locale || 'en')
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
92
131
|
private
|
|
93
132
|
|
|
94
133
|
def current_locale
|
|
@@ -96,7 +135,7 @@ module Jekyll
|
|
|
96
135
|
return nil unless page
|
|
97
136
|
|
|
98
137
|
if page.is_a?(Hash)
|
|
99
|
-
page.dig('data', 'locale')
|
|
138
|
+
page.dig('data', 'locale') || page['locale']
|
|
100
139
|
else
|
|
101
140
|
page&.data&.[]('locale')
|
|
102
141
|
end
|
|
@@ -155,7 +194,11 @@ module Jekyll
|
|
|
155
194
|
|
|
156
195
|
# Get configured locales from page or site config
|
|
157
196
|
locales_config = configured_locales
|
|
158
|
-
|
|
197
|
+
|
|
198
|
+
# Normalize both the target locale and each configured locale to BCP 47 form
|
|
199
|
+
# before comparing, so 'es-ES' matches 'es_ES' and vice versa.
|
|
200
|
+
url_locale = LocaleUrlFormatter.to_url_segment(locale)
|
|
201
|
+
locales_config.any? { |l| LocaleUrlFormatter.to_url_segment(l) == url_locale }
|
|
159
202
|
end
|
|
160
203
|
|
|
161
204
|
def configured_locales
|
|
@@ -175,6 +218,18 @@ module Jekyll
|
|
|
175
218
|
[]
|
|
176
219
|
end
|
|
177
220
|
|
|
221
|
+
# Locales for the menu: whatever #configured_locales resolves for the
|
|
222
|
+
# current page, always including English even if not explicitly listed
|
|
223
|
+
# (English pages never carry a locale prefix, but must still appear as a
|
|
224
|
+
# switch target).
|
|
225
|
+
#
|
|
226
|
+
# @return [Array<String>]
|
|
227
|
+
def menu_locales
|
|
228
|
+
locales = configured_locales.dup
|
|
229
|
+
locales.unshift('en') unless locales.include?('en')
|
|
230
|
+
locales
|
|
231
|
+
end
|
|
232
|
+
|
|
178
233
|
def base_url(url)
|
|
179
234
|
page = @context.registers[:page]
|
|
180
235
|
|
|
@@ -172,7 +172,7 @@ module Jekyll
|
|
|
172
172
|
|
|
173
173
|
texts = [entry.msgid]
|
|
174
174
|
translations = make_api_request(texts, target_locale)
|
|
175
|
-
entry.msgstr = translations
|
|
175
|
+
entry.msgstr = translations&.first || ''
|
|
176
176
|
end
|
|
177
177
|
rescue StandardError => e
|
|
178
178
|
ErrorHandler.log_error('translate_batch', e)
|
|
@@ -225,6 +225,13 @@ module Jekyll
|
|
|
225
225
|
|
|
226
226
|
def handle_api_response(response)
|
|
227
227
|
response_data = JSON.parse(response.body)
|
|
228
|
+
|
|
229
|
+
if response_data['error']
|
|
230
|
+
Jekyll.logger.warn 'Localization',
|
|
231
|
+
"[LibreTranslator] API rejected request: #{response_data['error']}"
|
|
232
|
+
return nil
|
|
233
|
+
end
|
|
234
|
+
|
|
228
235
|
translated = response_data['translatedText']
|
|
229
236
|
translation_str = translated.is_a?(Array) ? translated.inspect : translated
|
|
230
237
|
Jekyll.logger.debug 'Localization', "[LibreTranslator] Parsed translation: #{translation_str}"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'locale/info'
|
|
4
|
+
|
|
5
|
+
module Jekyll
|
|
6
|
+
module L10n
|
|
7
|
+
# Resolves a canonical locale code into a human-readable display name using
|
|
8
|
+
# ISO 639-1 language names and ISO 3166-1 country names from the +locale+ gem.
|
|
9
|
+
#
|
|
10
|
+
# Supported input forms:
|
|
11
|
+
# - Language-only: <tt>"es"</tt> → <tt>"Spanish"</tt>
|
|
12
|
+
# - Language + country subtag (POSIX underscore form): <tt>"es_ES"</tt> → <tt>"Spanish (Spain)"</tt>,
|
|
13
|
+
# <tt>"pt_BR"</tt> → <tt>"Portuguese (Brazil)"</tt>
|
|
14
|
+
#
|
|
15
|
+
# Falls back to the raw code segment when the locale gem cannot resolve a
|
|
16
|
+
# language or region code. This is a defensive-only path — every locale that
|
|
17
|
+
# reaches this class from plugin-managed configuration has already passed
|
|
18
|
+
# +Constants::LOCALE_PATTERN+ validation, so unknown codes are not expected
|
|
19
|
+
# in normal operation.
|
|
20
|
+
#
|
|
21
|
+
# @example Language-only codes
|
|
22
|
+
# Jekyll::L10n::LocaleNameResolver.resolve('es') # => "Spanish"
|
|
23
|
+
# Jekyll::L10n::LocaleNameResolver.resolve('en') # => "English"
|
|
24
|
+
# Jekyll::L10n::LocaleNameResolver.resolve('pt') # => "Portuguese"
|
|
25
|
+
#
|
|
26
|
+
# @example Country-subtag codes
|
|
27
|
+
# Jekyll::L10n::LocaleNameResolver.resolve('es_ES') # => "Spanish (Spain)"
|
|
28
|
+
# Jekyll::L10n::LocaleNameResolver.resolve('pt_BR') # => "Portuguese (Brazil)"
|
|
29
|
+
#
|
|
30
|
+
# @example Fallback for unresolvable codes (defensive)
|
|
31
|
+
# Jekyll::L10n::LocaleNameResolver.resolve('xx') # => "xx"
|
|
32
|
+
# Jekyll::L10n::LocaleNameResolver.resolve('xx_YY') # => "xx (YY)"
|
|
33
|
+
class LocaleNameResolver
|
|
34
|
+
# Resolve a canonical locale code to a human-readable display name.
|
|
35
|
+
#
|
|
36
|
+
# @param locale [String, nil] The locale code to resolve (e.g. <tt>"es"</tt>,
|
|
37
|
+
# <tt>"es_ES"</tt>, <tt>"pt_BR"</tt>). +nil+ or empty string returns an
|
|
38
|
+
# empty string without raising.
|
|
39
|
+
# @return [String] The resolved display name, or the raw code segment(s) if
|
|
40
|
+
# the locale gem cannot resolve either part.
|
|
41
|
+
def self.resolve(locale)
|
|
42
|
+
language_code, country_code = locale.to_s.split('_', 2)
|
|
43
|
+
language_name = Locale::Info.get_language(language_code)&.name || language_code
|
|
44
|
+
return language_name unless country_code
|
|
45
|
+
|
|
46
|
+
country_name = Locale::Info.get_region(country_code)&.name || country_code
|
|
47
|
+
"#{language_name} (#{country_name})"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module L10n
|
|
5
|
+
# Converts a canonical (gettext/POSIX-style) locale identifier, e.g. "es_ES",
|
|
6
|
+
# into the BCP 47 form used in generated URLs and hrefs, e.g. "es-es".
|
|
7
|
+
# Lowercased per RFC 5646 ss2.1.1 (subtag case carries no meaning; this
|
|
8
|
+
# is a display-convention choice, not a standard requirement). Idempotent.
|
|
9
|
+
class LocaleUrlFormatter
|
|
10
|
+
def self.to_url_segment(locale)
|
|
11
|
+
locale.to_s.tr('_', '-').downcase
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -17,7 +17,7 @@ module Jekyll
|
|
|
17
17
|
#
|
|
18
18
|
# Key responsibilities:
|
|
19
19
|
# - Parse `with_locales_data` from page front matter
|
|
20
|
-
# - Validate locale codes against ISO 639-1
|
|
20
|
+
# - Validate locale codes against ISO 639-1 (with optional ISO 3166-1 alpha-2 country) format
|
|
21
21
|
# - Validate LibreTranslate configuration when enabled
|
|
22
22
|
# - Provide getter methods with sensible defaults
|
|
23
23
|
# - Raise detailed validation errors for invalid configurations
|
|
@@ -87,7 +87,8 @@ module Jekyll
|
|
|
87
87
|
# Get the list of locales configured for this page
|
|
88
88
|
#
|
|
89
89
|
# Returns the locales specified in `with_locales_data.locales`, or an empty array
|
|
90
|
-
# if not configured. All returned locales are guaranteed to match ISO 639-1
|
|
90
|
+
# if not configured. All returned locales are guaranteed to match ISO 639-1 format,
|
|
91
|
+
# optionally suffixed with an ISO 3166-1 alpha-2 country code.
|
|
91
92
|
#
|
|
92
93
|
# @return [Array<String>] BCP 47 locale codes (e.g., ['es', 'fr', 'pt_BR'])
|
|
93
94
|
def locales
|
|
@@ -307,7 +308,7 @@ module Jekyll
|
|
|
307
308
|
<<~ERROR
|
|
308
309
|
Invalid locale codes in #{@page_path}: #{invalid_codes.join(', ')}
|
|
309
310
|
|
|
310
|
-
Expected ISO 639-1
|
|
311
|
+
Expected ISO 639-1 language code, optionally with ISO 3166-1 alpha-2 country (e.g., 'es', 'fr', 'pt_BR')
|
|
311
312
|
Valid pattern: ^[a-z]{2}(_[A-Z]{2})?$
|
|
312
313
|
|
|
313
314
|
Common mistakes:
|
|
@@ -328,7 +329,7 @@ module Jekyll
|
|
|
328
329
|
'LibreTranslate is enabled but libretranslate_api_url is not configured'
|
|
329
330
|
end
|
|
330
331
|
|
|
331
|
-
# Validate source locale format (ISO 639-1
|
|
332
|
+
# Validate source locale format (ISO 639-1, optionally with ISO 3166-1 alpha-2 country)
|
|
332
333
|
unless LOCALE_PATTERN.match?(libretranslate_source_locale)
|
|
333
334
|
raise Jekyll::Errors::InvalidConfigurationError,
|
|
334
335
|
"Invalid libretranslate_source_locale: #{libretranslate_source_locale}"
|
|
@@ -10,12 +10,14 @@ module Jekyll
|
|
|
10
10
|
#
|
|
11
11
|
# Key responsibilities:
|
|
12
12
|
# * Extract localization data from site configuration
|
|
13
|
+
# * Compute the union of all configured locales across site and page front matter
|
|
13
14
|
# * Access site source directory
|
|
14
15
|
# * Access site destination directory
|
|
15
16
|
# * Handle both Jekyll site objects and hash-based doubles
|
|
16
17
|
#
|
|
17
18
|
# @example
|
|
18
19
|
# locales_data = SiteConfigAccessor.extract_locales_data(site)
|
|
20
|
+
# all_locales = SiteConfigAccessor.all_configured_locales(site)
|
|
19
21
|
# source = SiteConfigAccessor.source(site)
|
|
20
22
|
# dest = SiteConfigAccessor.dest(site)
|
|
21
23
|
class SiteConfigAccessor
|
|
@@ -33,6 +35,27 @@ module Jekyll
|
|
|
33
35
|
entry&.dig('values', 'with_locales_data') || {}
|
|
34
36
|
end
|
|
35
37
|
|
|
38
|
+
# Compute the union of all locales configured anywhere on the site.
|
|
39
|
+
#
|
|
40
|
+
# Combines locales from two sources:
|
|
41
|
+
# - The site-wide `with_locales_data.locales` default (from `_config.yml`)
|
|
42
|
+
# - Any page-level `with_locales_data.locales` front-matter override, since
|
|
43
|
+
# a page-level override replaces the site default for that page rather than
|
|
44
|
+
# inheriting it, so page scanning alone misses site-wide locales when few
|
|
45
|
+
# or no ordinary pages are present.
|
|
46
|
+
#
|
|
47
|
+
# This lets callers discover locales that exist only as a page-level override,
|
|
48
|
+
# not declared in site config, while also retaining all site-wide locales.
|
|
49
|
+
#
|
|
50
|
+
# @param site [Jekyll::Site, Hash] Jekyll site object or hash double
|
|
51
|
+
# @return [Array<String>] Unique locale codes configured anywhere on the site
|
|
52
|
+
def self.all_configured_locales(site)
|
|
53
|
+
site_wide_locales = extract_locales_data(site)['locales'] || []
|
|
54
|
+
pages = site.is_a?(Hash) ? site['pages'] : site.pages
|
|
55
|
+
page_locales = pages.flat_map { |p| p.data.dig('with_locales_data', 'locales') || [] }
|
|
56
|
+
(site_wide_locales + page_locales).uniq
|
|
57
|
+
end
|
|
58
|
+
|
|
36
59
|
# Get the site source directory.
|
|
37
60
|
#
|
|
38
61
|
# @param site [Jekyll::Site, Hash] Jekyll site object or hash double
|
|
@@ -3,19 +3,21 @@
|
|
|
3
3
|
require 'nokogiri'
|
|
4
4
|
require_relative '../constants'
|
|
5
5
|
require_relative 'html_parser'
|
|
6
|
+
require_relative 'locale_url_formatter'
|
|
6
7
|
|
|
7
8
|
module Jekyll
|
|
8
9
|
module L10n
|
|
9
10
|
# Transforms relative URLs in HTML to include locale prefixes.
|
|
10
11
|
#
|
|
11
12
|
# UrlTransformer modifies href attributes in links to prefix them with
|
|
12
|
-
# the target locale (e.g., /docs/page.html becomes
|
|
13
|
+
# the target locale in BCP 47 hyphenated form (e.g., /docs/page.html becomes
|
|
14
|
+
# /es-ES/docs/page.html for a canonical "es_ES" locale).
|
|
13
15
|
# It preserves external links, anchors, mailto, tel links, and links
|
|
14
16
|
# already containing locale prefixes. Skips English locale (default language).
|
|
15
17
|
#
|
|
16
18
|
# Key responsibilities:
|
|
17
19
|
# * Identify relative links to transform
|
|
18
|
-
# * Add locale prefix to href values
|
|
20
|
+
# * Add locale prefix in BCP 47 form to href values
|
|
19
21
|
# * Preserve external links and special URLs
|
|
20
22
|
# * Skip already-localized URLs
|
|
21
23
|
# * Handle baseurl paths correctly
|
|
@@ -23,20 +25,25 @@ module Jekyll
|
|
|
23
25
|
#
|
|
24
26
|
# @example
|
|
25
27
|
# html = '<a href="/docs/page.html">Link</a>'
|
|
26
|
-
# transformed = UrlTransformer.transform(html, '
|
|
27
|
-
# # Returns '<a href="/baseurl/es/docs/page.html">Link</a>'
|
|
28
|
+
# transformed = UrlTransformer.transform(html, 'es_ES', '/baseurl')
|
|
29
|
+
# # Returns '<a href="/baseurl/es-ES/docs/page.html">Link</a>'
|
|
28
30
|
class UrlTransformer
|
|
29
31
|
class << self
|
|
30
32
|
# Transform all relative URLs in HTML to include locale prefix.
|
|
31
33
|
#
|
|
32
|
-
# Parses HTML document, identifies relative links, adds locale prefix
|
|
33
|
-
# removes auto-inserted meta tags, and
|
|
34
|
+
# Parses HTML document, identifies relative links, adds locale prefix
|
|
35
|
+
# in BCP 47 hyphenated form, removes auto-inserted meta tags, and
|
|
36
|
+
# returns transformed HTML.
|
|
34
37
|
#
|
|
35
38
|
# @param html [String] HTML content with URLs to transform
|
|
36
|
-
# @param locale [String] Target locale code (e.g., 'es', 'fr')
|
|
39
|
+
# @param locale [String] Target locale code (e.g., 'es', 'es_ES', 'fr')
|
|
37
40
|
# @param baseurl [String] Base URL for site (e.g., '/baseurl')
|
|
38
41
|
# @return [String] HTML with locale-prefixed URLs
|
|
39
42
|
def transform(html, locale, baseurl)
|
|
43
|
+
# Convert canonical locale to BCP 47 hyphenated form for URL usage.
|
|
44
|
+
# This is idempotent: already-hyphenated input is unchanged.
|
|
45
|
+
locale = LocaleUrlFormatter.to_url_segment(locale)
|
|
46
|
+
|
|
40
47
|
return html if should_skip_transform?(locale)
|
|
41
48
|
|
|
42
49
|
# Use Nokogiri::HTML to properly parse full HTML documents while preserving
|
|
@@ -58,10 +65,14 @@ module Jekyll
|
|
|
58
65
|
# Useful when document is already parsed to avoid re-parsing.
|
|
59
66
|
#
|
|
60
67
|
# @param doc [Nokogiri::HTML::Document] Parsed HTML document
|
|
61
|
-
# @param locale [String] Target locale code
|
|
68
|
+
# @param locale [String] Target locale code (canonical or BCP 47 form)
|
|
62
69
|
# @param baseurl [String] Base URL for site
|
|
63
70
|
# @return [void]
|
|
64
71
|
def transform_document(doc, locale, baseurl)
|
|
72
|
+
# Convert canonical locale to BCP 47 hyphenated form for URL usage.
|
|
73
|
+
# This is idempotent: already-hyphenated input is unchanged.
|
|
74
|
+
locale = LocaleUrlFormatter.to_url_segment(locale)
|
|
75
|
+
|
|
65
76
|
return if should_skip_transform?(locale)
|
|
66
77
|
|
|
67
78
|
doc.css('a[href]').each do |link|
|
|
@@ -142,7 +153,7 @@ module Jekyll
|
|
|
142
153
|
|
|
143
154
|
def locale_prefix?(path)
|
|
144
155
|
# Check if the path contains a locale code
|
|
145
|
-
# After stripping baseurl, this detects: /es/, /fr/, /
|
|
156
|
+
# After stripping baseurl, this detects: /es/, /fr/, /pt-BR/, /zh-CN/, etc.
|
|
146
157
|
path.match?(%r{/#{Constants::LOCALE_CODE_SEGMENT}(?:/|$)}o)
|
|
147
158
|
end
|
|
148
159
|
|
data/lib/jekyll-l10n/version.rb
CHANGED
|
@@ -6,6 +6,6 @@ module Jekyll
|
|
|
6
6
|
# Regenerated alongside the gemspec on every `make jekyll-l10n.gemspec`
|
|
7
7
|
# run (locally or in CI), so it always matches spec.version in the built
|
|
8
8
|
# gem. Any hand edit is overwritten on the next run.
|
|
9
|
-
VERSION = '1.
|
|
9
|
+
VERSION = '1.7.0'
|
|
10
10
|
end
|
|
11
11
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-l10n
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alain Reguera Delgado
|
|
@@ -49,6 +49,26 @@ dependencies:
|
|
|
49
49
|
- - "<"
|
|
50
50
|
- !ruby/object:Gem::Version
|
|
51
51
|
version: '5.0'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: locale
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '2.1'
|
|
59
|
+
- - "<"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '2.1'
|
|
69
|
+
- - "<"
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '3.0'
|
|
52
72
|
- !ruby/object:Gem::Dependency
|
|
53
73
|
name: nokogiri
|
|
54
74
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -256,6 +276,8 @@ files:
|
|
|
256
276
|
- lib/jekyll-l10n/utils/html_elements.rb
|
|
257
277
|
- lib/jekyll-l10n/utils/html_parser.rb
|
|
258
278
|
- lib/jekyll-l10n/utils/html_text_utils.rb
|
|
279
|
+
- lib/jekyll-l10n/utils/locale_name_resolver.rb
|
|
280
|
+
- lib/jekyll-l10n/utils/locale_url_formatter.rb
|
|
259
281
|
- lib/jekyll-l10n/utils/logger_formatter.rb
|
|
260
282
|
- lib/jekyll-l10n/utils/page_locales_config.rb
|
|
261
283
|
- lib/jekyll-l10n/utils/po_entry_converter.rb
|