alchemy_i18n 5.1.0 → 8.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ab8a9e53a419ef8c25bb682c44271ad39fe5ed22df64208670828bbfea838b1
4
- data.tar.gz: 255cb8c9e05fbb283306b4b8b7122f3c02ef543706a3ea7477ca5276f3445f0e
3
+ metadata.gz: 06bfa8855d2267a11cec2a6792fbbab9698ac5abc29bb5d92d393d9d364c2623
4
+ data.tar.gz: 3404d4158ccb653f16972e383ba4281efa08ab6a185f3b78d7e4c80ddee6ce7b
5
5
  SHA512:
6
- metadata.gz: d45d6c6aaa9feb157fd50f59cef68fa583e9fc0da108ced892c4a9fdd2fe9c814628b934d517ef493b7e14de1336f62e4bfa2dc570fd01c939c3b56b4d1d0d82
7
- data.tar.gz: 591cd7f7e0496b33329d8c93c1199cf97c123bb2a0e0861f009bd0695fe3c12d5f75764b9d673cffb5a86a452d459c61e91df1dcf10c3196533b83ca9a4d8e10
6
+ metadata.gz: 63b2fc55c879c71a3e30746521edb0dd686dd1bc1dcea8b1a1cd8374bba5b0d3844f42dac0ba981b2ec04625c19c89df4c4a9c54c8189348c3584102a3cab511
7
+ data.tar.gz: 8b9005ecf643989a9911a2bc1314bc5b712b0db07c27df304c5994571dd9f5c549bc747ec9be565bd534d670fd49b387697092334c9e28ae0d684049a1b63d38
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 8.3.0 (2026-06-25)
4
+
5
+ <!-- Release notes generated using configuration in .github/release.yml at main -->
6
+
7
+ ## What's Changed
8
+ ### New Features
9
+ * Alchemy 8.3 by @tvdeyen in https://github.com/AlchemyCMS/alchemy_i18n/pull/98
10
+ ### Bug Fixes
11
+ * Update translations for alchemy/page/sitemap and remove unused show_page_in_translation key by @antwertinger in https://github.com/AlchemyCMS/alchemy_i18n/pull/97
12
+
13
+
14
+ **Full Changelog**: https://github.com/AlchemyCMS/alchemy_i18n/compare/v8.2.0...v8.3.0
15
+
16
+ ## 8.2.0 (2026-04-13)
17
+
18
+ <!-- Release notes generated using configuration in .github/release.yml at main -->
19
+
20
+ ## What's Changed
21
+ ### Other Changes
22
+ * Translations for schedule element button by @tvdeyen in https://github.com/AlchemyCMS/alchemy_i18n/pull/88
23
+ * Add XML to show_page_in_sitemap translations across all locales by @antwertinger in https://github.com/AlchemyCMS/alchemy_i18n/pull/92
24
+ * Add missing Alchemy 8.2 translations by @tvdeyen in https://github.com/AlchemyCMS/alchemy_i18n/pull/93
25
+
26
+ ## New Contributors
27
+ * @antwertinger made their first contribution in https://github.com/AlchemyCMS/alchemy_i18n/pull/92
28
+
29
+ **Full Changelog**: https://github.com/AlchemyCMS/alchemy_i18n/compare/v5.1.0...v8.2.0
30
+
3
31
  ## 5.1.0 (2026-02-18)
4
32
 
5
33
  ## What's Changed
data/README.md CHANGED
@@ -1,17 +1,21 @@
1
1
  # AlchemyCMS Translations
2
2
 
3
- Translations files for AlchemyCMS 8.0 and above.
3
+ Translation files for [AlchemyCMS](https://github.com/AlchemyCMS/alchemy_cms) 8.0 and above.
4
4
 
5
5
  ## Alchemy version
6
6
 
7
- - For a Alchemy 7.x compatible version use v4.3.1 or the `4.3-stable` branch.
8
- - For a Alchemy 7.0 compatible version use v4.0.2 or the `4.0-stable` branch.
9
- - For a Alchemy 6.1 compatible version use v3.2.0 or the `3.2-stable` branch.
10
- - For a Alchemy 5.3 compatible version use v2.3.1 or the `2.3-stable` branch.
7
+ | Alchemy CMS | alchemy_i18n | Branch |
8
+ |-------------|-------------|---------------|
9
+ | 8.2 | >= 8.2.0 | `8.2-stable` |
10
+ | 8.0 | 5.x | `5.1-stable` |
11
+ | 7.x | 4.3.x | `4.3-stable` |
12
+ | 7.0 | 4.0.x | `4.0-stable` |
13
+ | 6.1 | 3.2.x | `3.2-stable` |
14
+ | 5.3 | 2.3.x | `2.3-stable` |
11
15
 
12
16
  ## Installation
13
17
 
14
- Add this Gem to your `Gemfile` and run the install generator
18
+ Add the gem to your `Gemfile` and run the install generator:
15
19
 
16
20
  ```
17
21
  bundle add alchemy_i18n
@@ -19,23 +23,25 @@ bin/rails g alchemy_i18n:install
19
23
  ```
20
24
 
21
25
  Pass the locales you want to generate files for with the `--locales` option.
22
- Seperate multiple locales by space.
26
+ Separate multiple locales by space.
23
27
 
24
28
  ```
25
- bin/rails g alchemy_i18n:install --locales=de it es
29
+ bin/rails g alchemy_i18n:install --locales de it es
26
30
  ```
27
31
 
28
- ---
32
+ ## Available locales
29
33
 
30
- A ruby translation project managed on [Locale](http://www.localeapp.com/) that's open to all!
34
+ `de` `es` `fr` `it` `nb-NO` `nl` `pl` `ru` `zh-CN`
31
35
 
32
- ## Contributing to AlchemyCMS/alchemy_i18n
36
+ ### Noteworthy missing translations
33
37
 
34
- - Edit the translations directly on the [AlchemyCMS/alchemy_i18n](http://www.localeapp.com/projects/public?search=AlchemyCMS/alchemy_i18n) project on Locale.
35
- - **That's it!**
36
- - The maintainer will then pull translations from the Locale project and push to Github.
38
+ Russian (`ru`) and Chinese (`zh-CN`) are at roughly 68% coverage and need help.
37
39
 
38
- Happy translating!
40
+ ## Contributing
41
+
42
+ 1. Fork the repository
43
+ 2. Add or update translations in the locale files under `locales/`
44
+ 3. Open a pull request
39
45
 
40
46
  ## License
41
47
 
@@ -1,4 +1,5 @@
1
1
  Alchemy.translations = {
2
+ Add: "Erstelle",
2
3
  allowed_chars: "von %{count} Zeichen",
3
4
  cancel: "Abbrechen",
4
5
  cancelled: "Abgebrochen",
@@ -23,11 +24,17 @@ Alchemy.translations = {
23
24
  "Maximum number of files exceeded":
24
25
  "Maximale Anzahl gleichzeitig erlaubter Datei-Uploads erreicht.",
25
26
  "No anchors found": "Keine Sprungmarken gefunden.",
27
+ "No results found": "Keine Ergebnisse.",
26
28
  None: "Keine",
27
29
  "Uploaded bytes exceed file size":
28
30
  "Maximale Größe der erlaubten Dateigröße erreicht.",
29
31
  "Select a page first": "Zuerst eine Seite wählen.",
30
32
  "Successfully moved menu item": "Menü-Eintrag wurde erfolgreich bewegt.",
33
+ "Abort upload": "Upload abbrechen",
34
+ "Cancel all uploads": "Alle Uploads abbrechen",
35
+ "Clear selection": "Auswahl aufheben",
36
+ Remove: "Entfernen",
37
+ Close: "Schließen",
31
38
  formats: {
32
39
  datetime: "d.m.Y H:i",
33
40
  date: "d.m.Y",
@@ -1,4 +1,5 @@
1
1
  Alchemy.translations = {
2
+ Add: "Crear",
2
3
  allowed_chars: "de %{count} caracteres",
3
4
  cancel: "Cancelar",
4
5
  cancelled: "Cancelado",
@@ -13,14 +14,25 @@ Alchemy.translations = {
13
14
  page_found: "Página encontrada",
14
15
  pages_found: "Páginas encontradas",
15
16
  "Please confirm": "Por favor confirma",
17
+ "Preview failed to load":
18
+ "No se pudo cargar la vista previa. Inténtalo de nuevo.",
16
19
  url_validation_failed: "La url no tiene un formato válido.",
17
20
  warning: "¡Atención!",
18
21
  "File is too large": "El archivo es demasiado grande",
19
22
  "File is too small": "El archivo es demasiado pequeño",
20
23
  "File type not allowed": "El tipo de archivo no está permitido",
21
24
  "Maximum number of files exceeded": "Número máximo de archivos excedido.",
25
+ "No results found": "No se encontraron resultados.",
22
26
  "Uploaded bytes exceed file size":
23
27
  "Los bytes subidos exceden el tamaño del archivo",
28
+ "Abort upload": "Interrumpir subida",
29
+ "Cancel all uploads": "Cancelar todas las subidas",
30
+ "Clear selection": "Borrar selección",
31
+ Remove: "Eliminar",
32
+ None: "Ninguno",
33
+ "No anchors found": "No se encontraron anclas.",
34
+ "Select a page first": "Selecciona primero una página.",
35
+ Close: "Cerrar",
24
36
  formats: {
25
37
  datetime: "d/m/Y H:i",
26
38
  date: "d/m/Y",
@@ -1,4 +1,5 @@
1
1
  Alchemy.translations = {
2
+ Add: "Créer",
2
3
  allowed_chars: "de %{count} caractères",
3
4
  cancel: "abandonner",
4
5
  cancelled: "annulé",
@@ -13,6 +14,8 @@ Alchemy.translations = {
13
14
  page_found: "page trouvée",
14
15
  pages_found: "pages trouvées",
15
16
  "Please confirm": "Veuillez confirmer",
17
+ "Preview failed to load":
18
+ "Échec du chargement de l’aperçu. Veuillez réessayer.",
16
19
  url_validation_failed: "L’URL n’est pas correctement formatée.",
17
20
  warning: "Attention!",
18
21
  "File is too large": "Le fichier est trop grand.",
@@ -20,8 +23,17 @@ Alchemy.translations = {
20
23
  "File type not allowed": "Type de document non autorisé.",
21
24
  "Maximum number of files exceeded":
22
25
  "Le nombre maximum de fichiers est atteint.",
26
+ "No results found": "Aucun résultat trouvé.",
23
27
  "Uploaded bytes exceed file size":
24
28
  "Taille de fichier maximale autorisée atteint.",
29
+ "Abort upload": "Interrompre l’envoi",
30
+ "Cancel all uploads": "Annuler tous les envois",
31
+ "Clear selection": "Effacer la sélection",
32
+ Remove: "Supprimer",
33
+ None: "Aucun",
34
+ "No anchors found": "Aucune ancre trouvée.",
35
+ "Select a page first": "Sélectionnez d’abord une page.",
36
+ Close: "Fermer",
25
37
  formats: {
26
38
  datetime: "d.m.Y H:i",
27
39
  date: "d.m.Y",
@@ -1,4 +1,5 @@
1
1
  Alchemy.translations = {
2
+ Add: "Crea",
2
3
  allowed_chars: "di %{count} caratteri",
3
4
  cancel: "Annulla",
4
5
  cancelled: "Annullato",
@@ -13,17 +14,27 @@ Alchemy.translations = {
13
14
  page_found: "Pagina trovata",
14
15
  pages_found: "Pagine trovate",
15
16
  "Please confirm": "Per favore conferma",
17
+ "Preview failed to load": "Impossibile caricare l'anteprima. Riprova.",
16
18
  url_validation_failed: "Formato URL non valido.",
17
19
  warning: "Attenzione!",
18
20
  "File is too large": "Il file è troppo grande",
19
21
  "File is too small": "Il file è troppo piccolo",
20
22
  "File type not allowed": "Tipo di file non consentito",
21
23
  "Maximum number of files exceeded": "Numero massimo di file raggiunto.",
24
+ "No results found": "Nessun risultato trovato.",
22
25
  "Uploaded bytes exceed file size":
23
26
  "Il numero di byte caricati eccede la dimensione del file",
27
+ "Abort upload": "Interrompi caricamento",
28
+ "Cancel all uploads": "Annulla tutti i caricamenti",
29
+ "Clear selection": "Cancella selezione",
30
+ Remove: "Rimuovi",
31
+ None: "Nessuno",
32
+ "No anchors found": "Nessuna ancora trovata.",
33
+ "Select a page first": "Seleziona prima una pagina.",
34
+ Close: "Chiudi",
24
35
  formats: {
25
- datetime: "Y-m-d H:i",
26
- date: "Y-m-d",
36
+ datetime: "d/m/Y H:i",
37
+ date: "d/m/Y",
27
38
  time: "H:i",
28
39
  time_24hr: true,
29
40
  },
@@ -1,5 +1,6 @@
1
1
  Alchemy.translations = {
2
- allowed_chars: "of %{count} chars",
2
+ Add: "Aanmaken",
3
+ allowed_chars: "van %{count} tekens",
3
4
  cancel: "Annuleren",
4
5
  cancelled: "Afgebroken",
5
6
  click_to_edit: "klik om aan te passen",
@@ -8,21 +9,33 @@ Alchemy.translations = {
8
9
  "Dit element bevat niet opgeslagen wijzigingen. Wilt u dit echt inklappen?",
9
10
  help: "Hulp",
10
11
  ok: "Ok",
11
- page_found: "Page found",
12
- pages_found: "Pages found",
12
+ page_found: "Pagina gevonden",
13
+ pages_found: "Pagina's gevonden",
13
14
  page_dirty_notice:
14
15
  "Er zijn wijzigingen op deze pagina aangebracht. Deze gaan verloren als u doorgaat.",
15
16
  "Please confirm": "Bevestig alstublieft",
17
+ "Preview failed to load":
18
+ "Voorbeeld kon niet worden geladen. Probeer het opnieuw.",
16
19
  url_validation_failed: "De url heeft geen geldig formaat.",
17
20
  warning: "Waarschuwing!",
18
- "File is too large": "File is too large",
19
- "File is too small": "File is too small",
20
- "File type not allowed": "File type not allowed",
21
+ "File is too large": "Bestand is te groot.",
22
+ "File is too small": "Bestand is te klein.",
23
+ "File type not allowed": "Bestandstype niet toegestaan.",
21
24
  "Maximum number of files exceeded": "Uploadlimiet bereikt.",
22
- "Uploaded bytes exceed file size": "Uploaded bytes exceed file size",
25
+ "No results found": "Geen resultaten gevonden.",
26
+ "Uploaded bytes exceed file size":
27
+ "Geüploade bytes overschrijden de bestandsgrootte.",
28
+ "Abort upload": "Upload afbreken",
29
+ "Cancel all uploads": "Alle uploads annuleren",
30
+ "Clear selection": "Selectie wissen",
31
+ Remove: "Verwijderen",
32
+ None: "Geen",
33
+ "No anchors found": "Geen ankers gevonden.",
34
+ "Select a page first": "Selecteer eerst een pagina.",
35
+ Close: "Sluiten",
23
36
  formats: {
24
- datetime: "Y-m-d H:i",
25
- date: "Y-m-d",
37
+ datetime: "d-m-Y H:i",
38
+ date: "d-m-Y",
26
39
  time: "H:i",
27
40
  time_24hr: true,
28
41
  },
@@ -1,4 +1,5 @@
1
1
  Alchemy.translations = {
2
+ Add: "Создать",
2
3
  allowed_chars: "%{count} знаков",
3
4
  cancel: "Отмена",
4
5
  cancelled: "Отменено",
@@ -13,6 +14,8 @@ Alchemy.translations = {
13
14
  page_found: "Страница найдена",
14
15
  pages_found: "Страницы найдены",
15
16
  "Please confirm": "Пожалуйста, подтвердите",
17
+ "Preview failed to load":
18
+ "Не удалось загрузить предпросмотр. Попробуйте ещё раз.",
16
19
  url_validation_failed: "Адрес (URL) имеет неверный формат.",
17
20
  warning: "Внимание!",
18
21
  "File is too large": "Файл слишком большой",
@@ -20,7 +23,16 @@ Alchemy.translations = {
20
23
  "File type not allowed": "Этот тип файла не разрешен",
21
24
  "Maximum number of files exceeded":
22
25
  "Исчерпано максимальное количество файлов.",
26
+ "No results found": "Ничего не найдено.",
23
27
  "Uploaded bytes exceed file size": "Превышен максимальный размер файла",
28
+ "Abort upload": "Прервать загрузку",
29
+ "Cancel all uploads": "Отменить все загрузки",
30
+ "Clear selection": "Очистить выбор",
31
+ Remove: "Удалить",
32
+ None: "Не выбрано",
33
+ "No anchors found": "Якоря не найдены.",
34
+ "Select a page first": "Сначала выберите страницу.",
35
+ Close: "Закрыть",
24
36
  formats: {
25
37
  datetime: "d.m.Y H:i",
26
38
  date: "d.m.Y",
@@ -1,4 +1,5 @@
1
1
  Alchemy.translations = {
2
+ Add: "新建",
2
3
  allowed_chars: "%{count} 个字符",
3
4
  cancel: "取消",
4
5
  cancelled: "已取消",
@@ -12,16 +13,26 @@ Alchemy.translations = {
12
13
  page_found: "找到页面",
13
14
  pages_found: "找到页面",
14
15
  "Please confirm": "请确认",
16
+ "Preview failed to load": "预览加载失败。请重试。",
15
17
  url_validation_failed: "页面地址格式错误。",
16
18
  warning: "警告!",
17
19
  "File is too large": "文件太大",
18
20
  "File is too small": "文件太小",
19
21
  "File type not allowed": "文件类型不支持",
20
22
  "Maximum number of files exceeded": "超出最大允许的文件数。",
23
+ "No results found": "未找到结果。",
21
24
  "Uploaded bytes exceed file size": "上传得字节数超过文件大小",
25
+ "Abort upload": "中止上传",
26
+ "Cancel all uploads": "取消所有上传",
27
+ "Clear selection": "清除选择",
28
+ Remove: "移除",
29
+ None: "无",
30
+ "No anchors found": "未找到锚点。",
31
+ "Select a page first": "请先选择页面。",
32
+ Close: "关闭",
22
33
  formats: {
23
- datetime: "Y-m-d H:i",
24
- date: "Y-m-d",
34
+ datetime: "Ymd H:i",
35
+ date: "Ymd",
25
36
  time: "H:i",
26
37
  time_24hr: true,
27
38
  },
@@ -1,3 +1,3 @@
1
1
  module AlchemyI18n
2
- VERSION = "5.1.0"
2
+ VERSION = "8.3.0"
3
3
  end
@@ -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