jekyll-l10n 2.0.0 → 3.0.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +44 -0
  3. data/lib/jekyll-l10n/configuration/schema.rb +308 -0
  4. data/lib/jekyll-l10n/configuration.rb +165 -0
  5. data/lib/jekyll-l10n/constants.rb +45 -10
  6. data/lib/jekyll-l10n/errors.rb +7 -0
  7. data/lib/jekyll-l10n/extraction/compendium_merger.rb +9 -11
  8. data/lib/jekyll-l10n/extraction/compendium_translator.rb +27 -11
  9. data/lib/jekyll-l10n/extraction/config_loader.rb +4 -4
  10. data/lib/jekyll-l10n/extraction/coverage_report.rb +129 -0
  11. data/lib/jekyll-l10n/extraction/dom_text_extractor.rb +2 -31
  12. data/lib/jekyll-l10n/extraction/extractor.rb +48 -28
  13. data/lib/jekyll-l10n/extraction/html_string_extractor.rb +24 -10
  14. data/lib/jekyll-l10n/extraction/logger.rb +6 -5
  15. data/lib/jekyll-l10n/extraction/result_saver.rb +5 -10
  16. data/lib/jekyll-l10n/instrumentation.rb +172 -139
  17. data/lib/jekyll-l10n/jekyll/generator.rb +10 -22
  18. data/lib/jekyll-l10n/jekyll/localized_page.rb +27 -49
  19. data/lib/jekyll-l10n/jekyll/post_write_html_reprocessor.rb +42 -25
  20. data/lib/jekyll-l10n/jekyll/post_write_processor.rb +2 -1
  21. data/lib/jekyll-l10n/jekyll/regeneration_checker.rb +16 -10
  22. data/lib/jekyll-l10n/jekyll/url_filter.rb +8 -7
  23. data/lib/jekyll-l10n/po_file/entry_parser.rb +155 -0
  24. data/lib/jekyll-l10n/po_file/fuzzy_matcher.rb +67 -53
  25. data/lib/jekyll-l10n/po_file/loader.rb +20 -11
  26. data/lib/jekyll-l10n/po_file/manager.rb +32 -17
  27. data/lib/jekyll-l10n/po_file/merger.rb +2 -1
  28. data/lib/jekyll-l10n/po_file/path_builder.rb +11 -1
  29. data/lib/jekyll-l10n/po_file/reader.rb +40 -508
  30. data/lib/jekyll-l10n/po_file/writer.rb +31 -16
  31. data/lib/jekyll-l10n/translation/block_text_extractor.rb +3 -7
  32. data/lib/jekyll-l10n/translation/html_translator.rb +136 -157
  33. data/lib/jekyll-l10n/translation/libre_translate_languages.rb +73 -0
  34. data/lib/jekyll-l10n/translation/libre_translator.rb +134 -78
  35. data/lib/jekyll-l10n/translation/request_slots.rb +69 -0
  36. data/lib/jekyll-l10n/translation/translation_progress.rb +77 -0
  37. data/lib/jekyll-l10n/translation/translator.rb +20 -17
  38. data/lib/jekyll-l10n/utils/baseurl_normalizer.rb +108 -0
  39. data/lib/jekyll-l10n/utils/debug_logger.rb +4 -51
  40. data/lib/jekyll-l10n/utils/error_handler.rb +9 -24
  41. data/lib/jekyll-l10n/utils/external_link_icon_preserver.rb +5 -62
  42. data/lib/jekyll-l10n/utils/html_parser.rb +24 -31
  43. data/lib/jekyll-l10n/utils/html_text_utils.rb +146 -91
  44. data/lib/jekyll-l10n/utils/logger_formatter.rb +6 -4
  45. data/lib/jekyll-l10n/utils/msgstr_validator.rb +74 -0
  46. data/lib/jekyll-l10n/utils/page_locales_config.rb +126 -107
  47. data/lib/jekyll-l10n/utils/po_entry_converter.rb +0 -43
  48. data/lib/jekyll-l10n/utils/site_config_accessor.rb +56 -54
  49. data/lib/jekyll-l10n/utils/translation_resolver.rb +56 -6
  50. data/lib/jekyll-l10n/utils/url_path_builder.rb +5 -27
  51. data/lib/jekyll-l10n/utils/url_transformer.rb +5 -11
  52. data/lib/jekyll-l10n/version.rb +1 -1
  53. data/lib/jekyll-l10n.rb +9 -6
  54. metadata +12 -3
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../constants'
4
+
3
5
  module Jekyll
4
6
  module L10n
5
7
  # Formats and conditionally logs messages with component prefixes.
@@ -25,7 +27,7 @@ module Jekyll
25
27
  # @param message [String] Message to log
26
28
  # @return [void]
27
29
  def self.info(component, message)
28
- Jekyll.logger.info 'Localization', "[#{component}] #{message}"
30
+ Jekyll.logger.info Constants::LOG_PREFIX, "[#{component}] #{message}"
29
31
  end
30
32
 
31
33
  # Log a debug message with component prefix.
@@ -34,7 +36,7 @@ module Jekyll
34
36
  # @param message [String] Message to log
35
37
  # @return [void]
36
38
  def self.debug(component, message)
37
- Jekyll.logger.debug 'Localization', "[#{component}] #{message}"
39
+ Jekyll.logger.debug Constants::LOG_PREFIX, "[#{component}] #{message}"
38
40
  end
39
41
 
40
42
  # Log a warning message with component prefix.
@@ -43,7 +45,7 @@ module Jekyll
43
45
  # @param message [String] Message to log
44
46
  # @return [void]
45
47
  def self.warn(component, message)
46
- Jekyll.logger.warn 'Localization', "[#{component}] #{message}"
48
+ Jekyll.logger.warn Constants::LOG_PREFIX, "[#{component}] #{message}"
47
49
  end
48
50
 
49
51
  # Log an error message with component prefix.
@@ -52,7 +54,7 @@ module Jekyll
52
54
  # @param message [String] Message to log
53
55
  # @return [void]
54
56
  def self.error(component, message)
55
- Jekyll.logger.error 'Localization', "[#{component}] #{message}"
57
+ Jekyll.logger.error Constants::LOG_PREFIX, "[#{component}] #{message}"
56
58
  end
57
59
 
58
60
  # Check if debug logging is enabled.
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../constants'
4
+ require_relative 'html_parser'
5
+
6
+ module Jekyll
7
+ module L10n
8
+ # Checks that a msgstr carries the same markup as its msgid.
9
+ #
10
+ # A block's msgid holds placeholders (<g id="N">, <x id="N"></x>) and
11
+ # literal elements such as <code>. Translation writes the msgstr into the
12
+ # page as HTML, so a msgstr that drops or repeats a placeholder, or adds
13
+ # markup of its own (a <script>, an attribute), would change the page
14
+ # structure. Such entries are dropped and the page shows the fallback.
15
+ #
16
+ # Both strings are parsed, and their elements are compared with their
17
+ # attributes, in any order and nesting: a translation may reorder
18
+ # placeholders, but not change them.
19
+ #
20
+ # @example
21
+ # MsgstrValidator.valid?('See <g id="1">docs</g>.', 'Voir <g id="1">docs</g>.') # => true
22
+ # MsgstrValidator.valid?('See <g id="1">docs</g>.', 'Voir docs.') # => false
23
+ module MsgstrValidator
24
+ module_function
25
+
26
+ # @param msgid [String]
27
+ # @param msgstr [String]
28
+ # @return [Boolean] true when both hold the same elements and attributes
29
+ def valid?(msgid, msgstr)
30
+ return true unless msgid.include?('<') || msgstr.include?('<')
31
+
32
+ # The answer depends only on the two strings, and a build checks the
33
+ # same pairs each time a compendium is loaded again.
34
+ results.fetch([msgid, msgstr]) { results[[msgid, msgstr]] = markup(msgid) == markup(msgstr) }
35
+ end
36
+
37
+ # Drop the entries whose msgstr markup does not match their msgid.
38
+ #
39
+ # A compendium also keeps entries for text the site no longer has, so
40
+ # each one is only logged at debug level here; CoverageReport warns about
41
+ # the ones the site uses, once per build.
42
+ #
43
+ # @param translations [Hash{String => String}] msgid => msgstr
44
+ # @param po_path [String] The PO file the entries come from, for the log
45
+ # @return [Hash{String => String}] The valid entries
46
+ def reject_invalid(translations, po_path)
47
+ translations.select do |msgid, msgstr|
48
+ next true if msgstr.empty? || valid?(msgid, msgstr)
49
+
50
+ log_invalid(msgid, po_path)
51
+ false
52
+ end
53
+ end
54
+
55
+ def markup(text)
56
+ HtmlParser.parse_fragment(text).css('*').map do |element|
57
+ [element.name, element.attribute_nodes.map { |attr| [attr.name, attr.value] }.sort]
58
+ end.sort
59
+ end
60
+
61
+ def log_invalid(msgid, po_path)
62
+ Jekyll.logger.debug Constants::LOG_PREFIX,
63
+ "#{po_path}: msgstr markup does not match its msgid, dropping the " \
64
+ "entry: #{msgid[0..Constants::LOG_TRUNCATE_LONG]}"
65
+ end
66
+
67
+ def results
68
+ @results ||= {}
69
+ end
70
+
71
+ private_class_method :markup, :log_invalid, :results
72
+ end
73
+ end
74
+ end
@@ -1,20 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../configuration'
3
4
  require_relative '../constants'
4
5
 
5
6
  module Jekyll
6
7
  module L10n
7
- # Wraps a `with_locales_data` hash and answers questions about it, filling in
8
- # the defaults from Constants for anything not set.
8
+ # Wraps the plugin's settings and answers questions about them, filling in
9
+ # the defaults from Schema::SETTINGS for anything not set.
9
10
  #
10
- # It raises InvalidConfigurationError right away for bad locale codes or bad
11
- # LibreTranslate settings, so mistakes stop the build early.
11
+ # When LibreTranslate is enabled but its URL, source locale or format is
12
+ # invalid, it warns once and turns machine translation off, so the build
13
+ # still finishes. Bad locale codes never reach it: Configuration::Schema
14
+ # drops them, with a warning.
12
15
  #
13
16
  # Build it from the right data:
14
17
  # - For one page, use SiteConfigAccessor.effective_page_data(page), never raw
15
18
  # `page.data`; otherwise the page loses the site defaults.
16
- # - For site-wide settings, use
17
- # `{ 'with_locales_data' => SiteConfigAccessor.extract_locales_data(site) }`.
19
+ # - For site-wide settings (the `jekyll_l10n` key), use
20
+ # SiteConfigAccessor.site_config(site). A page's config has none of
21
+ # them, so its site-wide readers return the defaults.
18
22
  #
19
23
  # @example
20
24
  # config = PageLocalesConfig.new(SiteConfigAccessor.effective_page_data(page))
@@ -22,40 +26,24 @@ module Jekyll
22
26
  # config.fallback_mode # => "english"
23
27
  #
24
28
  class PageLocalesConfig
25
- # Shortcuts to the defaults in Constants.
26
- LOCALE_PATTERN = Constants::LOCALE_PATTERN
27
- DEFAULT_LOCALES_DIR = Constants::DEFAULT_LOCALES_DIR
28
- DEFAULT_FALLBACK_MODE = Constants::DEFAULT_FALLBACK_MODE
29
- DEFAULT_TRANSLATABLE_ATTRIBUTES = Constants::DEFAULT_TRANSLATABLE_ATTRIBUTES
30
- DEFAULT_LIBRETRANSLATE_TIMEOUT = Constants::DEFAULT_LIBRETRANSLATE_TIMEOUT
31
- DEFAULT_LIBRETRANSLATE_BATCH_SIZE = Constants::DEFAULT_LIBRETRANSLATE_BATCH_SIZE
32
- DEFAULT_LIBRETRANSLATE_RETRY_ATTEMPTS = Constants::DEFAULT_LIBRETRANSLATE_RETRY_ATTEMPTS
33
- DEFAULT_LIBRETRANSLATE_RETRY_DELAY = Constants::DEFAULT_LIBRETRANSLATE_RETRY_DELAY
34
- DEFAULT_LIBRETRANSLATE_STOP_ON_ERROR = Constants::DEFAULT_LIBRETRANSLATE_STOP_ON_ERROR
35
- DEFAULT_LIBRETRANSLATE_PROGRESS_INTERVAL = Constants::DEFAULT_LIBRETRANSLATE_PROGRESS_INTERVAL
36
- DEFAULT_LIBRETRANSLATE_SOURCE_LOCALE = Constants::DEFAULT_LIBRETRANSLATE_SOURCE_LOCALE
37
- DEFAULT_LIBRETRANSLATE_FORMAT = Constants::DEFAULT_LIBRETRANSLATE_FORMAT
38
-
39
29
  # @!attribute [r] data
40
30
  # The hash this config was built from
41
31
  # @return [Hash]
42
32
  attr_reader :data
43
33
 
44
- # @param page_data [Hash] A hash with a `with_locales_data` key, and
45
- # optionally `path` (used in error messages)
46
- # @raise [Jekyll::Errors::InvalidConfigurationError] If a locale code or a
47
- # LibreTranslate setting is invalid
34
+ # @param page_data [Hash] A hash with a `with_locales_data` key
48
35
  def initialize(page_data)
49
- @config = page_data['with_locales_data'] || {}
36
+ # Cleaned without warnings: Configuration warns where values are written.
37
+ # Both scopes are kept, so SiteConfigAccessor.site_config can pass the
38
+ # site-wide settings merged in.
39
+ @config = Configuration::Schema.normalize(page_data[Configuration::PAGE_DATA_KEY], source: nil, scope: :all)
50
40
  @data = page_data
51
- @page_path = page_data['path'] || 'unknown'
52
-
53
- validate_locales!
54
- validate_libretranslate!
41
+ @libretranslate_problem = libretranslate_problem if libretranslate_requested?
42
+ warn_libretranslate_off if @libretranslate_problem
55
43
  end
56
44
 
57
- # Locales to translate into. The constructor has already checked each code
58
- # against LOCALE_PATTERN.
45
+ # Locales to translate into. Configuration::Schema has already dropped
46
+ # any code that does not match Constants::LOCALE_PATTERN.
59
47
  #
60
48
  # @return [Array<String>] Locale codes such as ["es", "pt_BR"], or []
61
49
  def locales
@@ -64,35 +52,35 @@ module Jekyll
64
52
 
65
53
  # @return [String] Directory for PO files, relative to the site source
66
54
  def locales_dir
67
- @config['locales_dir'] || DEFAULT_LOCALES_DIR
55
+ @config['locales_dir'] || schema_default('locales_dir')
68
56
  end
69
57
 
70
58
  # @return [Boolean] false only when `extract_on_build` is false
71
59
  def extract_on_build?
72
- @config['extract_on_build'] != false
60
+ @config.fetch('extract_on_build', schema_default('extract_on_build'))
73
61
  end
74
62
 
75
63
  # Whether to merge page PO files into the locale compendium. A site-wide
76
- # setting: read it from the site-wide config, not from a page.
64
+ # setting: read it from SiteConfigAccessor.site_config, not from a page.
77
65
  #
78
66
  # @return [Boolean] false only when `update_compendium` is false
79
67
  def update_compendium?
80
- @config['update_compendium'] != false
68
+ @config.fetch('update_compendium', schema_default('update_compendium'))
81
69
  end
82
70
 
83
71
  # Whether to print the extraction summary at the end of the build. A
84
- # site-wide setting: Extractor reads it from the site-wide config.
72
+ # site-wide setting: read it from SiteConfigAccessor.site_config.
85
73
  #
86
74
  # @return [Boolean] false only when `logging.show_statistics` is false
87
75
  def show_statistics?
88
- @config.dig('logging', 'show_statistics') != false
76
+ logging_setting('show_statistics')
89
77
  end
90
78
 
91
79
  # Turns on HtmlTranslator's log of each translation lookup for this page.
92
80
  #
93
81
  # @return [Boolean] true when `logging.debug` is true
94
82
  def debug_logging?
95
- @config.dig('logging', 'debug') == true
83
+ logging_setting('debug')
96
84
  end
97
85
 
98
86
  # Turns on per-entry LibreTranslate logging. Those lines go to Jekyll's
@@ -100,7 +88,7 @@ module Jekyll
100
88
  #
101
89
  # @return [Boolean] true when `logging.trace` or `logging.debug` is true
102
90
  def trace_logging?
103
- @config.dig('logging', 'trace') == true || debug_logging?
91
+ logging_setting('trace') || debug_logging?
104
92
  end
105
93
 
106
94
  # What to show when a string has no translation: "english" (the original
@@ -108,15 +96,32 @@ module Jekyll
108
96
  #
109
97
  # @return [String] The fallback mode
110
98
  def fallback_mode
111
- @config.dig('translation', 'fallback') || DEFAULT_FALLBACK_MODE
99
+ @config.dig('translation', 'fallback') || schema_default('translation.fallback')
100
+ end
101
+
102
+ # The share of each locale's compendium that must be translated, or the
103
+ # build fails. A site-wide setting: read it from
104
+ # SiteConfigAccessor.site_config.
105
+ #
106
+ # @return [Integer, nil] A percentage, or nil when no minimum is set
107
+ def min_coverage
108
+ @config.dig('translation', 'min_coverage')
112
109
  end
113
110
 
114
111
  # HTML attributes whose values are extracted and translated. Defaults to
115
- # DEFAULT_TRANSLATABLE_ATTRIBUTES.
112
+ # Constants::DEFAULT_TRANSLATABLE_ATTRIBUTES.
116
113
  #
117
114
  # @return [Array<String>] Attribute names
118
115
  def translatable_attributes
119
- @config.dig('extraction', 'translatable_attributes') || DEFAULT_TRANSLATABLE_ATTRIBUTES
116
+ @config.dig('extraction', 'translatable_attributes') || schema_default('extraction.translatable_attributes')
117
+ end
118
+
119
+ # CSS selectors of elements left out of extraction. Defaults to
120
+ # Constants::DEFAULT_EXCLUDE_SELECTORS.
121
+ #
122
+ # @return [Array<String>] Selectors
123
+ def exclude_selectors
124
+ @config.dig('extraction', 'exclude_selectors') || schema_default('extraction.exclude_selectors')
120
125
  end
121
126
 
122
127
  # @return [Boolean] true when at least one locale is configured
@@ -128,37 +133,34 @@ module Jekyll
128
133
  # `libretranslate_enabled` always wins. Without it, only a
129
134
  # `libretranslate_api_url` you set yourself turns it on; the default URL
130
135
  # does not, so an unconfigured site never tries to reach a server.
136
+ # Either way, it stays off while its URL, source locale or format is
137
+ # invalid.
131
138
  #
132
139
  # @return [Boolean] true if LibreTranslate is enabled
133
140
  def libretranslate_enabled?
134
- if libretranslate_config.key?('libretranslate_enabled')
135
- return libretranslate_config['libretranslate_enabled'] == true
136
- end
137
-
138
- url = libretranslate_config['libretranslate_api_url']
139
- url.is_a?(String) && !url.strip.empty?
141
+ libretranslate_requested? && @libretranslate_problem.nil?
140
142
  end
141
143
 
142
144
  # @return [String] Locale code of the original content. Defaults to
143
- # DEFAULT_LIBRETRANSLATE_SOURCE_LOCALE.
145
+ # Constants::DEFAULT_LIBRETRANSLATE_SOURCE_LOCALE.
144
146
  def libretranslate_source_locale
145
- libretranslate_setting('libretranslate_source_locale', DEFAULT_LIBRETRANSLATE_SOURCE_LOCALE)
147
+ libretranslate_setting('libretranslate_source_locale')
146
148
  end
147
149
 
148
150
  # How strings are sent to LibreTranslate. "html" keeps inline markup intact.
149
151
  #
150
- # @return [String] "text" or "html". Defaults to DEFAULT_LIBRETRANSLATE_FORMAT.
152
+ # @return [String] "text" or "html". Default from Configuration::Schema::SETTINGS.
151
153
  def libretranslate_format
152
- libretranslate_setting('libretranslate_format', DEFAULT_LIBRETRANSLATE_FORMAT)
154
+ libretranslate_setting('libretranslate_format')
153
155
  end
154
156
 
155
157
  # Base URL of the LibreTranslate server, without `/translate`, which
156
158
  # LibreTranslator appends. Returning the default here does not turn
157
159
  # LibreTranslate on; see {#libretranslate_enabled?}.
158
160
  #
159
- # @return [String] The URL. Defaults to Constants::DEFAULT_LIBRETRANSLATE_API_URL.
161
+ # @return [String] The URL. Default from Configuration::Schema::SETTINGS.
160
162
  def libretranslate_api_url
161
- libretranslate_setting('libretranslate_api_url', Constants::DEFAULT_LIBRETRANSLATE_API_URL)
163
+ libretranslate_setting('libretranslate_api_url')
162
164
  end
163
165
 
164
166
  # @return [String, nil] API key for servers that require one, or nil
@@ -167,45 +169,60 @@ module Jekyll
167
169
  end
168
170
 
169
171
  # @return [Integer] Request timeout in seconds. Defaults to
170
- # DEFAULT_LIBRETRANSLATE_TIMEOUT.
172
+ # Constants::DEFAULT_LIBRETRANSLATE_TIMEOUT.
171
173
  def libretranslate_timeout
172
- libretranslate_setting('libretranslate_timeout', DEFAULT_LIBRETRANSLATE_TIMEOUT)
174
+ libretranslate_setting('libretranslate_timeout')
173
175
  end
174
176
 
175
- # Strings sent per request. Larger batches mean fewer requests but may
176
- # exceed the server's size limit.
177
+ # Entries a worker takes from the queue at a time. Each entry is still
178
+ # its own request.
177
179
  #
178
- # @return [Integer] Defaults to DEFAULT_LIBRETRANSLATE_BATCH_SIZE.
180
+ # @return [Integer] Default from Configuration::Schema::SETTINGS.
179
181
  def libretranslate_batch_size
180
- libretranslate_setting('libretranslate_batch_size', DEFAULT_LIBRETRANSLATE_BATCH_SIZE)
182
+ libretranslate_setting('libretranslate_batch_size')
183
+ end
184
+
185
+ # Requests sent to LibreTranslate at once. 1 sends them one at a time.
186
+ #
187
+ # @return [Integer] Default from Configuration::Schema::SETTINGS.
188
+ def libretranslate_concurrency
189
+ libretranslate_setting('libretranslate_concurrency')
181
190
  end
182
191
 
183
192
  # @return [Integer] Retries for a failed request. Defaults to
184
- # DEFAULT_LIBRETRANSLATE_RETRY_ATTEMPTS.
193
+ # Constants::DEFAULT_LIBRETRANSLATE_RETRY_ATTEMPTS.
185
194
  def libretranslate_retry_attempts
186
- libretranslate_setting('libretranslate_retry_attempts', DEFAULT_LIBRETRANSLATE_RETRY_ATTEMPTS)
195
+ libretranslate_setting('libretranslate_retry_attempts')
187
196
  end
188
197
 
189
198
  # @return [Integer] Seconds to wait between retries. Defaults to
190
- # DEFAULT_LIBRETRANSLATE_RETRY_DELAY.
199
+ # Constants::DEFAULT_LIBRETRANSLATE_RETRY_DELAY.
191
200
  def libretranslate_retry_delay
192
- libretranslate_setting('libretranslate_retry_delay', DEFAULT_LIBRETRANSLATE_RETRY_DELAY)
201
+ libretranslate_setting('libretranslate_retry_delay')
193
202
  end
194
203
 
195
204
  # When true, the first API error stops machine translation. When false, the
196
205
  # error is logged and the remaining entries are still translated.
197
206
  #
198
- # @return [Boolean] Defaults to Constants::DEFAULT_LIBRETRANSLATE_STOP_ON_ERROR.
207
+ # @return [Boolean] Default from Configuration::Schema::SETTINGS.
199
208
  def libretranslate_stop_on_error?
200
209
  val = libretranslate_config['libretranslate_stop_on_error']
201
- val.nil? ? Constants::DEFAULT_LIBRETRANSLATE_STOP_ON_ERROR : val != false
210
+ val.nil? ? schema_default('translation.libretranslate_stop_on_error') : val != false
211
+ end
212
+
213
+ # When true, machine translations are written as fuzzy entries: pages
214
+ # keep the fallback until a person reviews each one and removes the flag.
215
+ #
216
+ # @return [Boolean] Default from Configuration::Schema::SETTINGS.
217
+ def libretranslate_mark_fuzzy?
218
+ libretranslate_config.fetch('libretranslate_mark_fuzzy', schema_default('translation.libretranslate_mark_fuzzy'))
202
219
  end
203
220
 
204
221
  # Log progress after every this many translated entries; 0 turns it off.
205
222
  #
206
- # @return [Integer] Defaults to DEFAULT_LIBRETRANSLATE_PROGRESS_INTERVAL.
223
+ # @return [Integer] Default from Configuration::Schema::SETTINGS.
207
224
  def libretranslate_progress_interval
208
- libretranslate_setting('libretranslate_progress_interval', DEFAULT_LIBRETRANSLATE_PROGRESS_INTERVAL)
225
+ libretranslate_setting('libretranslate_progress_interval')
209
226
  end
210
227
 
211
228
  private
@@ -214,55 +231,57 @@ module Jekyll
214
231
  @config['translation'] || {}
215
232
  end
216
233
 
217
- def libretranslate_setting(key, default)
218
- libretranslate_config[key] || default
234
+ # Reads a logging setting by its leaf key, falling back to the default
235
+ # recorded in Schema::SETTINGS for "logging.<key>".
236
+ #
237
+ # @param key [String] leaf key under the `logging` section
238
+ # @return [Boolean]
239
+ def logging_setting(key)
240
+ (@config['logging'] || {}).fetch(key, schema_default("logging.#{key}"))
219
241
  end
220
242
 
221
- def validate_locales!
222
- invalid = locales.grep_v(LOCALE_PATTERN)
223
-
224
- return if invalid.empty?
225
-
226
- error_msg = build_validation_error_message(invalid)
227
-
228
- Jekyll.logger.error('Localization', error_msg)
229
-
230
- raise Jekyll::Errors::InvalidConfigurationError, error_msg
243
+ # Returns the configured value for a libretranslate setting, or the default
244
+ # recorded in Schema::SETTINGS for "translation.<key>".
245
+ #
246
+ # @param key [String] leaf key under the `translation` section
247
+ # @return [Object]
248
+ def libretranslate_setting(key)
249
+ libretranslate_config[key] || schema_default("translation.#{key}")
231
250
  end
232
251
 
233
- def build_validation_error_message(invalid_codes)
234
- <<~ERROR
235
- Invalid locale codes in #{@page_path}: #{invalid_codes.join(', ')}
236
-
237
- Expected ISO 639-1 language code, optionally with ISO 3166-1 alpha-2 country (e.g., 'es', 'fr', 'pt_BR')
238
- Valid pattern: ^[a-z]{2}(_[A-Z]{2})?$
252
+ # Whether the settings ask for LibreTranslate, before they are checked.
253
+ def libretranslate_requested?
254
+ if libretranslate_config.key?('libretranslate_enabled')
255
+ return libretranslate_config['libretranslate_enabled'] == true
256
+ end
239
257
 
240
- Common mistakes:
241
- - Use lowercase language code: 'es' not 'ES' or 'ESP'
242
- - Use underscore for country: 'pt_BR' not 'pt-BR'
243
- - Use ISO codes: 'es' not 'español'
244
- - Minimum 2 characters: 'es' not 'e'
245
- - Country code uppercase: 'pt_BR' not 'pt_br'
246
- ERROR
258
+ url = libretranslate_config['libretranslate_api_url']
259
+ url.is_a?(String) && !url.strip.empty?
247
260
  end
248
261
 
249
- def validate_libretranslate!
250
- return unless libretranslate_enabled?
251
-
252
- if libretranslate_api_url.nil? || libretranslate_api_url.empty?
253
- raise Jekyll::Errors::InvalidConfigurationError,
254
- 'LibreTranslate is enabled but libretranslate_api_url is not configured'
255
- end
256
-
257
- unless LOCALE_PATTERN.match?(libretranslate_source_locale)
258
- raise Jekyll::Errors::InvalidConfigurationError,
259
- "Invalid libretranslate_source_locale: #{libretranslate_source_locale}"
262
+ # @return [String, nil] Why the LibreTranslate settings cannot be used,
263
+ # or nil when they can
264
+ def libretranslate_problem
265
+ if libretranslate_api_url.strip.empty?
266
+ 'libretranslate_api_url is empty'
267
+ elsif !Constants::LOCALE_PATTERN.match?(libretranslate_source_locale)
268
+ "libretranslate_source_locale #{libretranslate_source_locale.inspect} is not a locale code such as en or pt_BR"
269
+ elsif !%w[text html].include?(libretranslate_format)
270
+ "libretranslate_format #{libretranslate_format.inspect} must be \"text\" or \"html\""
260
271
  end
272
+ end
261
273
 
262
- return if %w[text html].include?(libretranslate_format)
274
+ def warn_libretranslate_off
275
+ Configuration.warn_once("LibreTranslate is turned off: #{@libretranslate_problem}; " \
276
+ 'fix it under `jekyll_l10n.translation` in _config.yml.')
277
+ end
263
278
 
264
- raise Jekyll::Errors::InvalidConfigurationError,
265
- "Invalid libretranslate_format: #{libretranslate_format} (must be 'text' or 'html')"
279
+ # Returns the default value for a setting path from Schema::SETTINGS.
280
+ #
281
+ # @param path [String] dotted setting path, e.g. "locales_dir"
282
+ # @return [Object] the default value
283
+ def schema_default(path)
284
+ Configuration::Schema::SETTINGS.fetch(path)[:default]
266
285
  end
267
286
  end
268
287
  end
@@ -14,40 +14,12 @@ module Jekyll
14
14
  # * Convert translation hashes to POEntry objects
15
15
  # * Convert POEntry objects back to hash format
16
16
  # * Preserve metadata (references, fuzzy flags) during conversion
17
- # * Handle both hash and array return formats
18
17
  #
19
18
  # @example
20
19
  # hash = { "Hello" => { msgstr: "Hola", reference: "file.html:10" } }
21
20
  # entries = PoEntryConverter.hash_to_po_entry_array(hash)
22
21
  # # entries is array of GetText::POEntry objects
23
22
  class PoEntryConverter
24
- # Convert translation hash to POEntry hash.
25
- #
26
- # Converts simple translation hash { msgid => msgstr } or metadata format
27
- # { msgid => { msgstr: "...", reference: "...", fuzzy: false } } to a
28
- # hash of GetText::POEntry objects keyed by msgid.
29
- #
30
- # @param hash [Hash] Translation hash
31
- # @return [Hash] Hash of { msgid => POEntry }
32
- def self.hash_to_po_entries(hash)
33
- return {} if hash.nil? || hash.empty?
34
-
35
- hash.each_with_object({}) do |(key, value), result|
36
- entry = ::GetText::POEntry.new(:normal)
37
- entry.msgid = key
38
-
39
- if value.is_a?(Hash)
40
- entry.msgstr = value[:msgstr]
41
- entry.add_comment(value[:reference]) if value[:reference]
42
- entry.flag = 'fuzzy' if value[:fuzzy]
43
- else
44
- entry.msgstr = value
45
- end
46
-
47
- result[key] = entry
48
- end
49
- end
50
-
51
23
  # Convert translation hash to array of POEntry objects.
52
24
  #
53
25
  # Converts hash format to array of GetText::POEntry objects, preserving
@@ -75,21 +47,6 @@ module Jekyll
75
47
  entry
76
48
  end
77
49
 
78
- # Convert POEntry objects to translation hash.
79
- #
80
- # @param entries [Array<GetText::POEntry>] Array of POEntry objects
81
- # @return [Hash] Hash of { msgid => { msgstr: "...", reference: "..." } }
82
- def self.po_entries_to_hash(entries)
83
- return {} if entries.nil? || entries.empty?
84
-
85
- entries.to_h do |entry|
86
- [entry.msgid, {
87
- msgstr: entry.msgstr.to_s,
88
- reference: entry.extracted_comment
89
- }]
90
- end
91
- end
92
-
93
50
  # Convert POEntry objects to array of hashes.
94
51
  #
95
52
  # Each entry becomes a hash with :msgid and :msgstr (and optional :reference).