jekyll-l10n 1.0.5

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +94 -0
  4. data/lib/jekyll-l10n/constants.rb +136 -0
  5. data/lib/jekyll-l10n/errors.rb +60 -0
  6. data/lib/jekyll-l10n/extraction/compendium_merger.rb +142 -0
  7. data/lib/jekyll-l10n/extraction/compendium_translator.rb +138 -0
  8. data/lib/jekyll-l10n/extraction/config_loader.rb +114 -0
  9. data/lib/jekyll-l10n/extraction/dom_attribute_extractor.rb +69 -0
  10. data/lib/jekyll-l10n/extraction/dom_text_extractor.rb +89 -0
  11. data/lib/jekyll-l10n/extraction/extractor.rb +153 -0
  12. data/lib/jekyll-l10n/extraction/html_string_extractor.rb +103 -0
  13. data/lib/jekyll-l10n/extraction/logger.rb +48 -0
  14. data/lib/jekyll-l10n/extraction/result_saver.rb +95 -0
  15. data/lib/jekyll-l10n/jekyll/file_sync.rb +110 -0
  16. data/lib/jekyll-l10n/jekyll/generator.rb +106 -0
  17. data/lib/jekyll-l10n/jekyll/localized_page.rb +150 -0
  18. data/lib/jekyll-l10n/jekyll/localized_page_mapper.rb +51 -0
  19. data/lib/jekyll-l10n/jekyll/page_locator.rb +59 -0
  20. data/lib/jekyll-l10n/jekyll/page_writer.rb +120 -0
  21. data/lib/jekyll-l10n/jekyll/post_write_html_reprocessor.rb +118 -0
  22. data/lib/jekyll-l10n/jekyll/post_write_processor.rb +71 -0
  23. data/lib/jekyll-l10n/jekyll/regeneration_checker.rb +123 -0
  24. data/lib/jekyll-l10n/jekyll/url_filter.rb +199 -0
  25. data/lib/jekyll-l10n/po_file/loader.rb +64 -0
  26. data/lib/jekyll-l10n/po_file/manager.rb +160 -0
  27. data/lib/jekyll-l10n/po_file/merger.rb +80 -0
  28. data/lib/jekyll-l10n/po_file/path_builder.rb +42 -0
  29. data/lib/jekyll-l10n/po_file/reader.rb +518 -0
  30. data/lib/jekyll-l10n/po_file/writer.rb +232 -0
  31. data/lib/jekyll-l10n/translation/block_text_extractor.rb +56 -0
  32. data/lib/jekyll-l10n/translation/html_translator.rb +229 -0
  33. data/lib/jekyll-l10n/translation/libre_translator.rb +226 -0
  34. data/lib/jekyll-l10n/translation/page_translation_loader.rb +99 -0
  35. data/lib/jekyll-l10n/translation/translator.rb +179 -0
  36. data/lib/jekyll-l10n/utils/debug_logger.rb +153 -0
  37. data/lib/jekyll-l10n/utils/error_handler.rb +67 -0
  38. data/lib/jekyll-l10n/utils/external_link_icon_preserver.rb +122 -0
  39. data/lib/jekyll-l10n/utils/file_operations.rb +55 -0
  40. data/lib/jekyll-l10n/utils/html_elements.rb +34 -0
  41. data/lib/jekyll-l10n/utils/html_parser.rb +52 -0
  42. data/lib/jekyll-l10n/utils/html_text_utils.rb +131 -0
  43. data/lib/jekyll-l10n/utils/logger_formatter.rb +114 -0
  44. data/lib/jekyll-l10n/utils/page_locales_config.rb +344 -0
  45. data/lib/jekyll-l10n/utils/po_entry_converter.rb +111 -0
  46. data/lib/jekyll-l10n/utils/site_config_accessor.rb +51 -0
  47. data/lib/jekyll-l10n/utils/text_normalizer.rb +47 -0
  48. data/lib/jekyll-l10n/utils/text_validator.rb +35 -0
  49. data/lib/jekyll-l10n/utils/translation_resolver.rb +115 -0
  50. data/lib/jekyll-l10n/utils/url_path_builder.rb +65 -0
  51. data/lib/jekyll-l10n/utils/url_transformer.rb +141 -0
  52. data/lib/jekyll-l10n/utils/xpath_reference_generator.rb +45 -0
  53. data/lib/jekyll-l10n/version.rb +10 -0
  54. data/lib/jekyll-l10n.rb +268 -0
  55. metadata +200 -0
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../translation/page_translation_loader"
4
+ require_relative "../translation/html_translator"
5
+ require_relative "../utils/page_locales_config"
6
+ require_relative "../utils/file_operations"
7
+ require_relative "../utils/site_config_accessor"
8
+ require_relative "../utils/external_link_icon_preserver"
9
+
10
+ module Jekyll
11
+ module L10n
12
+ # Applies translations to localized HTML pages after Jekyll build.
13
+ #
14
+ # PostWriteHtmlReprocessor finds all localized HTML pages generated by the
15
+ # Generator, loads appropriate translations, applies them using HtmlTranslator,
16
+ # and rewrites the HTML files. It processes pages after Jekyll's standard
17
+ # build completes, during the post-write phase.
18
+ #
19
+ # Key responsibilities:
20
+ # * Find localized HTML pages in the build output
21
+ # * Load page translations (compendium + page-specific)
22
+ # * Create HtmlTranslator with proper configuration
23
+ # * Apply translations to HTML documents
24
+ # * Preserve external link icons from original HTML
25
+ # * Write translated HTML back to files
26
+ # * Handle errors gracefully with logging
27
+ #
28
+ # @example
29
+ # reprocessor = PostWriteHtmlReprocessor.new(site)
30
+ # reprocessor.reprocess_localized_pages
31
+ # # Reads localized HTML, translates, preserves icons, writes back
32
+ class PostWriteHtmlReprocessor
33
+ attr_reader :site
34
+
35
+ # Initialize a new PostWriteHtmlReprocessor.
36
+ #
37
+ # @param site [Jekyll::Site] Jekyll site object
38
+ def initialize(site)
39
+ @site = site
40
+ @dest = SiteConfigAccessor.dest(@site)
41
+ end
42
+
43
+ # Reprocess all localized pages with translations.
44
+ #
45
+ # Finds all localized HTML pages that were written during the Jekyll build,
46
+ # loads their translations, applies translations to text and attributes,
47
+ # preserves external link icon styling from the original HTML, and writes
48
+ # the translated HTML back to disk.
49
+ #
50
+ # @return [void]
51
+ def reprocess_localized_pages
52
+ localized_files = find_localized_html_files
53
+ return if localized_files.empty?
54
+
55
+ msg = "Applying translations to #{localized_files.length} localized pages..."
56
+ Jekyll.logger.info "Localization", msg
57
+
58
+ localized_files.each do |html_path, locale, original_url|
59
+ translate_html_file(html_path, locale, original_url)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def find_localized_html_files
66
+ results = []
67
+
68
+ @site.pages.each do |page|
69
+ next unless page.data["localized"] == true
70
+
71
+ locale = page.data["locale"]
72
+ original_url = page.data["original_url"]
73
+ html_path = page.destination(@dest)
74
+
75
+ results << [html_path, locale, original_url] if File.exist?(html_path)
76
+ end
77
+
78
+ results
79
+ end
80
+
81
+ # rubocop:disable Metrics/AbcSize
82
+ def translate_html_file(html_path, locale, original_url)
83
+ # rubocop:enable Metrics/AbcSize
84
+ original_html = FileOperations.read_utf8(html_path)
85
+ original_page = find_original_page(original_url)
86
+
87
+ return unless original_page
88
+
89
+ config = PageLocalesConfig.new(original_page.data)
90
+ translations = PageTranslationLoader.load(@site, locale, original_url, config)
91
+
92
+ return if translations.nil? || translations.empty?
93
+
94
+ translator = HtmlTranslator.new(
95
+ config.fallback_mode,
96
+ config.translatable_attributes,
97
+ :debug_logging => config.debug_logging?
98
+ )
99
+
100
+ baseurl = @site.config["baseurl"] || ""
101
+ translated_html = translator.translate(original_html, translations, locale, baseurl)
102
+
103
+ if translated_html
104
+ # Preserve external link icons from the original HTML after translation
105
+ translated_html = ExternalLinkIconPreserver.preserve(original_html, translated_html)
106
+ FileOperations.write_utf8(html_path, translated_html)
107
+ end
108
+ rescue StandardError => e
109
+ error_msg = "Failed to reprocess #{html_path}: #{e.message}"
110
+ Jekyll.logger.warn "Localization", error_msg
111
+ end
112
+
113
+ def find_original_page(url)
114
+ @site.pages.find { |p| p.url == url && p.data["localized"] != true }
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../extraction/extractor"
4
+ require_relative "post_write_html_reprocessor"
5
+
6
+ module Jekyll
7
+ module L10n
8
+ # Post-Write Processor - Extracts strings and applies translations after Jekyll build
9
+ #
10
+ # This class handles the final localization phase of the Jekyll build process. It runs
11
+ # after Jekyll has written all HTML files to disk (via the site:post_write hook).
12
+ #
13
+ # The processor coordinates two main operations:
14
+ # 1. String Extraction: Finds new and modified translatable strings in generated HTML
15
+ # 2. HTML Reprocessing: Applies translations to localized page variants
16
+ #
17
+ # Key responsibilities:
18
+ # - Invoke the Extractor to scan generated HTML for translatable content
19
+ # - Update PO files with new/changed strings
20
+ # - Reprocess localized HTML if PO files were modified
21
+ # - Provide logging of extraction results
22
+ #
23
+ # This is invoked automatically by Jekyll during the post_write phase and should not be
24
+ # called directly in most use cases.
25
+ #
26
+ # This runs in phase 5 of the Jekyll build pipeline (Post-Write Phase). See the "Build
27
+ # Pipeline" section in lib/jekyll-l10n.rb for the complete workflow.
28
+ #
29
+ # @example Manual invocation (advanced)
30
+ # processor = PostWriteProcessor.new(site)
31
+ # processor.process_localizations
32
+ #
33
+ # @see Jekyll::L10n for Build Pipeline documentation
34
+ # @see Jekyll::L10n::Extractor for details on string extraction
35
+ # @see Jekyll::L10n::PostWriteHtmlReprocessor for details on translation application
36
+ #
37
+ class PostWriteProcessor
38
+ # @!attribute [r] site
39
+ # The Jekyll site object with all generated pages
40
+ # @return [Jekyll::Site]
41
+ attr_reader :site
42
+
43
+ # Initialize the post-write processor
44
+ #
45
+ # @param site [Jekyll::Site] The Jekyll site object
46
+ def initialize(site)
47
+ @site = site
48
+ end
49
+
50
+ # Process all localizations for the site
51
+ #
52
+ # Runs the extraction and reprocessing pipeline:
53
+ # 1. Extracts translatable strings from all HTML files
54
+ # 2. Updates PO files with new entries and modified translations
55
+ # 3. If PO files were created or updated, reprocess localized pages to apply translations
56
+ #
57
+ # @return [void]
58
+ # @note This is automatically called by Jekyll's site:post_write hook
59
+ def process_localizations
60
+ extractor = Extractor.new(@site)
61
+ extraction_result = extractor.extract_site
62
+
63
+ # Reprocess localized HTML if PO files were created/updated
64
+ if extraction_result && extraction_result[:po_files_created].positive?
65
+ reprocessor = PostWriteHtmlReprocessor.new(@site)
66
+ reprocessor.reprocess_localized_pages
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../utils/site_config_accessor"
4
+ require_relative "../po_file/path_builder"
5
+
6
+ module Jekyll
7
+ module L10n
8
+ # Regeneration Checker - Optimizes incremental builds by skipping unchanged pages
9
+ #
10
+ # This class implements incremental build optimization for localized pages. When Jekyll's
11
+ # incremental build mode is enabled, it checks whether a specific locale variant of a page
12
+ # needs to be regenerated based on source modification times.
13
+ #
14
+ # The checker compares modification times of:
15
+ # - Source page file
16
+ # - Page-specific PO translation file
17
+ # - Compendium (shared translations) PO file
18
+ # - Jekyll configuration (_config.yml)
19
+ #
20
+ # If any of these are newer than the output HTML, the page is regenerated.
21
+ # This significantly speeds up rebuilds when only a few files have changed.
22
+ #
23
+ # @example Usage
24
+ # checker = RegenerationChecker.new(site)
25
+ # if checker.should_regenerate?(page, 'es')
26
+ # # Create localized_page and add to site.pages
27
+ # end
28
+ #
29
+ class RegenerationChecker
30
+ # @!visibility private
31
+ DEFAULT_LOCALES_DIR = "_locales"
32
+
33
+ # Initialize the regeneration checker
34
+ #
35
+ # @param site [Jekyll::Site] The Jekyll site object
36
+ # @param locales_dir [String] Directory containing PO files (defaults to
37
+ # DEFAULT_LOCALES_DIR = "_locales")
38
+ def initialize(site, locales_dir = DEFAULT_LOCALES_DIR)
39
+ @site = site
40
+ @locales_dir = locales_dir
41
+ @source = SiteConfigAccessor.source(@site)
42
+ @destination = @site&.config&.dig("destination") || ""
43
+ end
44
+
45
+ # Determine if a localized page variant should be regenerated
46
+ #
47
+ # Returns true if:
48
+ # - Incremental builds are disabled (always regenerate)
49
+ # - Output file doesn't exist yet
50
+ # - Source page has been modified since output was generated
51
+ # - PO translation files have been modified since output was generated
52
+ # - Jekyll configuration has been modified since output was generated
53
+ #
54
+ # Returns false if incremental builds are enabled and output is up-to-date.
55
+ #
56
+ # @param page [Jekyll::Page] The source page to check
57
+ # @param locale [String] The locale code for the variant (e.g., 'es', 'fr')
58
+ # @return [Boolean] true if the page should be regenerated, false if output is current
59
+ def should_regenerate?(page, locale)
60
+ return true unless incremental_enabled?
61
+ return true unless dest_path_exists?(page, locale)
62
+
63
+ dest_mtime = File.mtime(dest_path(page, locale))
64
+
65
+ source_modified?(page, dest_mtime) ||
66
+ po_files_modified?(page, locale, dest_mtime) ||
67
+ config_modified?(dest_mtime)
68
+ end
69
+
70
+ private
71
+
72
+ def incremental_enabled?
73
+ return false if @site&.config.nil?
74
+
75
+ localization_config = @site.config.dig("localization_gettext", "with_locales_data",
76
+ "incremental") ||
77
+ @site.config.dig("localization_gettext", "incremental") ||
78
+ false
79
+ localization_config == true
80
+ end
81
+
82
+ def dest_path_exists?(page, locale)
83
+ File.exist?(dest_path(page, locale))
84
+ end
85
+
86
+ def dest_path(page, locale)
87
+ # Replicate LocalizedPage destination logic without instantiation
88
+ url = "/#{locale}#{page.url.chomp("/")}/"
89
+ "#{@destination}#{url}index.html"
90
+ end
91
+
92
+ def source_modified?(page, dest_mtime)
93
+ page_mtime = File.mtime(page.path)
94
+ page_mtime > dest_mtime
95
+ rescue StandardError
96
+ true
97
+ end
98
+
99
+ def po_files_modified?(page, locale, dest_mtime)
100
+ # Check page-specific PO file
101
+ page_po_path = PoPathBuilder.build(@source, @locales_dir, locale, page.path)
102
+ return true if file_modified?(page_po_path, dest_mtime)
103
+
104
+ # Check compendium PO file
105
+ compendium_po_path = PoPathBuilder.build(@source, @locales_dir, locale, nil)
106
+ file_modified?(compendium_po_path, dest_mtime)
107
+ end
108
+
109
+ def config_modified?(dest_mtime)
110
+ config_path = File.join(@source, "_config.yml")
111
+ file_modified?(config_path, dest_mtime)
112
+ end
113
+
114
+ def file_modified?(file_path, dest_mtime)
115
+ return false unless File.exist?(file_path)
116
+
117
+ File.mtime(file_path) > dest_mtime
118
+ rescue StandardError
119
+ false
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "liquid"
4
+
5
+ module Jekyll
6
+ module L10n
7
+ # URL Filter - Liquid filters for locale-aware URL generation
8
+ #
9
+ # Provides Liquid filters for use in Jekyll templates to generate locale-prefixed URLs.
10
+ # These filters are automatically registered with Liquid during plugin initialization.
11
+ #
12
+ # This module is designed to be included in Liquid::Template's filter registry, making
13
+ # the filters available in all Jekyll templates.
14
+ #
15
+ # Key responsibilities:
16
+ # - Generate locale-prefixed URLs for links
17
+ # - Switch between locale variants of the same page
18
+ # - Validate locales against site configuration
19
+ # - Handle edge cases (external URLs, anchors, protocols)
20
+ #
21
+ # @example Usage in Jekyll Liquid templates
22
+ # <!-- Add locale prefix to URL -->
23
+ # <a href="{{ '/about/' | locale_url: 'es' }}">Acerca de</a>
24
+ #
25
+ # <!-- Switch to different locale variant -->
26
+ # <a href="{{ page.url | switch_locale_url: 'fr' }}">Français</a>
27
+ #
28
+ # @see Jekyll::L10n for filter registration (line 218 in lib/jekyll-l10n.rb)
29
+ #
30
+ module UrlFilter
31
+ # Generate a locale-prefixed URL for the given URL and locale
32
+ #
33
+ # Adds a locale prefix to the given URL, making it suitable for use in locale-specific
34
+ # links. For example, '/about/' becomes '/es/about/' for locale 'es'. The filter handles
35
+ # various edge cases and only applies the prefix when appropriate.
36
+ #
37
+ # The method performs several checks to determine if prefixing is appropriate:
38
+ # - Invalid locale codes (nil, empty, or 'en') are skipped
39
+ # - URLs already localized are not prefixed again
40
+ # - External URLs, anchors, and relative paths are not modified
41
+ #
42
+ # @param url [String] The URL to prefix (e.g., '/about/', '/path/to/page/')
43
+ # @param locale [String, nil] The locale code (e.g., 'es', 'pt_BR'). Defaults to nil
44
+ # (uses current page's locale). If nil, uses page's locale from context.
45
+ # @return [String] The locale-prefixed URL, or original URL if prefixing is not appropriate
46
+ # @example
47
+ # <!-- Explicit locale -->
48
+ # {{ '/about/' | locale_url: 'es' }} # => '/es/about/'
49
+ #
50
+ # <!-- Current page's locale -->
51
+ # {{ '/about/' | locale_url }} # => '/es/about/' (if page locale is 'es')
52
+ def locale_url(url, locale = nil)
53
+ locale ||= current_locale
54
+ return url if should_skip?(url, locale)
55
+
56
+ "/#{locale}#{url}"
57
+ end
58
+
59
+ # Generate a URL for the given locale variant of the current page
60
+ #
61
+ # Switches the current URL to a different locale variant. For example, if the current
62
+ # page is '/es/about/', calling this with 'fr' returns '/fr/about/'. This is useful
63
+ # for building language switcher menus.
64
+ #
65
+ # This filter first extracts the base URL (without locale prefix) from the given URL,
66
+ # then applies the target locale using the standard locale_url logic.
67
+ #
68
+ # @param url [String] The current URL (may or may not have a locale prefix)
69
+ # @param target_locale [String] The target locale code (e.g., 'es', 'fr', 'pt_BR')
70
+ # @return [String] The URL for the target locale, or original URL if locale is invalid
71
+ # @example
72
+ # <!-- Current page is /es/about/, switch to French -->
73
+ # {{ page.url | switch_locale_url: 'fr' }} # => '/fr/about/'
74
+ #
75
+ # <!-- Build a language switcher -->
76
+ # <a href="{{ page.url | switch_locale_url: 'es' }}">Español</a>
77
+ # <a href="{{ page.url | switch_locale_url: 'fr' }}">Français</a>
78
+ # <a href="{{ page.url | switch_locale_url: 'pt' }}">Português</a>
79
+ def switch_locale_url(url, target_locale)
80
+ # Validate target locale is configured
81
+ return url unless valid_target_locale?(target_locale)
82
+
83
+ # Get base URL without locale prefix
84
+ base_url_value = base_url(url)
85
+
86
+ # Apply target locale using existing locale_url logic
87
+ locale_url(base_url_value, target_locale)
88
+ end
89
+
90
+ private
91
+
92
+ def current_locale
93
+ page = @context.registers[:page]
94
+ return nil unless page
95
+
96
+ if page.is_a?(Hash)
97
+ page.dig("data", "locale")
98
+ else
99
+ page&.data&.[]("locale")
100
+ end
101
+ rescue StandardError => e
102
+ Jekyll.logger.warn "Localization", "Error retrieving current locale: #{e.message}"
103
+ nil
104
+ end
105
+
106
+ def should_skip?(url, locale)
107
+ url_str = url.to_s
108
+
109
+ skip_checks = [
110
+ invalid_locale?(locale),
111
+ already_localized?(url_str),
112
+ external_url?(url_str),
113
+ protocol_url?(url_str),
114
+ anchor_only?(url_str),
115
+ relative_path?(url_str),
116
+ !root_relative_path?(url_str),
117
+ ]
118
+
119
+ skip_checks.any?
120
+ end
121
+
122
+ def invalid_locale?(locale)
123
+ locale.nil? || locale.empty? || locale == "en"
124
+ end
125
+
126
+ def already_localized?(url_str)
127
+ %r!^/[a-z]{2}(?:[_-][A-Z]{2})?(?=/|\?)!.match?(url_str)
128
+ end
129
+
130
+ def external_url?(url_str)
131
+ %r!^https?://!.match?(url_str)
132
+ end
133
+
134
+ def protocol_url?(url_str)
135
+ %r!^[a-z]+:!.match?(url_str)
136
+ end
137
+
138
+ def anchor_only?(url_str)
139
+ url_str.start_with?("#")
140
+ end
141
+
142
+ def relative_path?(url_str)
143
+ url_str.start_with?("..")
144
+ end
145
+
146
+ def root_relative_path?(url_str)
147
+ url_str.start_with?("/")
148
+ end
149
+
150
+ def valid_target_locale?(locale)
151
+ return false if locale.nil? || locale.empty?
152
+ return true if locale == "en" # English is always valid
153
+
154
+ # Get configured locales from page or site config
155
+ locales_config = configured_locales
156
+ locales_config.include?(locale)
157
+ end
158
+
159
+ def configured_locales
160
+ page = @context.registers[:page]
161
+
162
+ # Try to get from page data first (most specific)
163
+ if page
164
+ locales = if page.is_a?(Hash)
165
+ page.dig("with_locales_data", "locales")
166
+ else
167
+ page.data&.dig("with_locales_data", "locales")
168
+ end
169
+ return locales if locales && !locales.empty?
170
+ end
171
+
172
+ # Fallback to empty array if not configured
173
+ []
174
+ end
175
+
176
+ def base_url(url)
177
+ page = @context.registers[:page]
178
+
179
+ # Try to get original_url from page data (preferred method)
180
+ if page
181
+ original = if page.is_a?(Hash)
182
+ page.dig("data", "original_url") || page["original_url"]
183
+ else
184
+ page.data&.[]("original_url")
185
+ end
186
+ return original if original
187
+ end
188
+
189
+ # Fallback: strip locale prefix from current URL
190
+ strip_locale_from_url(url)
191
+ end
192
+
193
+ def strip_locale_from_url(url)
194
+ # Strip leading locale prefix like /es/, /fr/, /pt_BR/, /zh-CN/
195
+ url.sub(%r!^/([a-z]{2}(?:[_-][A-Z]{2})?)(?=/|$)!, "")
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "reader"
4
+ require_relative "manager"
5
+ require_relative "path_builder"
6
+
7
+ module Jekyll
8
+ module L10n
9
+ # Loads PO files from disk with automatic caching.
10
+ #
11
+ # PoFileLoader handles loading PO files and managing the global in-memory cache
12
+ # to avoid redundant disk I/O. It constructs proper file paths for locale and
13
+ # page combinations, checks the cache first, and reads from disk only when needed.
14
+ #
15
+ # Key responsibilities:
16
+ # * Check global cache for already-loaded files
17
+ # * Construct file paths for locale/page combinations
18
+ # * Load PO files from disk when not cached
19
+ # * Parse PO file content into translation hashes
20
+ # * Cache parsed translations for reuse
21
+ # * Handle file-not-found gracefully (return empty hash)
22
+ # * Log errors during loading
23
+ #
24
+ # @example
25
+ # translations = PoFileLoader.load('_site', '_locales', 'es', 'docs/index.html')
26
+ # # Returns cached or newly-loaded translations for that page/locale
27
+ #
28
+ # @see Jekyll::L10n::PoFileManager for caching implementation details
29
+ class PoFileLoader
30
+ # Load a PO file with caching.
31
+ #
32
+ # First checks the global PoFileManager cache for already-loaded translations.
33
+ # If not found, constructs the file path, loads from disk if file exists,
34
+ # parses the PO file, caches the result, and returns. Returns empty hash
35
+ # if file doesn't exist or if an error occurs during loading.
36
+ #
37
+ # @param source [String] Site source directory
38
+ # @param locales_dir [String] Locales directory name (e.g., '_locales')
39
+ # @param locale [String] Target locale code (e.g., 'es', 'fr')
40
+ # @param page_path [String, nil] Optional page path for page-specific file.
41
+ # If nil, loads compendium.
42
+ # @return [Hash] Translation hash { msgid => msgstr } or empty hash if not found
43
+ def self.load(source, locales_dir, locale, page_path)
44
+ cache_key = "#{locale}:#{page_path}"
45
+ cached = PoFileManager.cache[cache_key]
46
+ return cached if cached
47
+
48
+ po_path = PoPathBuilder.build(source, locales_dir, locale, page_path)
49
+ return {} unless File.exist?(po_path)
50
+
51
+ load_and_cache(cache_key, po_path)
52
+ end
53
+
54
+ def self.load_and_cache(cache_key, po_path)
55
+ translations = PoFileReader.parse(po_path)
56
+ PoFileManager.cache[cache_key] = translations
57
+ translations
58
+ rescue StandardError => e
59
+ Jekyll.logger.warn "Localization", "Error loading PO file #{po_path}: #{e.message}"
60
+ {}
61
+ end
62
+ end
63
+ end
64
+ end