alchemy_i18n 5.0.2 → 8.2.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/CHANGELOG.md +23 -0
- data/app/views/alchemy/admin/translations/_de.js +2 -0
- data/lib/alchemy_i18n/version.rb +1 -1
- data/lib/tasks/translations.rake +169 -0
- data/locales/alchemy.de.yml +177 -40
- data/locales/alchemy.es.yml +372 -115
- data/locales/alchemy.fr.yml +407 -63
- data/locales/alchemy.it.yml +417 -115
- data/locales/alchemy.nb-NO.yml +1079 -721
- data/locales/alchemy.nl.yml +375 -75
- data/locales/alchemy.pl.yml +894 -587
- data/locales/alchemy.ru.yml +2 -1
- data/locales/alchemy.zh-CN.yml +2 -1
- metadata +3 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b673fa605827b72a338cb5705d976eeb3cb6cfc1ceedc0f8d83e5f7a2be70591
|
|
4
|
+
data.tar.gz: 35ffeec42fe5b7eeffb1fbb6b4c67ff8a35faa98f4495c0f291962fa7e079dd2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2dbfb604ff8191cbebb82e6358dc37457972fe50c327747ac4eb9b981ccdeb11720de87274bf432dfc3936c1d53ee7d75d8675e74ab84158371a4427138357a7
|
|
7
|
+
data.tar.gz: '0758bc0a5c3fd74b2e7c372c8a039d7e5a0b13490541ac92ad17d0a5a2cf16577b638f5d7797f0de01d377b1d42e42381f5fc555e8d9cfd8187c2f3750b71ea8'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 8.2.0 (2026-04-13)
|
|
4
|
+
|
|
5
|
+
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
|
6
|
+
|
|
7
|
+
## What's Changed
|
|
8
|
+
### Other Changes
|
|
9
|
+
* Translations for schedule element button by @tvdeyen in https://github.com/AlchemyCMS/alchemy_i18n/pull/88
|
|
10
|
+
* Add XML to show_page_in_sitemap translations across all locales by @antwertinger in https://github.com/AlchemyCMS/alchemy_i18n/pull/92
|
|
11
|
+
* Add missing Alchemy 8.2 translations by @tvdeyen in https://github.com/AlchemyCMS/alchemy_i18n/pull/93
|
|
12
|
+
|
|
13
|
+
## New Contributors
|
|
14
|
+
* @antwertinger made their first contribution in https://github.com/AlchemyCMS/alchemy_i18n/pull/92
|
|
15
|
+
|
|
16
|
+
**Full Changelog**: https://github.com/AlchemyCMS/alchemy_i18n/compare/v5.1.0...v8.2.0
|
|
17
|
+
|
|
18
|
+
## 5.1.0 (2026-02-18)
|
|
19
|
+
|
|
20
|
+
## What's Changed
|
|
21
|
+
* Add German publish button translations by @tvdeyen in https://github.com/AlchemyCMS/alchemy_i18n/pull/89
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
**Full Changelog**: https://github.com/AlchemyCMS/alchemy_i18n/compare/v5.0.2...v5.1.0
|
|
25
|
+
|
|
3
26
|
## 5.0.2 (2026-01-28)
|
|
4
27
|
|
|
5
28
|
## What's Changed
|
data/lib/alchemy_i18n/version.rb
CHANGED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
namespace :translations do
|
|
4
|
+
ALCHEMY_CMS_PATH = ENV.fetch("ALCHEMY_CMS_PATH", File.expand_path("~/code/alchemy_cms"))
|
|
5
|
+
EN_LOCALE_PATH = File.join(ALCHEMY_CMS_PATH, "config/locales/alchemy.en.yml")
|
|
6
|
+
LOCALES_DIR = File.expand_path("../../locales", __dir__)
|
|
7
|
+
|
|
8
|
+
LOCALE_FILES = Dir.glob(File.join(LOCALES_DIR, "alchemy.*.yml")).sort
|
|
9
|
+
|
|
10
|
+
def flatten_keys(hash, prefix = "")
|
|
11
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
12
|
+
full_key = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
|
|
13
|
+
if value.is_a?(Hash)
|
|
14
|
+
result.merge!(flatten_keys(value, full_key))
|
|
15
|
+
else
|
|
16
|
+
result[full_key] = value
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def load_en_keys
|
|
22
|
+
en = YAML.load_file(EN_LOCALE_PATH, aliases: true)
|
|
23
|
+
flatten_keys(en["en"])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def load_locale_keys(file)
|
|
27
|
+
locale_code = File.basename(file, ".yml").sub("alchemy.", "")
|
|
28
|
+
data = YAML.load_file(file, aliases: true)
|
|
29
|
+
[locale_code, flatten_keys(data[locale_code])]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Extracts translation keys from Alchemy.t() calls, resolving scopes.
|
|
33
|
+
# All Alchemy.t() calls are automatically scoped under "alchemy.",
|
|
34
|
+
# and additional scope: parameters are appended.
|
|
35
|
+
#
|
|
36
|
+
# Examples:
|
|
37
|
+
# Alchemy.t(:hello) => "alchemy.hello"
|
|
38
|
+
# Alchemy.t(:name, scope: :filters) => "alchemy.filters.name"
|
|
39
|
+
# Alchemy.t(:all, scope: [:filters, :page]) => "alchemy.filters.page.all"
|
|
40
|
+
# Alchemy.t("Welcome") => "alchemy.Welcome"
|
|
41
|
+
def used_translation_keys
|
|
42
|
+
keys = []
|
|
43
|
+
# Match full Alchemy.t(...) calls, capturing the entire argument list
|
|
44
|
+
call_pattern = /Alchemy\.t\(([^)]+)\)/
|
|
45
|
+
|
|
46
|
+
Dir.glob(File.join(ALCHEMY_CMS_PATH, "{app,lib}", "**", "*.{rb,erb}")).each do |file|
|
|
47
|
+
content = File.read(file)
|
|
48
|
+
content.scan(call_pattern) do |args_str,|
|
|
49
|
+
# Skip calls with interpolation
|
|
50
|
+
next if args_str.include?('#{')
|
|
51
|
+
|
|
52
|
+
# Extract the key (first argument)
|
|
53
|
+
key = case args_str
|
|
54
|
+
when /\A:(\w+[?!]?)/ then $1
|
|
55
|
+
when /\A"([^"]+)"/ then $1
|
|
56
|
+
when /\A'([^']+)'/ then $1
|
|
57
|
+
else next
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Extract scope if present, but only from this call (not nested Alchemy.t calls)
|
|
61
|
+
scope_source = args_str.sub(/Alchemy\.t\(.*/, "")
|
|
62
|
+
scope_parts = ["alchemy"]
|
|
63
|
+
if scope_source =~ /scope:\s*\[([^\]]+)\]/
|
|
64
|
+
scope_content = $1
|
|
65
|
+
# Skip if scope contains dynamic parts (variables, method calls)
|
|
66
|
+
# Only symbols (:foo) and strings ("foo"/'foo') are static
|
|
67
|
+
next unless scope_content.strip.split(/\s*,\s*/).all? { |part|
|
|
68
|
+
part =~ /\A:\w+\z/ || part =~ /\A["'][^"']+["']\z/
|
|
69
|
+
}
|
|
70
|
+
scope_content.scan(/(?::(\w+)|"([^"]+)"|'([^']+)')/).each do |sym, dq, sq|
|
|
71
|
+
scope_parts << (sym || dq || sq)
|
|
72
|
+
end
|
|
73
|
+
elsif scope_source =~ /scope:\s*:(\w+)/
|
|
74
|
+
scope_parts << $1 unless $1 == "alchemy"
|
|
75
|
+
elsif scope_source =~ /scope:\s*['"]([^'"]+)['"]/
|
|
76
|
+
scope_parts << $1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
keys << (scope_parts + [key]).join(".")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
keys.uniq.sort
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
desc "Show missing translations for a locale (e.g. rake translations:missing[de])"
|
|
87
|
+
task :missing, [:locale] do |_, args|
|
|
88
|
+
locale = args[:locale]
|
|
89
|
+
|
|
90
|
+
unless File.exist?(EN_LOCALE_PATH)
|
|
91
|
+
abort "ERROR: English locale not found at #{EN_LOCALE_PATH}\nSet ALCHEMY_CMS_PATH to your alchemy_cms checkout."
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
file = File.join(LOCALES_DIR, "alchemy.#{locale}.yml")
|
|
95
|
+
abort "ERROR: Locale file not found: #{file}" unless File.exist?(file)
|
|
96
|
+
|
|
97
|
+
en_flat = load_en_keys
|
|
98
|
+
_, locale_flat = load_locale_keys(file)
|
|
99
|
+
|
|
100
|
+
# Keys from EN locale file missing in target
|
|
101
|
+
missing_from_en = (en_flat.keys - locale_flat.keys).reject { |k| en_flat[k].nil? }
|
|
102
|
+
|
|
103
|
+
# Keys used in code but not in target locale
|
|
104
|
+
used_keys = used_translation_keys
|
|
105
|
+
missing_from_code = used_keys.reject { |k| locale_flat.key?(k) }
|
|
106
|
+
|
|
107
|
+
# Exclude keys already reported as missing from EN
|
|
108
|
+
missing_from_en_set = missing_from_en.to_set
|
|
109
|
+
missing_from_code.reject! { |k| missing_from_en_set.include?(k) }
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if missing_from_en.any?
|
|
113
|
+
puts "=== Missing from EN locale file (#{missing_from_en.size}) ==="
|
|
114
|
+
missing_from_en.sort.each { |k| puts " #{k}: #{en_flat[k].inspect}" }
|
|
115
|
+
puts
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if missing_from_code.any?
|
|
119
|
+
puts "=== Used in code but missing (#{missing_from_code.size}) ==="
|
|
120
|
+
missing_from_code.sort.each { |k| puts " #{k}" }
|
|
121
|
+
puts
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if missing_from_en.empty? && missing_from_code.empty?
|
|
125
|
+
puts "No missing translations for #{locale}!"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
desc "Show missing translation summary for all locales"
|
|
130
|
+
task :status do
|
|
131
|
+
unless File.exist?(EN_LOCALE_PATH)
|
|
132
|
+
abort "ERROR: English locale not found at #{EN_LOCALE_PATH}\nSet ALCHEMY_CMS_PATH to your alchemy_cms checkout."
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
en_flat = load_en_keys
|
|
136
|
+
en_count = en_flat.reject { |_, v| v.nil? }.size
|
|
137
|
+
|
|
138
|
+
puts "Reference: EN locale (#{en_count} keys)"
|
|
139
|
+
puts "-" * 50
|
|
140
|
+
|
|
141
|
+
LOCALE_FILES.each do |file|
|
|
142
|
+
locale_code, locale_flat = load_locale_keys(file)
|
|
143
|
+
missing = (en_flat.keys - locale_flat.keys).reject { |k| en_flat[k].nil? }
|
|
144
|
+
extra = locale_flat.keys - en_flat.keys
|
|
145
|
+
pct = ((en_count - missing.size).to_f / en_count * 100).round(1)
|
|
146
|
+
puts "#{locale_code.ljust(6)} #{locale_flat.size} keys, #{missing.size} missing, #{extra.size} extra (#{pct}%)"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
desc "Validate YAML syntax for all locale files"
|
|
151
|
+
task :validate do
|
|
152
|
+
errors = []
|
|
153
|
+
LOCALE_FILES.each do |file|
|
|
154
|
+
begin
|
|
155
|
+
YAML.load_file(file, aliases: true)
|
|
156
|
+
rescue => e
|
|
157
|
+
errors << "#{File.basename(file)}: #{e.message}"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if errors.any?
|
|
162
|
+
puts "YAML errors found:"
|
|
163
|
+
errors.each { |e| puts " #{e}" }
|
|
164
|
+
exit 1
|
|
165
|
+
else
|
|
166
|
+
puts "All #{LOCALE_FILES.size} locale files are valid YAML."
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|