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,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "loader"
4
+ require_relative "writer"
5
+ require_relative "merger"
6
+ require_relative "path_builder"
7
+ require_relative "../utils/site_config_accessor"
8
+ require_relative "../utils/file_operations"
9
+ require_relative "../utils/logger_formatter"
10
+
11
+ module Jekyll
12
+ module L10n
13
+ # Manages reading, writing, merging, and caching of GNU Gettext PO files.
14
+ #
15
+ # PoFileManager provides a centralized API for PO file operations and maintains
16
+ # an in-memory cache to avoid redundant disk I/O across multiple extraction and
17
+ # translation operations. It coordinates between loading, writing, and merging
18
+ # operations while managing file paths and locale directories.
19
+ #
20
+ # Key responsibilities:
21
+ # * Load and cache PO files (page-specific and compendium)
22
+ # * Save new or updated PO files with optional merging of existing translations
23
+ # * Merge PO files for locales (combining page-specific with compendium)
24
+ # * Manage global in-memory cache keyed by locale and page path
25
+ # * Clear cache when files are written or rebuilt
26
+ # * Construct proper file paths for locale/page combinations
27
+ #
28
+ # @example
29
+ # manager = PoFileManager.new(site, '_locales')
30
+ # translations = manager.load_po_file('es', 'docs/index.html')
31
+ # manager.save_po_file('es', entries, page_path: 'docs/index.html')
32
+ class PoFileManager
33
+ DEFAULT_LOCALES_DIR = "_locales"
34
+ attr_reader :site, :locales_dir
35
+
36
+ class << self
37
+ # Access the global PO file cache.
38
+ #
39
+ # @return [Hash] Cache mapping "locale:page_path" keys to translation hashes
40
+ def cache
41
+ @cache ||= {}
42
+ end
43
+
44
+ # Clear the global PO file cache.
45
+ #
46
+ # Clears all cached PO file translations. Should be called when the site
47
+ # is rebuilt or PO files are modified to ensure fresh data is loaded.
48
+ #
49
+ # @return [void]
50
+ def clear_cache
51
+ @cache = {}
52
+ end
53
+
54
+ # Get the current size of the global cache.
55
+ #
56
+ # @return [Integer] Number of entries currently cached
57
+ def cache_size
58
+ cache.size
59
+ end
60
+ end
61
+
62
+ # Initialize a new PoFileManager.
63
+ #
64
+ # @param site [Jekyll::Site] Jekyll site object
65
+ # @param locales_dir [String] Directory containing PO files (defaults to
66
+ # DEFAULT_LOCALES_DIR = "_locales")
67
+ def initialize(site, locales_dir = DEFAULT_LOCALES_DIR)
68
+ @site = site
69
+ @source = SiteConfigAccessor.source(@site)
70
+ @locales_dir = locales_dir
71
+ end
72
+
73
+ # Load the compendium (shared translations) for a locale.
74
+ #
75
+ # Compendiums are locale-level translation files shared across all pages.
76
+ # This delegates to load_po_file with page_path set to nil.
77
+ #
78
+ # @param locale [String] Target locale code (e.g., 'es', 'fr')
79
+ # @return [Hash] Translation hash or empty hash if file doesn't exist
80
+ def load_compendium(locale)
81
+ load_po_file(locale, nil)
82
+ end
83
+
84
+ # Load a PO file for a specific locale and optional page.
85
+ #
86
+ # First checks in-memory cache to avoid redundant disk I/O. If not cached,
87
+ # loads from disk using PoFileLoader and caches the result. Returns empty
88
+ # hash if file doesn't exist.
89
+ #
90
+ # @param locale [String] Target locale code (e.g., 'es', 'fr')
91
+ # @param page_path [String, nil] Optional page path for page-specific translations.
92
+ # Defaults to nil, which loads the compendium (shared translations) file instead.
93
+ # @return [Hash<String, String>] Translation hash mapping msgid to msgstr,
94
+ # or empty hash if file doesn't exist. Simple format where keys are original
95
+ # strings and values are translations.
96
+ def load_po_file(locale, page_path = nil)
97
+ PoFileLoader.load(@source, @locales_dir, locale, page_path)
98
+ end
99
+
100
+ # Save a PO file for a locale and optional page.
101
+ #
102
+ # Writes entries to PO file, optionally merging with existing translations to
103
+ # preserve any manual edits. Creates directory structure as needed. Invalidates
104
+ # relevant cache entries after write.
105
+ #
106
+ # @param locale [String] Target locale code (e.g., 'es', 'fr')
107
+ # @param entries [Array<Hash>] Array of extraction entries with :msgid, :msgstr, :reference
108
+ # @param page_path [String, nil] Optional page path for page-specific file
109
+ # @param skip_merge [Boolean] If true, overwrites file without merging existing translations
110
+ # @return [Boolean] True if write successful, false on error
111
+ def save_po_file(locale, entries, page_path: nil, skip_merge: false)
112
+ po_path = PoPathBuilder.build(@source, @locales_dir, locale, page_path)
113
+ prepare_and_write_po_file(po_path, entries, locale, :page_path => page_path,
114
+ :skip_merge => skip_merge)
115
+ rescue StandardError => e
116
+ Jekyll.logger.error "Localization", "Error saving PO file #{po_path}: #{e.message}"
117
+ false
118
+ end
119
+
120
+ # Save the compendium (shared translations) for a locale.
121
+ #
122
+ # Writes entries to the locale's compendium file, overwriting without merging
123
+ # (compendia are typically generated fresh each time).
124
+ #
125
+ # @param locale [String] Target locale code
126
+ # @param entries [Array<Hash>] Array of extraction entries
127
+ # @return [Boolean] True if write successful, false on error
128
+ def save_compendium(locale, entries)
129
+ save_po_file(locale, entries, :page_path => nil, :skip_merge => true)
130
+ end
131
+
132
+ # Merge PO files for a locale.
133
+ #
134
+ # Combines page-specific and compendium translations for a locale, handling
135
+ # merging logic to preserve translations while adding new strings.
136
+ #
137
+ # @param locale [String] Target locale code
138
+ # @return [Boolean] True if merge successful
139
+ def merge_po_files(locale)
140
+ PoFileMerger.merge_for_locale(@source, @locales_dir, locale)
141
+ end
142
+
143
+ private
144
+
145
+ def prepare_and_write_po_file(po_path, entries, locale, page_path: nil, skip_merge: false) # rubocop:disable Metrics/ParameterLists
146
+ LoggerFormatter.debug_if_enabled("PoFileManager",
147
+ "Writing PO file: #{po_path} (#{entries.length} entries)")
148
+ FileOperations.ensure_directory(po_path)
149
+ PoFileWriter.write(po_path, entries, locale, :skip_merge => skip_merge)
150
+ invalidate_po_cache(locale, page_path)
151
+ true
152
+ end
153
+
154
+ def invalidate_po_cache(locale, page_path)
155
+ cache_key = "#{locale}:#{page_path}"
156
+ self.class.cache.delete(cache_key)
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "reader"
4
+
5
+ module Jekyll
6
+ module L10n
7
+ # Merges multiple PO files for a locale into a single translation hash.
8
+ #
9
+ # PoFileMerger combines page-specific PO files in a locale directory into
10
+ # a single merged translation hash. Used when creating compendia by combining
11
+ # all extracted strings across pages for a locale.
12
+ #
13
+ # Key responsibilities:
14
+ # * Find all PO files for a locale
15
+ # * Parse PO files with reference metadata
16
+ # * Merge into single translation hash
17
+ # * Preserve references for debugging
18
+ # * Handle parsing errors gracefully
19
+ #
20
+ # @see Jekyll::L10n::PoFileReader for file parsing
21
+ # @see Jekyll::L10n::CompendiumMerger for compendium merge workflow
22
+ #
23
+ # @example
24
+ # merged = PoFileMerger.merge_for_locale('_site', '_locales', 'es')
25
+ # # Returns combined translations from all _locales/es/**/*.po files
26
+ class PoFileMerger
27
+ # Merge all PO files for a locale.
28
+ #
29
+ # Finds all PO files in the locale subdirectory, parses them with
30
+ # references, and merges into a single hash. First occurrence of
31
+ # each msgid is kept.
32
+ #
33
+ # @param source [String] Site source directory
34
+ # @param locales_dir [String] Locales directory name
35
+ # @param locale [String] Locale code (e.g., 'es', 'fr')
36
+ # @return [Hash<String, Hash>] Merged translation hash where keys are msgid strings
37
+ # and values are hashes with :msgstr and :reference keys
38
+ def self.merge_for_locale(source, locales_dir, locale)
39
+ locale_dir = File.join(source, locales_dir, locale)
40
+ return {} unless File.directory?(locale_dir)
41
+
42
+ po_files = Dir.glob(File.join(locale_dir, "**", "*.po")).sort
43
+ merge_files(po_files)
44
+ end
45
+
46
+ # Merge a list of PO files.
47
+ #
48
+ # Parses each file and merges into single hash. Later files don't override
49
+ # entries from earlier files (first occurrence wins).
50
+ #
51
+ # @param po_files [Array<String>] Paths to PO files to merge
52
+ # @return [Hash<String, Hash>] Merged translation hash where keys are msgid strings
53
+ # and values are hashes with :msgstr and :reference keys
54
+ def self.merge_files(po_files)
55
+ merged = {}
56
+ po_files.each do |po_file|
57
+ merge_file_into(po_file, merged)
58
+ end
59
+ merged
60
+ end
61
+
62
+ # Merge a single PO file into an existing merged hash.
63
+ #
64
+ # Parses the PO file with references and adds entries that aren't
65
+ # already in the merged hash. The hash can be empty, and it is modified in place.
66
+ #
67
+ # @param po_file [String] Path to PO file
68
+ # @param merged [Hash] Existing merged hash (can be empty, modified in place)
69
+ # @return [void]
70
+ def self.merge_file_into(po_file, merged)
71
+ translations = PoFileReader.parse_with_references(po_file)
72
+ translations.each do |msgid, entry|
73
+ merged[msgid] ||= entry
74
+ end
75
+ rescue StandardError => e
76
+ Jekyll.logger.warn "Localization", "Error merging PO file #{po_file}: #{e.message}"
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module L10n
5
+ # Constructs file paths for PO files.
6
+ #
7
+ # PoPathBuilder creates proper file paths for both compendium (locale-level)
8
+ # and page-specific PO files. Compendium files are at the locale level
9
+ # ({locales_dir}/{locale}.po), while page-specific files are nested
10
+ # ({locales_dir}/{locale}/{page_path}.po).
11
+ #
12
+ # Key responsibilities:
13
+ # * Build paths for compendium PO files
14
+ # * Build paths for page-specific PO files
15
+ # * Handle file path normalization
16
+ #
17
+ # @example
18
+ # compendium = PoPathBuilder.build('_site', '_locales', 'es', nil)
19
+ # # Returns '_site/_locales/es.po'
20
+ # page_specific = PoPathBuilder.build('_site', '_locales', 'es', 'docs/index.html')
21
+ # # Returns '_site/_locales/es/docs/index.html.po'
22
+ class PoPathBuilder
23
+ # Build a PO file path.
24
+ #
25
+ # For compendium (page_path nil): {source}/{locales_dir}/{locale}.po
26
+ # For page-specific: {source}/{locales_dir}/{locale}/{page_path}.po
27
+ #
28
+ # @param source [String] Site source directory
29
+ # @param locales_dir [String] Locales directory name (e.g., '_locales')
30
+ # @param locale [String] Locale code (e.g., 'es', 'fr')
31
+ # @param page_path [String, nil] Page path for page-specific file, nil for compendium
32
+ # @return [String] Full path to PO file
33
+ def self.build(source, locales_dir, locale, page_path)
34
+ if page_path.nil?
35
+ File.join(source, locales_dir, "#{locale}.po")
36
+ else
37
+ File.join(source, locales_dir, locale, "#{page_path}.po")
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end