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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +94 -0
- data/lib/jekyll-l10n/constants.rb +136 -0
- data/lib/jekyll-l10n/errors.rb +60 -0
- data/lib/jekyll-l10n/extraction/compendium_merger.rb +142 -0
- data/lib/jekyll-l10n/extraction/compendium_translator.rb +138 -0
- data/lib/jekyll-l10n/extraction/config_loader.rb +114 -0
- data/lib/jekyll-l10n/extraction/dom_attribute_extractor.rb +69 -0
- data/lib/jekyll-l10n/extraction/dom_text_extractor.rb +89 -0
- data/lib/jekyll-l10n/extraction/extractor.rb +153 -0
- data/lib/jekyll-l10n/extraction/html_string_extractor.rb +103 -0
- data/lib/jekyll-l10n/extraction/logger.rb +48 -0
- data/lib/jekyll-l10n/extraction/result_saver.rb +95 -0
- data/lib/jekyll-l10n/jekyll/file_sync.rb +110 -0
- data/lib/jekyll-l10n/jekyll/generator.rb +106 -0
- data/lib/jekyll-l10n/jekyll/localized_page.rb +150 -0
- data/lib/jekyll-l10n/jekyll/localized_page_mapper.rb +51 -0
- data/lib/jekyll-l10n/jekyll/page_locator.rb +59 -0
- data/lib/jekyll-l10n/jekyll/page_writer.rb +120 -0
- data/lib/jekyll-l10n/jekyll/post_write_html_reprocessor.rb +118 -0
- data/lib/jekyll-l10n/jekyll/post_write_processor.rb +71 -0
- data/lib/jekyll-l10n/jekyll/regeneration_checker.rb +123 -0
- data/lib/jekyll-l10n/jekyll/url_filter.rb +199 -0
- data/lib/jekyll-l10n/po_file/loader.rb +64 -0
- data/lib/jekyll-l10n/po_file/manager.rb +160 -0
- data/lib/jekyll-l10n/po_file/merger.rb +80 -0
- data/lib/jekyll-l10n/po_file/path_builder.rb +42 -0
- data/lib/jekyll-l10n/po_file/reader.rb +518 -0
- data/lib/jekyll-l10n/po_file/writer.rb +232 -0
- data/lib/jekyll-l10n/translation/block_text_extractor.rb +56 -0
- data/lib/jekyll-l10n/translation/html_translator.rb +229 -0
- data/lib/jekyll-l10n/translation/libre_translator.rb +226 -0
- data/lib/jekyll-l10n/translation/page_translation_loader.rb +99 -0
- data/lib/jekyll-l10n/translation/translator.rb +179 -0
- data/lib/jekyll-l10n/utils/debug_logger.rb +153 -0
- data/lib/jekyll-l10n/utils/error_handler.rb +67 -0
- data/lib/jekyll-l10n/utils/external_link_icon_preserver.rb +122 -0
- data/lib/jekyll-l10n/utils/file_operations.rb +55 -0
- data/lib/jekyll-l10n/utils/html_elements.rb +34 -0
- data/lib/jekyll-l10n/utils/html_parser.rb +52 -0
- data/lib/jekyll-l10n/utils/html_text_utils.rb +131 -0
- data/lib/jekyll-l10n/utils/logger_formatter.rb +114 -0
- data/lib/jekyll-l10n/utils/page_locales_config.rb +344 -0
- data/lib/jekyll-l10n/utils/po_entry_converter.rb +111 -0
- data/lib/jekyll-l10n/utils/site_config_accessor.rb +51 -0
- data/lib/jekyll-l10n/utils/text_normalizer.rb +47 -0
- data/lib/jekyll-l10n/utils/text_validator.rb +35 -0
- data/lib/jekyll-l10n/utils/translation_resolver.rb +115 -0
- data/lib/jekyll-l10n/utils/url_path_builder.rb +65 -0
- data/lib/jekyll-l10n/utils/url_transformer.rb +141 -0
- data/lib/jekyll-l10n/utils/xpath_reference_generator.rb +45 -0
- data/lib/jekyll-l10n/version.rb +10 -0
- data/lib/jekyll-l10n.rb +268 -0
- 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
|