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.
- checksums.yaml +4 -4
- data/README.md +44 -0
- data/lib/jekyll-l10n/configuration/schema.rb +308 -0
- data/lib/jekyll-l10n/configuration.rb +165 -0
- data/lib/jekyll-l10n/constants.rb +45 -10
- data/lib/jekyll-l10n/errors.rb +7 -0
- data/lib/jekyll-l10n/extraction/compendium_merger.rb +9 -11
- data/lib/jekyll-l10n/extraction/compendium_translator.rb +27 -11
- data/lib/jekyll-l10n/extraction/config_loader.rb +4 -4
- data/lib/jekyll-l10n/extraction/coverage_report.rb +129 -0
- data/lib/jekyll-l10n/extraction/dom_text_extractor.rb +2 -31
- data/lib/jekyll-l10n/extraction/extractor.rb +48 -28
- data/lib/jekyll-l10n/extraction/html_string_extractor.rb +24 -10
- data/lib/jekyll-l10n/extraction/logger.rb +6 -5
- data/lib/jekyll-l10n/extraction/result_saver.rb +5 -10
- data/lib/jekyll-l10n/instrumentation.rb +172 -139
- data/lib/jekyll-l10n/jekyll/generator.rb +10 -22
- data/lib/jekyll-l10n/jekyll/localized_page.rb +27 -49
- data/lib/jekyll-l10n/jekyll/post_write_html_reprocessor.rb +42 -25
- data/lib/jekyll-l10n/jekyll/post_write_processor.rb +2 -1
- data/lib/jekyll-l10n/jekyll/regeneration_checker.rb +16 -10
- data/lib/jekyll-l10n/jekyll/url_filter.rb +8 -7
- data/lib/jekyll-l10n/po_file/entry_parser.rb +155 -0
- data/lib/jekyll-l10n/po_file/fuzzy_matcher.rb +67 -53
- data/lib/jekyll-l10n/po_file/loader.rb +20 -11
- data/lib/jekyll-l10n/po_file/manager.rb +32 -17
- data/lib/jekyll-l10n/po_file/merger.rb +2 -1
- data/lib/jekyll-l10n/po_file/path_builder.rb +11 -1
- data/lib/jekyll-l10n/po_file/reader.rb +40 -508
- data/lib/jekyll-l10n/po_file/writer.rb +31 -16
- data/lib/jekyll-l10n/translation/block_text_extractor.rb +3 -7
- data/lib/jekyll-l10n/translation/html_translator.rb +136 -157
- data/lib/jekyll-l10n/translation/libre_translate_languages.rb +73 -0
- data/lib/jekyll-l10n/translation/libre_translator.rb +134 -78
- data/lib/jekyll-l10n/translation/request_slots.rb +69 -0
- data/lib/jekyll-l10n/translation/translation_progress.rb +77 -0
- data/lib/jekyll-l10n/translation/translator.rb +20 -17
- data/lib/jekyll-l10n/utils/baseurl_normalizer.rb +108 -0
- data/lib/jekyll-l10n/utils/debug_logger.rb +4 -51
- data/lib/jekyll-l10n/utils/error_handler.rb +9 -24
- data/lib/jekyll-l10n/utils/external_link_icon_preserver.rb +5 -62
- data/lib/jekyll-l10n/utils/html_parser.rb +24 -31
- data/lib/jekyll-l10n/utils/html_text_utils.rb +146 -91
- data/lib/jekyll-l10n/utils/logger_formatter.rb +6 -4
- data/lib/jekyll-l10n/utils/msgstr_validator.rb +74 -0
- data/lib/jekyll-l10n/utils/page_locales_config.rb +126 -107
- data/lib/jekyll-l10n/utils/po_entry_converter.rb +0 -43
- data/lib/jekyll-l10n/utils/site_config_accessor.rb +56 -54
- data/lib/jekyll-l10n/utils/translation_resolver.rb +56 -6
- data/lib/jekyll-l10n/utils/url_path_builder.rb +5 -27
- data/lib/jekyll-l10n/utils/url_transformer.rb +5 -11
- data/lib/jekyll-l10n/version.rb +1 -1
- data/lib/jekyll-l10n.rb +9 -6
- metadata +12 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'jekyll'
|
|
4
|
+
|
|
3
5
|
module Jekyll
|
|
4
6
|
module L10n
|
|
5
7
|
# OpenTelemetry instrumentation facade for jekyll-l10n.
|
|
@@ -7,7 +9,7 @@ module Jekyll
|
|
|
7
9
|
# All tracing is configured centrally in TRACED_METHODS — no span code lives
|
|
8
10
|
# in business logic classes. To add a span: append one entry. To rename a
|
|
9
11
|
# method: update the one entry. When a method is removed from its class the
|
|
10
|
-
# stale entry
|
|
12
|
+
# stale entry fails the TRACED_METHODS spec, signalling the entry to delete.
|
|
11
13
|
#
|
|
12
14
|
# Requires opentelemetry-api at runtime; falls back to a no-op if absent.
|
|
13
15
|
# Users opt in to real tracing by adding opentelemetry-sdk and
|
|
@@ -18,6 +20,161 @@ module Jekyll
|
|
|
18
20
|
module Instrumentation
|
|
19
21
|
TRACER_NAME = 'jekyll-l10n'
|
|
20
22
|
|
|
23
|
+
# -- Entry point -------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
# Returns true when OTel is requested via standard environment variables.
|
|
26
|
+
#
|
|
27
|
+
# install! guards on this so the prepend wrappers are only applied when
|
|
28
|
+
# a real exporter is configured. In CI and local tests (no OTel env vars)
|
|
29
|
+
# business logic classes are untouched, keeping allow_any_instance_of stubs
|
|
30
|
+
# and other RSpec mechanics fully functional.
|
|
31
|
+
def self.enabled?
|
|
32
|
+
ENV.key?('OTEL_EXPORTER_OTLP_ENDPOINT') || ENV.key?('OTEL_SERVICE_NAME')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Installs wrappers on all classes listed in TRACED_METHODS using Module#prepend.
|
|
36
|
+
#
|
|
37
|
+
# Called once at plugin load time (end of jekyll-l10n.rb, after all requires),
|
|
38
|
+
# but only when enabled? returns true. Use OTEL_EXPORTER_OTLP_ENDPOINT or
|
|
39
|
+
# OTEL_SERVICE_NAME to activate tracing.
|
|
40
|
+
def self.install!
|
|
41
|
+
return if @installed
|
|
42
|
+
|
|
43
|
+
@installed = true
|
|
44
|
+
setup_sdk!
|
|
45
|
+
TRACED_METHODS.group_by { |e| e[0] }.each do |class_name, entries|
|
|
46
|
+
prepend_wrappers(resolve_class(class_name), entries)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# -- Install phase -----------------------------------------------------
|
|
51
|
+
|
|
52
|
+
# Configures the OTel SDK from standard env vars if the SDK gem is available.
|
|
53
|
+
#
|
|
54
|
+
# Called from install! so that spans are exported to the configured backend
|
|
55
|
+
# without requiring sites to add a separate initializer. Silently no-ops when
|
|
56
|
+
# opentelemetry-sdk or opentelemetry-exporter-otlp is not installed.
|
|
57
|
+
#
|
|
58
|
+
# Uses BatchSpanProcessor, which exports from a background thread, so the
|
|
59
|
+
# build never waits on the collector while a span ends (SimpleSpanProcessor
|
|
60
|
+
# exports synchronously, inside every traced call — costly here, where each
|
|
61
|
+
# localized page produces several spans). Spans still reach the collector
|
|
62
|
+
# before Jekyll exits: the post_write hook below runs after every
|
|
63
|
+
# :low-or-higher post_write hook, this plugin's :high one included, and
|
|
64
|
+
# force-flushes the batch; at_exit shuts the provider down, exporting
|
|
65
|
+
# anything left (e.g. between serve rebuilds).
|
|
66
|
+
#
|
|
67
|
+
# jekyll-theme-centos loads several instrumented plugins in one process,
|
|
68
|
+
# and OpenTelemetry::SDK.configure unconditionally overwrites the global
|
|
69
|
+
# tracer_provider on every call — so only the first plugin to reach this
|
|
70
|
+
# method (see sdk_already_configured?) actually configures the SDK; every
|
|
71
|
+
# later plugin's call becomes a no-op and reuses that provider. The
|
|
72
|
+
# at_exit and post_write hooks below are therefore registered once, by
|
|
73
|
+
# whichever plugin wins, and cover every plugin's spans, not just this one's.
|
|
74
|
+
def self.setup_sdk!
|
|
75
|
+
return if sdk_already_configured?
|
|
76
|
+
|
|
77
|
+
# :nocov:
|
|
78
|
+
require 'opentelemetry/sdk'
|
|
79
|
+
require 'opentelemetry/exporter/otlp'
|
|
80
|
+
exporter = OpenTelemetry::Exporter::OTLP::Exporter.new
|
|
81
|
+
processor = OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
|
|
82
|
+
OpenTelemetry::SDK.configure { |c| c.add_span_processor(processor) }
|
|
83
|
+
|
|
84
|
+
at_exit { OpenTelemetry.tracer_provider&.shutdown }
|
|
85
|
+
Jekyll::Hooks.register(:site, :post_write, priority: 0) { OpenTelemetry.tracer_provider&.force_flush }
|
|
86
|
+
rescue LoadError
|
|
87
|
+
nil
|
|
88
|
+
# :nocov:
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# True when some plugin (this one or another) already installed a real
|
|
92
|
+
# SDK-backed tracer_provider, in which case setup_sdk! must not run again.
|
|
93
|
+
def self.sdk_already_configured?
|
|
94
|
+
defined?(OpenTelemetry::SDK::Trace::TracerProvider) &&
|
|
95
|
+
OpenTelemetry.tracer_provider.is_a?(OpenTelemetry::SDK::Trace::TracerProvider)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Resolves a dot-separated class name to a constant; returns nil on NameError.
|
|
99
|
+
# Public, unlike the other install! helpers, so specs can check TRACED_METHODS entries.
|
|
100
|
+
def self.resolve_class(name)
|
|
101
|
+
name.split('::').reduce(Object) { |m, c| m.const_get(c) }
|
|
102
|
+
rescue NameError
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Applies instance and class-method wrappers to klass; skips if klass is nil.
|
|
107
|
+
def self.prepend_wrappers(klass, entries)
|
|
108
|
+
return unless klass
|
|
109
|
+
|
|
110
|
+
class_entries, instance_entries = entries.partition { |e| e[1] == :class }
|
|
111
|
+
klass.prepend(build_wrapper_module(instance_entries)) unless instance_entries.empty?
|
|
112
|
+
klass.singleton_class.prepend(build_wrapper_module(class_entries)) unless class_entries.empty?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Builds an anonymous module that wraps each listed method in a span.
|
|
116
|
+
def self.build_wrapper_module(entries)
|
|
117
|
+
Module.new do
|
|
118
|
+
entries.each do |_class_name, method_type, method_name, span_name, attr_proc|
|
|
119
|
+
define_method(method_name) do |*args, **kwargs, &blk|
|
|
120
|
+
Instrumentation.instrument(span_name) do |span|
|
|
121
|
+
result = super(*args, **kwargs, &blk)
|
|
122
|
+
attr_proc&.call(span, self, args, result)
|
|
123
|
+
result
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
span.record_exception(e)
|
|
126
|
+
raise
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
private method_name if method_type == :private
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private_class_method :build_wrapper_module, :prepend_wrappers, :setup_sdk!, :sdk_already_configured?
|
|
135
|
+
|
|
136
|
+
# -- Runtime API -------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
# Returns the active OTel tracer, or a no-op tracer if opentelemetry-api is absent.
|
|
139
|
+
def self.tracer
|
|
140
|
+
@tracer ||=
|
|
141
|
+
if defined?(OpenTelemetry)
|
|
142
|
+
# :nocov:
|
|
143
|
+
OpenTelemetry.tracer_provider.tracer(TRACER_NAME, Jekyll::L10n::VERSION)
|
|
144
|
+
# :nocov:
|
|
145
|
+
else
|
|
146
|
+
NoopTracer.new
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Wraps a block in an OTel span.
|
|
151
|
+
#
|
|
152
|
+
# @param span_name [String] Dot-separated span name (e.g. 'l10n.extract_page')
|
|
153
|
+
# @param attributes [Hash] Initial span attributes
|
|
154
|
+
# @yieldparam span [OpenTelemetry::Trace::Span, NoopSpan] Active span
|
|
155
|
+
# @return [Object] The return value of the block
|
|
156
|
+
def self.instrument(span_name, attributes: {}, &block)
|
|
157
|
+
tracer.in_span(span_name, attributes: attributes, &block)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Thread.new that keeps the caller's trace context, so spans created in
|
|
161
|
+
# the thread nest under the span that started it. OTel tracks the
|
|
162
|
+
# current span per thread, and a plain Thread.new starts without one.
|
|
163
|
+
def self.traced_thread(&block)
|
|
164
|
+
return Thread.new(&block) unless defined?(OpenTelemetry::Context)
|
|
165
|
+
|
|
166
|
+
context = OpenTelemetry::Context.current
|
|
167
|
+
Thread.new { OpenTelemetry::Context.with_current(context, &block) }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Resets the cached tracer and installation flag. Call in tests after changing OTel configuration.
|
|
171
|
+
def self.reset!
|
|
172
|
+
@tracer = nil
|
|
173
|
+
@installed = false
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# -- Registry ----------------------------------------------------------
|
|
177
|
+
|
|
21
178
|
# Central instrumentation registry.
|
|
22
179
|
#
|
|
23
180
|
# Each row: [class_name, method_type, method_name, span_name, attribute_proc]
|
|
@@ -48,7 +205,7 @@ module Jekyll
|
|
|
48
205
|
end
|
|
49
206
|
|
|
50
207
|
TRACED_METHODS = [
|
|
51
|
-
#
|
|
208
|
+
# -- Jekyll integration -----------------------------------------------
|
|
52
209
|
['Jekyll::L10n::Generator', :instance, :generate, 'l10n.generate',
|
|
53
210
|
lambda { |span, _this, args, _result|
|
|
54
211
|
span.set_attribute('l10n.site_page_count', args[0]&.pages&.size || 0)
|
|
@@ -64,7 +221,7 @@ module Jekyll
|
|
|
64
221
|
span.set_attribute('l10n.page_url', this.page.url.to_s)
|
|
65
222
|
}],
|
|
66
223
|
|
|
67
|
-
#
|
|
224
|
+
# -- Extraction pipeline ----------------------------------------------
|
|
68
225
|
['Jekyll::L10n::Extractor', :instance, :extract_site, 'l10n.extract_site',
|
|
69
226
|
lambda { |span, _this, _args, result|
|
|
70
227
|
span.set_attribute('l10n.file_count', Instrumentation.hash_val(result, :files_processed))
|
|
@@ -102,7 +259,7 @@ module Jekyll
|
|
|
102
259
|
span.set_attribute('l10n.locale_count', config.locales.size) if config.respond_to?(:locales)
|
|
103
260
|
}],
|
|
104
261
|
|
|
105
|
-
#
|
|
262
|
+
# -- Translation pipeline ---------------------------------------------
|
|
106
263
|
['Jekyll::L10n::PostWriteHtmlReprocessor', :instance, :reprocess_localized_pages,
|
|
107
264
|
'l10n.reprocess_localized_pages', nil],
|
|
108
265
|
|
|
@@ -134,7 +291,7 @@ module Jekyll
|
|
|
134
291
|
span.set_attribute('l10n.batch_size', args[0].is_a?(Array) ? args[0].size : 1)
|
|
135
292
|
}],
|
|
136
293
|
|
|
137
|
-
#
|
|
294
|
+
# -- Utilities --------------------------------------------------------
|
|
138
295
|
['Jekyll::L10n::HtmlParser', :class, :parse_document, 'l10n.html_parse',
|
|
139
296
|
lambda { |span, _this, args, _result|
|
|
140
297
|
span.set_attribute('l10n.html_size_bytes', args[0].bytesize)
|
|
@@ -151,7 +308,16 @@ module Jekyll
|
|
|
151
308
|
['Jekyll::L10n::ExternalLinkIconPreserver', :class, :preserve,
|
|
152
309
|
'l10n.icon_preserve', nil],
|
|
153
310
|
|
|
154
|
-
#
|
|
311
|
+
# reject_invalid runs once per PO file load; valid? runs per entry, too often to trace
|
|
312
|
+
['Jekyll::L10n::MsgstrValidator', :class, :reject_invalid, 'l10n.msgstr_validate',
|
|
313
|
+
lambda { |span, _this, args, result|
|
|
314
|
+
entry_count = Instrumentation.hash_size(args[0])
|
|
315
|
+
span.set_attribute('l10n.file_path', args[1].to_s)
|
|
316
|
+
span.set_attribute('l10n.entry_count', entry_count)
|
|
317
|
+
span.set_attribute('l10n.invalid_count', entry_count - Instrumentation.hash_size(result))
|
|
318
|
+
}],
|
|
319
|
+
|
|
320
|
+
# -- PO file operations -----------------------------------------------
|
|
155
321
|
['Jekyll::L10n::PoFileReader', :instance, :parse_for_translation,
|
|
156
322
|
'l10n.po_file_read',
|
|
157
323
|
lambda { |span, this, _args, result|
|
|
@@ -167,139 +333,6 @@ module Jekyll
|
|
|
167
333
|
].freeze
|
|
168
334
|
# :nocov:
|
|
169
335
|
|
|
170
|
-
# Returns the active OTel tracer, or a no-op tracer if opentelemetry-api is absent.
|
|
171
|
-
def self.tracer
|
|
172
|
-
@tracer ||=
|
|
173
|
-
if defined?(OpenTelemetry)
|
|
174
|
-
# :nocov:
|
|
175
|
-
OpenTelemetry.tracer_provider.tracer(TRACER_NAME, Jekyll::L10n::VERSION)
|
|
176
|
-
# :nocov:
|
|
177
|
-
else
|
|
178
|
-
NoopTracer.new
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Wraps a block in an OTel span.
|
|
183
|
-
#
|
|
184
|
-
# @param span_name [String] Dot-separated span name (e.g. 'l10n.extract_page')
|
|
185
|
-
# @param attributes [Hash] Initial span attributes
|
|
186
|
-
# @yieldparam span [OpenTelemetry::Trace::Span, NoopSpan] Active span
|
|
187
|
-
# @return [Object] The return value of the block
|
|
188
|
-
def self.instrument(span_name, attributes: {}, &block)
|
|
189
|
-
tracer.in_span(span_name, attributes: attributes, &block)
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
# Resets the cached tracer and installation flag. Call in tests after changing OTel configuration.
|
|
193
|
-
def self.reset!
|
|
194
|
-
@tracer = nil
|
|
195
|
-
@installed = false
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# Returns true when OTel is requested via standard environment variables.
|
|
199
|
-
#
|
|
200
|
-
# install! guards on this so the prepend wrappers are only applied when
|
|
201
|
-
# a real exporter is configured. In CI and local tests (no OTel env vars)
|
|
202
|
-
# business logic classes are untouched, keeping allow_any_instance_of stubs
|
|
203
|
-
# and other RSpec mechanics fully functional.
|
|
204
|
-
def self.enabled?
|
|
205
|
-
ENV.key?('OTEL_EXPORTER_OTLP_ENDPOINT') || ENV.key?('OTEL_SERVICE_NAME')
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# Installs wrappers on all classes listed in TRACED_METHODS using Module#prepend.
|
|
209
|
-
#
|
|
210
|
-
# Called once at plugin load time (end of jekyll-l10n.rb, after all requires),
|
|
211
|
-
# but only when enabled? returns true. Use OTEL_EXPORTER_OTLP_ENDPOINT or
|
|
212
|
-
# OTEL_SERVICE_NAME to activate tracing.
|
|
213
|
-
def self.install!
|
|
214
|
-
return if @installed
|
|
215
|
-
|
|
216
|
-
@installed = true
|
|
217
|
-
setup_sdk!
|
|
218
|
-
TRACED_METHODS.group_by { |e| e[0] }.each do |class_name, entries|
|
|
219
|
-
prepend_wrappers(resolve_class(class_name), entries)
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# Configures the OTel SDK from standard env vars if the SDK gem is available.
|
|
224
|
-
#
|
|
225
|
-
# Called from install! so that spans are exported to the configured backend
|
|
226
|
-
# without requiring sites to add a separate initializer. Silently no-ops when
|
|
227
|
-
# opentelemetry-sdk or opentelemetry-exporter-otlp is not installed.
|
|
228
|
-
#
|
|
229
|
-
# SimpleSpanProcessor is used instead of the SDK default (BatchSpanProcessor)
|
|
230
|
-
# because Jekyll is a short-lived CLI process. BatchSpanProcessor exports on a
|
|
231
|
-
# 5-second schedule; outer spans that close last (post_write, generate) are
|
|
232
|
-
# silently dropped when the process exits before the next flush. Simple exports
|
|
233
|
-
# each span synchronously the moment it closes, guaranteeing no span loss.
|
|
234
|
-
#
|
|
235
|
-
# jekyll-theme-centos loads several instrumented plugins in one process,
|
|
236
|
-
# and OpenTelemetry::SDK.configure unconditionally overwrites the global
|
|
237
|
-
# tracer_provider on every call — so only the first plugin to reach this
|
|
238
|
-
# method (see sdk_already_configured?) actually configures the SDK; every
|
|
239
|
-
# later plugin's call becomes a no-op and reuses that provider. The
|
|
240
|
-
# at_exit and post_write hooks below are therefore registered once, by
|
|
241
|
-
# whichever plugin wins, and cover every plugin's spans, not just this one's.
|
|
242
|
-
def self.setup_sdk!
|
|
243
|
-
return if sdk_already_configured?
|
|
244
|
-
|
|
245
|
-
# :nocov:
|
|
246
|
-
require 'opentelemetry/sdk'
|
|
247
|
-
require 'opentelemetry/exporter/otlp'
|
|
248
|
-
exporter = OpenTelemetry::Exporter::OTLP::Exporter.new
|
|
249
|
-
processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter)
|
|
250
|
-
OpenTelemetry::SDK.configure { |c| c.add_span_processor(processor) }
|
|
251
|
-
|
|
252
|
-
at_exit { OpenTelemetry.tracer_provider&.shutdown }
|
|
253
|
-
Jekyll::Hooks.register(:site, :post_write, priority: 0) { OpenTelemetry.tracer_provider&.force_flush }
|
|
254
|
-
rescue LoadError
|
|
255
|
-
nil
|
|
256
|
-
# :nocov:
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
# True when some plugin (this one or another) already installed a real
|
|
260
|
-
# SDK-backed tracer_provider, in which case setup_sdk! must not run again.
|
|
261
|
-
def self.sdk_already_configured?
|
|
262
|
-
defined?(OpenTelemetry::SDK::Trace::TracerProvider) &&
|
|
263
|
-
OpenTelemetry.tracer_provider.is_a?(OpenTelemetry::SDK::Trace::TracerProvider)
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# Resolves a dot-separated class name to a constant; returns nil on NameError.
|
|
267
|
-
def self.resolve_class(name)
|
|
268
|
-
name.split('::').reduce(Object) { |m, c| m.const_get(c) }
|
|
269
|
-
rescue NameError
|
|
270
|
-
nil
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# Applies instance and class-method wrappers to klass; skips if klass is nil.
|
|
274
|
-
def self.prepend_wrappers(klass, entries)
|
|
275
|
-
return unless klass
|
|
276
|
-
|
|
277
|
-
class_entries, instance_entries = entries.partition { |e| e[1] == :class }
|
|
278
|
-
klass.prepend(build_wrapper_module(instance_entries)) unless instance_entries.empty?
|
|
279
|
-
klass.singleton_class.prepend(build_wrapper_module(class_entries)) unless class_entries.empty?
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
# Builds an anonymous module that wraps each listed method in a span.
|
|
283
|
-
def self.build_wrapper_module(entries)
|
|
284
|
-
Module.new do
|
|
285
|
-
entries.each do |_class_name, method_type, method_name, span_name, attr_proc|
|
|
286
|
-
define_method(method_name) do |*args, **kwargs, &blk|
|
|
287
|
-
Instrumentation.instrument(span_name) do |span|
|
|
288
|
-
result = super(*args, **kwargs, &blk)
|
|
289
|
-
attr_proc&.call(span, self, args, result)
|
|
290
|
-
result
|
|
291
|
-
rescue StandardError => e
|
|
292
|
-
span.record_exception(e)
|
|
293
|
-
raise
|
|
294
|
-
end
|
|
295
|
-
end
|
|
296
|
-
private method_name if method_type == :private
|
|
297
|
-
end
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
private_class_method :build_wrapper_module, :prepend_wrappers, :setup_sdk!, :sdk_already_configured?
|
|
302
|
-
|
|
303
336
|
# No-op tracer used when opentelemetry-api is not loaded.
|
|
304
337
|
class NoopTracer
|
|
305
338
|
def in_span(_name, **_opts)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../configuration'
|
|
3
4
|
require_relative '../constants'
|
|
4
5
|
require_relative 'regeneration_checker'
|
|
5
6
|
require_relative '../utils/locale_url_formatter'
|
|
7
|
+
require_relative '../utils/page_locales_config'
|
|
6
8
|
require_relative '../utils/site_config_accessor'
|
|
7
9
|
|
|
8
10
|
module Jekyll
|
|
@@ -38,7 +40,7 @@ module Jekyll
|
|
|
38
40
|
def generate(site)
|
|
39
41
|
return unless any_pages_to_localize?(site)
|
|
40
42
|
|
|
41
|
-
Jekyll.logger.info
|
|
43
|
+
Jekyll.logger.info Constants::LOG_PREFIX, 'Generating localized pages...'
|
|
42
44
|
generate_localized_pages(site)
|
|
43
45
|
end
|
|
44
46
|
|
|
@@ -56,8 +58,6 @@ module Jekyll
|
|
|
56
58
|
locales = get_page_locales(page)
|
|
57
59
|
|
|
58
60
|
locales.each do |locale|
|
|
59
|
-
next unless valid_locale_code?(locale)
|
|
60
|
-
|
|
61
61
|
if checker.should_regenerate?(page, locale)
|
|
62
62
|
localized_page = create_localized_page(site, page, locale)
|
|
63
63
|
site.pages << localized_page
|
|
@@ -86,32 +86,26 @@ module Jekyll
|
|
|
86
86
|
|
|
87
87
|
def should_localize_page?(page)
|
|
88
88
|
return false if page.data['localized'] == true
|
|
89
|
-
return false unless page
|
|
89
|
+
return false unless Configuration.localizable?(page)
|
|
90
90
|
|
|
91
91
|
# Localize HTML pages only, never CSS, JavaScript, or other assets.
|
|
92
92
|
is_html = page.output_ext == '.html'
|
|
93
93
|
is_not_asset = !page.path.match?(%r{/(assets|vendor|node_modules|\..*)/}i)
|
|
94
94
|
|
|
95
95
|
if !is_html
|
|
96
|
-
Jekyll.logger.debug
|
|
96
|
+
Jekyll.logger.debug Constants::LOG_PREFIX,
|
|
97
97
|
"Skipping non-HTML: #{page.path} (ext: #{page.output_ext})"
|
|
98
98
|
elsif !is_not_asset
|
|
99
|
-
Jekyll.logger.debug
|
|
99
|
+
Jekyll.logger.debug Constants::LOG_PREFIX, "Skipping asset: #{page.path}"
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
is_html && is_not_asset
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
# Configuration::Schema has already dropped bad locale codes, with a
|
|
106
|
+
# warning, so the page is built for its other locales.
|
|
105
107
|
def get_page_locales(page)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
locales = if data.is_a?(Hash)
|
|
109
|
-
data['locales'] || []
|
|
110
|
-
else
|
|
111
|
-
[]
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
locales.select { |l| valid_locale_code?(l) }
|
|
108
|
+
PageLocalesConfig.new(SiteConfigAccessor.effective_page_data(page)).locales
|
|
115
109
|
end
|
|
116
110
|
|
|
117
111
|
def create_localized_page(site, page, locale)
|
|
@@ -120,14 +114,8 @@ module Jekyll
|
|
|
120
114
|
locale: locale)
|
|
121
115
|
end
|
|
122
116
|
|
|
123
|
-
def valid_locale_code?(locale)
|
|
124
|
-
return false unless locale.is_a?(String)
|
|
125
|
-
|
|
126
|
-
locale.match?(Constants::LOCALE_PATTERN)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
117
|
def any_pages_to_localize?(site)
|
|
130
|
-
site.pages.any? { |page| page
|
|
118
|
+
site.pages.any? { |page| Configuration.localizable?(page) }
|
|
131
119
|
end
|
|
132
120
|
end
|
|
133
121
|
end
|
|
@@ -39,7 +39,13 @@ module Jekyll
|
|
|
39
39
|
# @!attribute [rw] original_url
|
|
40
40
|
# The URL of the source page before locale prefix was added
|
|
41
41
|
# @return [String]
|
|
42
|
-
|
|
42
|
+
# @!attribute [rw] translations_digest
|
|
43
|
+
# Digest of the translations hash the page was rendered with. Set by
|
|
44
|
+
# Translator after a successful translation; read by PostWriteHtmlReprocessor
|
|
45
|
+
# to skip pages whose translations have not changed since the render pass.
|
|
46
|
+
# Instance attribute only — must not leak into Liquid.
|
|
47
|
+
# @return [Integer, nil]
|
|
48
|
+
attr_accessor :locale, :lang, :original_page, :original_url, :translations_digest
|
|
43
49
|
|
|
44
50
|
# Initialize a new localized page variant
|
|
45
51
|
#
|
|
@@ -81,62 +87,34 @@ module Jekyll
|
|
|
81
87
|
setup_localized_data(page, locale)
|
|
82
88
|
end
|
|
83
89
|
|
|
84
|
-
# Get the URL placeholder substitutions for this localized page
|
|
85
|
-
#
|
|
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.
|
|
88
|
-
# This ensures that Jekyll's permalink logic respects the locale prefix.
|
|
89
|
-
#
|
|
90
|
-
# @return [Hash<Symbol, String>] Hash of placeholders with locale-prefixed path
|
|
91
|
-
def url_placeholders
|
|
92
|
-
placeholders = super
|
|
93
|
-
if placeholders[:path]
|
|
94
|
-
url_locale = LocaleUrlFormatter.to_url_segment(@locale)
|
|
95
|
-
placeholders[:path] = "/#{url_locale}#{placeholders[:path]}"
|
|
96
|
-
end
|
|
97
|
-
placeholders
|
|
98
|
-
end
|
|
99
|
-
|
|
100
90
|
# Get the full URL for this localized page
|
|
101
91
|
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
# '/
|
|
105
|
-
#
|
|
92
|
+
# The English page's URL with the locale segment (BCP 47 hyphenated form)
|
|
93
|
+
# in front, and nothing else changed: the page's permalink decides the
|
|
94
|
+
# URL's shape, so '/about', '/about/' and '/about.html' become
|
|
95
|
+
# '/es-es/about', '/es-es/about/' and '/es-es/about.html' for 'es_ES'.
|
|
96
|
+
# Jekyll::Page#destination then writes the page where that URL says, by
|
|
97
|
+
# the same rule as the English page.
|
|
106
98
|
#
|
|
107
|
-
# @return [String] The locale-prefixed URL
|
|
108
|
-
# (e.g., '/es-ES/path/to/page/' or '/es-ES/assets/style.css')
|
|
99
|
+
# @return [String] The locale-prefixed URL
|
|
109
100
|
def url
|
|
110
|
-
|
|
111
|
-
url = "/#{url_locale}#{@original_url}"
|
|
112
|
-
# Only add trailing slash for directory URLs (no file extension)
|
|
113
|
-
url += '/' unless url.end_with?('/') || url =~ /\.[a-zA-Z0-9]+$/
|
|
114
|
-
url
|
|
101
|
+
"/#{LocaleUrlFormatter.to_url_segment(@locale)}#{@original_url}"
|
|
115
102
|
end
|
|
116
103
|
|
|
117
|
-
#
|
|
104
|
+
# Write the page, replacing the empty directory an earlier release left
|
|
105
|
+
# at its path.
|
|
118
106
|
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
# 'dest/es-ES/assets/style.css'.
|
|
107
|
+
# Earlier releases wrote a page whose URL ends in a file name to a
|
|
108
|
+
# directory of that name (es/nav.html/index.html). Jekyll's cleanup
|
|
109
|
+
# removes the old file but keeps the directory, because the new file
|
|
110
|
+
# has the same path.
|
|
124
111
|
#
|
|
125
|
-
# @param dest [String
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
# Only use index.html for HTML pages
|
|
133
|
-
if @output_ext == '.html'
|
|
134
|
-
url_path += '/' unless url_path.end_with?('/')
|
|
135
|
-
"#{dest}#{url_path}index.html"
|
|
136
|
-
else
|
|
137
|
-
# For assets, preserve the file extension
|
|
138
|
-
"#{dest}#{url_path}"
|
|
139
|
-
end
|
|
112
|
+
# @param dest [String] The destination directory
|
|
113
|
+
# @return [void]
|
|
114
|
+
def write(dest)
|
|
115
|
+
path = destination(dest)
|
|
116
|
+
Dir.rmdir(path) if File.directory?(path) && Dir.empty?(path)
|
|
117
|
+
super
|
|
140
118
|
end
|
|
141
119
|
|
|
142
120
|
# Convert this localized page to Liquid template data
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../constants'
|
|
3
4
|
require_relative '../translation/page_translation_loader'
|
|
4
5
|
require_relative '../translation/html_translator'
|
|
5
6
|
require_relative '../utils/page_locales_config'
|
|
@@ -32,20 +33,29 @@ module Jekyll
|
|
|
32
33
|
@dest = SiteConfigAccessor.dest(@site)
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
# Reads each localized file
|
|
36
|
-
# of the English page,
|
|
36
|
+
# Reads each localized file whose translations changed since the render
|
|
37
|
+
# pass, translates it, keeps the external link icons of the English page,
|
|
38
|
+
# and writes it back. Pages that carry a matching +translations_digest+
|
|
39
|
+
# from the Translator are skipped.
|
|
40
|
+
#
|
|
41
|
+
# Logs "Applied new translations to N of M localized pages" only when at
|
|
42
|
+
# least one page was reprocessed.
|
|
37
43
|
#
|
|
38
44
|
# @return [void]
|
|
39
45
|
def reprocess_localized_pages
|
|
40
46
|
localized_files = find_localized_html_files
|
|
41
47
|
return if localized_files.empty?
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
localized_files.each do |html_path, locale, original_url|
|
|
47
|
-
translate_html_file(html_path, locale, original_url)
|
|
49
|
+
reprocessed = 0
|
|
50
|
+
localized_files.each do |html_path, locale, original_url, page|
|
|
51
|
+
reprocessed += 1 if translate_html_file(html_path, locale, original_url, page)
|
|
48
52
|
end
|
|
53
|
+
|
|
54
|
+
return unless reprocessed.positive?
|
|
55
|
+
|
|
56
|
+
msg = "Applied new translations to #{reprocessed} of #{localized_files.length} " \
|
|
57
|
+
'localized pages'
|
|
58
|
+
Jekyll.logger.info Constants::LOG_PREFIX, msg
|
|
49
59
|
end
|
|
50
60
|
|
|
51
61
|
private
|
|
@@ -60,16 +70,13 @@ module Jekyll
|
|
|
60
70
|
original_url = page.data['original_url']
|
|
61
71
|
html_path = page.destination(@dest)
|
|
62
72
|
|
|
63
|
-
results << [html_path, locale, original_url] if File.exist?(html_path)
|
|
73
|
+
results << [html_path, locale, original_url, page] if File.exist?(html_path)
|
|
64
74
|
end
|
|
65
75
|
|
|
66
76
|
results
|
|
67
77
|
end
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
def translate_html_file(html_path, locale, original_url)
|
|
71
|
-
# rubocop:enable Metrics/AbcSize
|
|
72
|
-
original_html = FileOperations.read_utf8(html_path)
|
|
79
|
+
def translate_html_file(html_path, locale, original_url, page = nil)
|
|
73
80
|
original_page = find_original_page(original_url)
|
|
74
81
|
|
|
75
82
|
return unless original_page
|
|
@@ -79,23 +86,33 @@ module Jekyll
|
|
|
79
86
|
|
|
80
87
|
return if translations.nil? || translations.empty?
|
|
81
88
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
config.translatable_attributes,
|
|
85
|
-
debug_logging: config.debug_logging?
|
|
86
|
-
)
|
|
89
|
+
# Skip when the page was already rendered with these same translations
|
|
90
|
+
return if skip_reprocess?(page, translations)
|
|
87
91
|
|
|
88
|
-
|
|
89
|
-
translated_html = translator.translate(original_html, translations, locale, baseurl)
|
|
92
|
+
translator = HtmlTranslator.from_config(config)
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
# Preserve external link icons from the original HTML after translation
|
|
93
|
-
translated_html = ExternalLinkIconPreserver.preserve(original_html, translated_html)
|
|
94
|
-
FileOperations.write_utf8(html_path, translated_html)
|
|
95
|
-
end
|
|
94
|
+
write_translated_html(html_path, translator, translations, locale)
|
|
96
95
|
rescue StandardError => e
|
|
97
96
|
error_msg = "Failed to reprocess #{html_path}: #{e.message}"
|
|
98
|
-
Jekyll.logger.warn
|
|
97
|
+
Jekyll.logger.warn Constants::LOG_PREFIX, error_msg
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def skip_reprocess?(page, translations)
|
|
101
|
+
page.respond_to?(:translations_digest) &&
|
|
102
|
+
page.translations_digest == translations.hash
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def write_translated_html(html_path, translator, translations, locale)
|
|
106
|
+
# Read only now, so pages skipped above cost no file read.
|
|
107
|
+
original_html = FileOperations.read_utf8(html_path)
|
|
108
|
+
baseurl = @site.config['baseurl'] || ''
|
|
109
|
+
translated_html = translator.translate(original_html, translations, locale, baseurl)
|
|
110
|
+
return unless translated_html
|
|
111
|
+
|
|
112
|
+
# Preserve external link icons from the original HTML after translation
|
|
113
|
+
translated_html = ExternalLinkIconPreserver.preserve(original_html, translated_html)
|
|
114
|
+
FileOperations.write_utf8(html_path, translated_html)
|
|
115
|
+
true
|
|
99
116
|
end
|
|
100
117
|
|
|
101
118
|
def find_original_page(url)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../configuration'
|
|
3
4
|
require_relative '../extraction/extractor'
|
|
4
5
|
require_relative 'post_write_html_reprocessor'
|
|
5
6
|
|
|
@@ -77,7 +78,7 @@ module Jekyll
|
|
|
77
78
|
#
|
|
78
79
|
# @return [Boolean] True if at least one page has with_locales set to true
|
|
79
80
|
def any_pages_localized?
|
|
80
|
-
@site.pages.any? { |page| page
|
|
81
|
+
@site.pages.any? { |page| Configuration.localizable?(page) }
|
|
81
82
|
end
|
|
82
83
|
end
|
|
83
84
|
end
|