jekyll-theme-zer0 1.25.0 → 1.27.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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +201 -0
  3. data/README.md +11 -28
  4. data/_data/authors.yml +4 -3
  5. data/_data/backlog.yml +54 -1
  6. data/_data/features.yml +101 -61
  7. data/_data/feedback_types.yml +148 -0
  8. data/_data/i18n/languages.yml +36 -0
  9. data/_data/navigation/README.md +1 -1
  10. data/_data/theme-manifest.yml +0 -2
  11. data/_data/ui-text.yml +36 -246
  12. data/_includes/README.md +4 -0
  13. data/_includes/components/README.md +1 -0
  14. data/_includes/components/author-avatar-url.html +4 -2
  15. data/_includes/components/env-switcher.html +3 -1
  16. data/_includes/components/language-toggle.html +81 -0
  17. data/_includes/components/page-feedback.html +252 -0
  18. data/_includes/components/search-modal.html +2 -2
  19. data/_includes/components/shortcuts-modal.html +1 -1
  20. data/_includes/components/translation-notice.html +27 -0
  21. data/_includes/content/intro.html +35 -76
  22. data/_includes/core/console-capture.html +89 -0
  23. data/_includes/core/footer.html +9 -4
  24. data/_includes/core/head.html +13 -0
  25. data/_includes/core/header.html +9 -4
  26. data/_includes/core/hreflang.html +33 -0
  27. data/_includes/core/i18n.html +36 -0
  28. data/_includes/navigation/breadcrumbs.html +1 -1
  29. data/_includes/navigation/navbar.html +5 -4
  30. data/_includes/navigation/sidebar-right.html +3 -2
  31. data/_includes/navigation/unified-drawer.html +1 -1
  32. data/_layouts/article.html +7 -1
  33. data/_layouts/default.html +5 -3
  34. data/_layouts/news.html +4 -2
  35. data/_layouts/root.html +17 -8
  36. data/_layouts/section.html +4 -2
  37. data/_sass/components/_page-feedback.scss +276 -0
  38. data/_sass/core/_docs-layout.scss +2 -78
  39. data/_sass/core/_obsidian.scss +9 -1
  40. data/_sass/layouts/_navbar-extras.scss +6 -1
  41. data/_sass/tokens/_layers.scss +4 -0
  42. data/assets/css/main.scss +1 -0
  43. data/assets/js/obsidian-graph.js +5 -1
  44. data/assets/js/page-feedback.js +660 -0
  45. data/scripts/README.md +20 -26
  46. data/scripts/bin/audit-consumer +1 -1
  47. data/scripts/bin/manifest +0 -1
  48. data/scripts/bin/sync-plugins +0 -1
  49. data/scripts/dev/rasterize-svg.js +65 -0
  50. data/scripts/features/generate-preview-images +49 -1390
  51. data/scripts/features/install-preview-generator +55 -33
  52. data/scripts/install/README.md +9 -20
  53. data/scripts/install/ai/prompts/wizard.system.md +8 -17
  54. data/scripts/lib/README.md +1 -5
  55. data/scripts/lib/install/deploy/README.md +3 -9
  56. data/scripts/lib/preview_generator.py +2261 -1341
  57. data/scripts/translate.rb +1114 -0
  58. metadata +14 -4
  59. data/_data/prompts.yml +0 -302
  60. data/_plugins/preview_image_generator.rb +0 -351
data/_data/ui-text.yml CHANGED
@@ -1,6 +1,16 @@
1
- # User interface text and labels
2
- # Used by: Various includes and layouts throughout the theme
3
- # Purpose: Multilingual UI strings for internationalization (i18n)
1
+ # User interface text and labels — ENGLISH SOURCE OF TRUTH
2
+ # Used by: _includes/core/i18n.html (and through it, layouts/includes)
3
+ #
4
+ # Multilingual model (ZER0-078): this file holds ONLY the English strings.
5
+ # Alternate languages are NOT hand-maintained here anymore — they are
6
+ # machine-generated into _data/i18n/<lang>.yml by scripts/translate.rb
7
+ # (Claude Code OAuth) via .github/workflows/translate.yml. To change a
8
+ # translated label, edit the English string here and let the translate
9
+ # workflow regenerate the alternates. Languages are configured under
10
+ # `translation.languages` in _config.yml.
11
+ #
12
+ # (The former hand-written es/fr/de/ar blocks were removed in favor of the
13
+ # generated pipeline — see docs/systems/multilingual-translation.md.)
4
14
 
5
15
  # English (default)
6
16
  # -----------------
@@ -8,6 +18,7 @@ en: &DEFAULT_EN
8
18
  skip_links : "Skip links"
9
19
  skip_primary_nav : "Skip to primary navigation"
10
20
  skip_content : "Skip to content"
21
+ skip_main_content : "Skip to main content"
11
22
  skip_footer : "Skip to footer"
12
23
  page : "Page"
13
24
  pagination_previous : "Previous"
@@ -79,7 +90,28 @@ en: &DEFAULT_EN
79
90
  shortcuts_toggle_sidebar : "Toggle sidebar"
80
91
  shortcuts_toggle_toc : "Toggle table of contents"
81
92
  shortcuts_show_help : "Show this help"
82
-
93
+ # Header / navbar chrome (ZER0-078)
94
+ home_label : "Home"
95
+ main_nav_label : "Main Navigation"
96
+ search_button_label : "Search"
97
+ settings_button_label : "Settings"
98
+ toc_title : "Table of Contents"
99
+ # Article intro / metadata chrome (ZER0-078)
100
+ published_label : "Published"
101
+ updated_label : "Updated"
102
+ reading_time_label : "Estimated reading time:"
103
+ reading_time_minute : "minute"
104
+ share_label : "Share"
105
+ copy_link_label : "Copy Link"
106
+ improve_label : "Improve"
107
+ edit_on_github : "Edit on GitHub"
108
+ # Language toggle & machine translation (ZER0-078)
109
+ lang_toggle_label : "Language"
110
+ lang_toggle_aria : "Change language"
111
+ lang_not_available : "Not yet translated"
112
+ machine_translation_notice : "This page was translated automatically from the English original."
113
+ machine_translation_view_source : "View the original"
114
+
83
115
  en-US:
84
116
  <<: *DEFAULT_EN
85
117
  en-CA:
@@ -88,245 +120,3 @@ en-GB:
88
120
  <<: *DEFAULT_EN
89
121
  en-AU:
90
122
  <<: *DEFAULT_EN
91
-
92
- # Spanish
93
- # -------
94
- es: &DEFAULT_ES
95
- skip_links : "Saltar enlaces"
96
- skip_primary_nav : "Saltar a navegación principal"
97
- skip_content : "Saltar a contenido"
98
- skip_footer : "Saltar a pie"
99
- page : "Página"
100
- pagination_previous : "Anterior"
101
- pagination_next : "Siguiente"
102
- breadcrumb_home_label : "Inicio"
103
- breadcrumb_separator : "/"
104
- menu_label : "Alternar menú"
105
- search_label : "Alternar búsqueda"
106
- toc_label : "En esta página"
107
- ext_link_label : "Enlace directo"
108
- less_than : "menos de"
109
- minute_read : "minuto(s) de lectura"
110
- share_on_label : "Compartir en"
111
- meta_label :
112
- tags_label : "Etiquetas:"
113
- categories_label : "Categorías:"
114
- date_label : "Actualizado:"
115
- author_label : "Autor:"
116
- comments_label : "Deja un comentario"
117
- comments_title : "Comentarios"
118
- more_label : "Ver más"
119
- related_label : "Puede que también te interese"
120
- follow_label : "Seguir:"
121
- feed_label : "Feed"
122
- powered_by : "Funciona con"
123
- website_label : "Sitio web"
124
- email_label : "Correo electrónico"
125
- recent_posts : "Entradas recientes"
126
- undefined_wpm : "El parámetro words_per_minute (palabras por minuto) no está definido en _config.yml"
127
- comment_form_info : "Tu dirección de correo electrónico no se publicará. Los campos obligatorios están marcados"
128
- comment_form_comment_label : "Comentario"
129
- comment_form_md_info : "Puedes utilizar Markdown"
130
- comment_form_name_label : "Nombre"
131
- comment_form_email_label : "Dirección de correo electrónico"
132
- comment_form_website_label : "Sitio web (opcional)"
133
- comment_btn_submit : "Enviar comentario"
134
- comment_btn_submitted : "Enviado"
135
- comment_success_msg : "¡Gracias por tu comentario! Se mostrará en el sitio una vez que haya sido aprobado."
136
- comment_error_msg : "Lo sentimos, ha habido un error con tu envío. Por favor, asegúrate de que todos los campos obligatorios se han completado e inténtalo de nuevo."
137
- loading_label : "Cargando..."
138
- search_label_text : "Introduce tu término de búsqueda..."
139
- search_placeholder_text : "Introduce tu término de búsqueda..."
140
- search_no_results : "Sin resultados"
141
- results_found : "Resultado(s) encontrado(s)"
142
- back_to_top : "Volver arriba"
143
- # Statistics labels
144
- stats_total_posts : "Total de Publicaciones"
145
- stats_total_pages : "Total de Páginas"
146
- stats_categories : "Categorías"
147
- stats_tags : "Etiquetas"
148
- stats_last_updated : "Última Actualización"
149
- stats_overview : "Resumen"
150
- stats_content_breakdown : "Desglose de Contenido"
151
- # Theme-specific labels
152
- theme_toggle : "Cambiar tema"
153
- dark_mode : "Modo oscuro"
154
- light_mode : "Modo claro"
155
- view_source : "Ver código fuente"
156
- edit_page : "Editar esta página"
157
- report_issue : "Reportar un problema"
158
-
159
- es-ES:
160
- <<: *DEFAULT_ES
161
- es-MX:
162
- <<: *DEFAULT_ES
163
-
164
- # French
165
- # ------
166
- fr: &DEFAULT_FR
167
- skip_links : "Liens de saut"
168
- skip_primary_nav : "Aller à la navigation principale"
169
- skip_content : "Aller au contenu"
170
- skip_footer : "Aller au pied de page"
171
- page : "Page"
172
- pagination_previous : "Précédent"
173
- pagination_next : "Suivant"
174
- breadcrumb_home_label : "Accueil"
175
- breadcrumb_separator : "/"
176
- menu_label : "Basculer le menu"
177
- search_label : "Basculer la recherche"
178
- toc_label : "Sur cette page"
179
- ext_link_label : "Lien direct"
180
- less_than : "moins de"
181
- minute_read : "minute(s) de lecture"
182
- share_on_label : "Partager sur"
183
- meta_label :
184
- tags_label : "Tags :"
185
- categories_label : "Catégories :"
186
- date_label : "Mis à jour :"
187
- author_label : "Auteur :"
188
- comments_label : "Laisser un commentaire"
189
- comments_title : "Commentaires"
190
- more_label : "En savoir plus"
191
- related_label : "Vous pourriez aussi aimer"
192
- follow_label : "Suivre :"
193
- feed_label : "Flux"
194
- powered_by : "Propulsé par"
195
- website_label : "Site web"
196
- email_label : "Email"
197
- recent_posts : "Articles récents"
198
- back_to_top : "Retour en haut"
199
- # Statistics labels
200
- stats_total_posts : "Total des Publications"
201
- stats_total_pages : "Total des Pages"
202
- stats_categories : "Catégories"
203
- stats_tags : "Tags"
204
- stats_last_updated : "Dernière Mise à Jour"
205
- stats_overview : "Aperçu"
206
- stats_content_breakdown : "Répartition du Contenu"
207
- # Theme-specific labels
208
- theme_toggle : "Changer de thème"
209
- dark_mode : "Mode sombre"
210
- light_mode : "Mode clair"
211
- view_source : "Voir la source"
212
- edit_page : "Modifier cette page"
213
- report_issue : "Signaler un problème"
214
-
215
- fr-FR:
216
- <<: *DEFAULT_FR
217
- fr-CA:
218
- <<: *DEFAULT_FR
219
-
220
- # German
221
- # ------
222
- de: &DEFAULT_DE
223
- skip_links : "Links überspringen"
224
- skip_primary_nav : "Zur Hauptnavigation springen"
225
- skip_content : "Zum Inhalt springen"
226
- skip_footer : "Zur Fußzeile springen"
227
- page : "Seite"
228
- pagination_previous : "Zurück"
229
- pagination_next : "Weiter"
230
- breadcrumb_home_label : "Startseite"
231
- breadcrumb_separator : "/"
232
- menu_label : "Menü umschalten"
233
- search_label : "Suche umschalten"
234
- toc_label : "Auf dieser Seite"
235
- ext_link_label : "Direktlink"
236
- less_than : "weniger als"
237
- minute_read : "Minute(n) Lesezeit"
238
- share_on_label : "Teilen auf"
239
- meta_label :
240
- tags_label : "Tags:"
241
- categories_label : "Kategorien:"
242
- date_label : "Aktualisiert:"
243
- author_label : "Autor:"
244
- comments_label : "Einen Kommentar hinterlassen"
245
- comments_title : "Kommentare"
246
- more_label : "Mehr erfahren"
247
- related_label : "Das könnte Sie auch interessieren"
248
- follow_label : "Folgen:"
249
- feed_label : "Feed"
250
- powered_by : "Betrieben mit"
251
- website_label : "Webseite"
252
- email_label : "E-Mail"
253
- recent_posts : "Neueste Beiträge"
254
- back_to_top : "Nach oben"
255
- # Statistics labels
256
- stats_total_posts : "Gesamte Beiträge"
257
- stats_total_pages : "Gesamte Seiten"
258
- stats_categories : "Kategorien"
259
- stats_tags : "Tags"
260
- stats_last_updated : "Zuletzt Aktualisiert"
261
- stats_overview : "Übersicht"
262
- stats_content_breakdown : "Inhaltsaufschlüsselung"
263
- # Theme-specific labels
264
- theme_toggle : "Thema wechseln"
265
- dark_mode : "Dunkler Modus"
266
- light_mode : "Heller Modus"
267
- view_source : "Quelle ansehen"
268
- edit_page : "Diese Seite bearbeiten"
269
- report_issue : "Ein Problem melden"
270
-
271
- de-DE:
272
- <<: *DEFAULT_DE
273
- de-AT:
274
- <<: *DEFAULT_DE
275
- de-CH:
276
- <<: *DEFAULT_DE
277
-
278
- # Arabic
279
- # ------
280
- ar: &DEFAULT_AR
281
- skip_links : "تخطي الروابط"
282
- skip_primary_nav : "انتقل إلى التنقل الرئيسي"
283
- skip_content : "انتقل إلى المحتوى"
284
- skip_footer : "انتقل إلى التذييل"
285
- page : "صفحة"
286
- pagination_previous : "السابق"
287
- pagination_next : "التالي"
288
- breadcrumb_home_label : "الرئيسية"
289
- breadcrumb_separator : "/"
290
- menu_label : "تبديل القائمة"
291
- search_label : "تبديل البحث"
292
- toc_label : "في هذه الصفحة"
293
- ext_link_label : "رابط مباشر"
294
- less_than : "أقل من"
295
- minute_read : "دقيقة للقراءة"
296
- share_on_label : "مشاركة على"
297
- meta_label :
298
- tags_label : "الوسوم:"
299
- categories_label : "الأقسام:"
300
- date_label : "تاريخ التحديث:"
301
- author_label : "الكاتب:"
302
- comments_label : "اترك تعليقاً"
303
- comments_title : "التعليقات"
304
- more_label : "اقرأ المزيد"
305
- related_label : "قد يعجبك أيضاً"
306
- follow_label : "تابعنا:"
307
- feed_label : "الخلاصة"
308
- powered_by : "يعمل بواسطة"
309
- website_label : "الموقع"
310
- email_label : "البريد الإلكتروني"
311
- recent_posts : "المنشورات الأخيرة"
312
- back_to_top : "العودة للأعلى"
313
- # Statistics labels
314
- stats_total_posts : "إجمالي المنشورات"
315
- stats_total_pages : "إجمالي الصفحات"
316
- stats_categories : "الأقسام"
317
- stats_tags : "الوسوم"
318
- stats_last_updated : "آخر تحديث"
319
- stats_overview : "نظرة عامة"
320
- stats_content_breakdown : "تفصيل المحتوى"
321
- # Theme-specific labels
322
- theme_toggle : "تبديل المظهر"
323
- dark_mode : "الوضع الداكن"
324
- light_mode : "الوضع الفاتح"
325
- view_source : "عرض المصدر"
326
- edit_page : "تحرير هذه الصفحة"
327
- report_issue : "الإبلاغ عن مشكلة"
328
-
329
- ar-SA:
330
- <<: *DEFAULT_AR
331
- ar-EG:
332
- <<: *DEFAULT_AR
data/_includes/README.md CHANGED
@@ -12,6 +12,8 @@ Essential layout components that form the foundation of the site:
12
12
  - `header.html` - Main site header with navigation
13
13
  - `footer.html` - Site footer (if exists)
14
14
  - `branding.html` - Site branding and title display
15
+ - `i18n.html` - Resolves the per-page-language UI-string set into the `ui` variable (generated translations fall back to English)
16
+ - `hreflang.html` - `<link rel="alternate" hreflang>` tags for pages with machine-generated translations
15
17
 
16
18
  ### `navigation/`
17
19
 
@@ -40,6 +42,8 @@ Analytics and tracking integrations:
40
42
  Reusable UI components and widgets:
41
43
 
42
44
  - `searchbar.html` - Deprecated search stub (superseded by `search-modal.html`)
45
+ - `language-toggle.html` - Navbar dropdown switching between the English original and its machine-generated translations
46
+ - `translation-notice.html` - Disclosure banner on machine-translated pages linking back to the English original
43
47
  - `powered-by.html` - "Powered by" credits display
44
48
  - `quick-index.html` - Quick page index
45
49
  - `dev-shortcuts.html` - Developer shortcuts
@@ -91,6 +91,7 @@ If a component needs a token that doesn't exist yet, propose it in `_sass/tokens
91
91
  | `info-section.html` | Settings offcanvas — Appearance / Site / Developer. |
92
92
  | `js-cdn.html` | Body-end script bundle. |
93
93
  | `search-modal.html` | Global search modal. |
94
+ | `page-feedback.html` | "Improve this page" capture widget — request type + live page context + captured console logs → GitHub issue (base prefill + optional AI enrichment). Pairs with `assets/js/page-feedback.js` and `core/console-capture.html`; types from `_data/feedback_types.yml`. |
94
95
 
95
96
  ## Adding a new component
96
97
 
@@ -10,7 +10,9 @@
10
10
  reference an external avatar, e.g. a GitHub one:
11
11
  avatar: "https://avatars.githubusercontent.com/u/10567847?v=4"
12
12
  2. `avatar` is a relative path (e.g. /images/authors/me.png) — prefixed with
13
- the site's assets path ({{ site.baseurl }}/{{ site.public_folder }}).
13
+ the site's public_folder and passed through relative_url, so baseurl is
14
+ applied and the slashes never double into a protocol-relative //… URL
15
+ (even when public_folder and/or baseurl are empty or unset).
14
16
  3. No `avatar`, but a `github` handle is set — falls back to that account's
15
17
  GitHub avatar: https://github.com/<handle>.png
16
18
  4. Otherwise — empty output (the caller renders a fallback icon).
@@ -22,7 +24,7 @@
22
24
  {%- endcomment -%}
23
25
  {%- assign _avatar = include.data.avatar -%}
24
26
  {%- if _avatar and _avatar != "" -%}
25
- {%- if _avatar contains "://" -%}{{ _avatar }}{%- else -%}{{ site.baseurl }}/{{ site.public_folder }}{{ _avatar }}{%- endif -%}
27
+ {%- if _avatar contains "://" -%}{{ _avatar }}{%- else -%}{%- capture _avatar_path -%}/{{ site.public_folder }}{{ _avatar }}{%- endcapture -%}{{ _avatar_path | replace: "//", "/" | relative_url }}{%- endif -%}
26
28
  {%- elsif include.data.github and include.data.github != "" -%}
27
29
  https://github.com/{{ include.data.github }}.png
28
30
  {%- endif -%}
@@ -132,7 +132,8 @@
132
132
  </div>
133
133
  </li>
134
134
 
135
- <!-- Development URL -->
135
+ {% unless is_production %}
136
+ <!-- Development URL (dev builds only — never leak localhost into production HTML) -->
136
137
  <li class="list-group-item d-flex justify-content-between align-items-center gap-2">
137
138
  <div class="d-flex align-items-center min-w-0">
138
139
  <span class="badge bg-warning text-dark me-2">Dev</span>
@@ -162,6 +163,7 @@
162
163
  </button>
163
164
  </div>
164
165
  </li>
166
+ {% endunless %}
165
167
 
166
168
  {% if site.repository %}
167
169
  <!-- GitHub Source -->
@@ -0,0 +1,81 @@
1
+ {% comment %} Feature: ZER0-078 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: language-toggle
4
+ Path: _includes/components/language-toggle.html
5
+ Purpose: Navbar dropdown that switches between the English original and
6
+ its machine-generated translations (scripts/translate.rb).
7
+ Shows the source language plus every configured target
8
+ language; targets without a generated translation for the
9
+ current page render as disabled entries.
10
+ Params: none — reads page.lang / page.url / page.translation_source_url
11
+ and the generated _data/i18n/manifest.yml.
12
+ Depends on: site.translation.* config, site.data.i18n.manifest,
13
+ _data/i18n/languages.yml (display names), core/i18n.html,
14
+ Bootstrap 5 dropdown, Bootstrap Icons (bi-translate)
15
+ Notes: Renders nothing when translation is disabled or no target
16
+ languages are configured. The chosen language is remembered in
17
+ localStorage("zer0-lang") for future UX (no auto-redirect).
18
+ Must NOT be include_cached — the menu is page-dependent.
19
+ {%- endcomment -%}
20
+ {%- assign _lt_langs = site.translation.languages -%}
21
+ {%- if site.translation.enabled and _lt_langs and _lt_langs.size > 0 -%}
22
+ {%- include core/i18n.html -%}
23
+ {%- assign _lt_source = site.translation.source_lang | default: 'en' -%}
24
+ {%- assign _lt_current = page.lang | default: _lt_source -%}
25
+ {%- if _lt_current == _lt_source -%}
26
+ {%- assign _lt_src_url = page.url -%}
27
+ {%- else -%}
28
+ {%- assign _lt_src_url = page.translation_source_url -%}
29
+ {%- endif -%}
30
+ {%- assign _lt_entry = site.data.i18n.manifest.pages[_lt_src_url] -%}
31
+ {%- assign _lt_names = site.data.i18n.languages -%}
32
+ {%- assign _lt_source_name = _lt_names[_lt_source].native | default: _lt_source -%}
33
+ <div class="dropdown nav-lang-toggle" id="zer0-lang-toggle">
34
+ <button
35
+ class="btn nav-lang-button dropdown-toggle"
36
+ type="button"
37
+ id="langToggleButton"
38
+ data-bs-toggle="dropdown"
39
+ aria-expanded="false"
40
+ aria-label="{{ ui.lang_toggle_aria | default: 'Change language' }}"
41
+ title="{{ ui.lang_toggle_label | default: 'Language' }}"
42
+ >
43
+ <i class="{{ site.default_icon }} bi-translate" aria-hidden="true"></i>
44
+ <span class="nav-link-text d-none d-xl-inline ms-1 text-uppercase">{{ _lt_current | slice: 0, 2 }}</span>
45
+ </button>
46
+ <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="langToggleButton">
47
+ <li>
48
+ {%- if _lt_current == _lt_source %}
49
+ <span class="dropdown-item active" aria-current="true">{{ _lt_source_name }}</span>
50
+ {%- else %}
51
+ <a class="dropdown-item" data-lang="{{ _lt_source }}" hreflang="{{ _lt_source }}" href="{{ _lt_src_url | default: '/' | relative_url }}">{{ _lt_source_name }}</a>
52
+ {%- endif %}
53
+ </li>
54
+ {%- for _lt_lang in _lt_langs -%}
55
+ {%- assign _lt_name = _lt_names[_lt_lang].native | default: _lt_lang -%}
56
+ {%- assign _lt_t = _lt_entry[_lt_lang] %}
57
+ <li>
58
+ {%- if _lt_current == _lt_lang %}
59
+ <span class="dropdown-item active" aria-current="true">{{ _lt_name }}</span>
60
+ {%- elsif _lt_t %}
61
+ <a class="dropdown-item" data-lang="{{ _lt_lang }}" hreflang="{{ _lt_lang }}" href="{{ _lt_t.url | relative_url }}">{{ _lt_name }}</a>
62
+ {%- else %}
63
+ <span class="dropdown-item disabled" aria-disabled="true" title="{{ ui.lang_not_available | default: 'Not yet translated' }}">{{ _lt_name }} <small class="text-body-secondary">({{ ui.lang_not_available | default: 'Not yet translated' }})</small></span>
64
+ {%- endif %}
65
+ </li>
66
+ {%- endfor %}
67
+ </ul>
68
+ </div>
69
+ <script>
70
+ // Remember the visitor's language pick (no auto-redirect — just preference).
71
+ (function () {
72
+ var toggle = document.getElementById('zer0-lang-toggle');
73
+ if (!toggle) return;
74
+ toggle.addEventListener('click', function (event) {
75
+ var item = event.target.closest('a.dropdown-item[data-lang]');
76
+ if (!item) return;
77
+ try { localStorage.setItem('zer0-lang', item.getAttribute('data-lang')); } catch (e) { /* private mode */ }
78
+ });
79
+ })();
80
+ </script>
81
+ {%- endif -%}