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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +21 -15
- data/app/views/alchemy/admin/translations/_de.js +7 -0
- data/app/views/alchemy/admin/translations/_es.js +12 -0
- data/app/views/alchemy/admin/translations/_fr.js +12 -0
- data/app/views/alchemy/admin/translations/_it.js +13 -2
- data/app/views/alchemy/admin/translations/_nl.js +22 -9
- data/app/views/alchemy/admin/translations/_ru.js +12 -0
- data/app/views/alchemy/admin/translations/_zh-CN.js +13 -2
- data/lib/alchemy_i18n/version.rb +1 -1
- data/lib/tasks/translations.rake +169 -0
- data/locales/alchemy.de.yml +200 -43
- data/locales/alchemy.es.yml +396 -117
- data/locales/alchemy.fr.yml +434 -67
- data/locales/alchemy.it.yml +442 -117
- data/locales/alchemy.nb-NO.yml +1078 -721
- data/locales/alchemy.nl.yml +379 -80
- data/locales/alchemy.pl.yml +893 -587
- data/locales/alchemy.ru.yml +7 -6
- data/locales/alchemy.zh-CN.yml +7 -6
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 06bfa8855d2267a11cec2a6792fbbab9698ac5abc29bb5d92d393d9d364c2623
|
|
4
|
+
data.tar.gz: 3404d4158ccb653f16972e383ba4281efa08ab6a185f3b78d7e4c80ddee6ce7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
3
|
+
Translation files for [AlchemyCMS](https://github.com/AlchemyCMS/alchemy_cms) 8.0 and above.
|
|
4
4
|
|
|
5
5
|
## Alchemy version
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
-
|
|
26
|
+
Separate multiple locales by space.
|
|
23
27
|
|
|
24
28
|
```
|
|
25
|
-
bin/rails g alchemy_i18n:install --locales
|
|
29
|
+
bin/rails g alchemy_i18n:install --locales de it es
|
|
26
30
|
```
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
## Available locales
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
`de` `es` `fr` `it` `nb-NO` `nl` `pl` `ru` `zh-CN`
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
### Noteworthy missing translations
|
|
33
37
|
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
26
|
-
date: "Y
|
|
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
|
-
|
|
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: "
|
|
12
|
-
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": "
|
|
19
|
-
"File is too small": "
|
|
20
|
-
"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
|
-
"
|
|
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: "
|
|
25
|
-
date: "
|
|
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
|
|
24
|
-
date: "Y
|
|
34
|
+
datetime: "Y年m月d日 H:i",
|
|
35
|
+
date: "Y年m月d日",
|
|
25
36
|
time: "H:i",
|
|
26
37
|
time_24hr: true,
|
|
27
38
|
},
|
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
|