datalog-theme 0.7.0 → 0.9.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 (203) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +188 -0
  3. data/CITATION.cff +2 -2
  4. data/README.md +25 -17
  5. data/_data/cdn-integrity.yml +0 -30
  6. data/_data/i18n/en.yml +297 -0
  7. data/_data/i18n/es.yml +297 -0
  8. data/_data/i18n/pt.yml +297 -0
  9. data/_data/js_manifest.json +16 -0
  10. data/_includes/analytics/dashboard.html +3 -1
  11. data/_includes/components/api-function.html +20 -1
  12. data/_includes/components/author-bio.html +21 -11
  13. data/_includes/components/author-list.html +32 -0
  14. data/_includes/components/citation-tools.html +33 -25
  15. data/_includes/components/comments-thread.html +90 -0
  16. data/_includes/components/contact-form.html +120 -0
  17. data/_includes/components/correction-report.html +82 -0
  18. data/_includes/components/enhanced-code-block.html +1 -1
  19. data/_includes/components/enhanced-toc.html +6 -7
  20. data/_includes/components/license-link.html +13 -0
  21. data/_includes/components/license-notice.html +28 -0
  22. data/_includes/components/moderation-inbox.html +139 -0
  23. data/_includes/components/reactions.html +49 -0
  24. data/_includes/components/reading-list.html +31 -0
  25. data/_includes/components/reading-mode-toggle.html +65 -0
  26. data/_includes/components/reading-state-bookmark.html +27 -0
  27. data/_includes/components/reading-state-panel.html +71 -0
  28. data/_includes/components/reproducibility.html +61 -0
  29. data/_includes/components/responsive-image.html +3 -3
  30. data/_includes/components/revision-history.html +39 -0
  31. data/_includes/components/revision-notice.html +35 -0
  32. data/_includes/components/series-nav.html +64 -0
  33. data/_includes/components/subscribe-form.html +88 -0
  34. data/_includes/components/subscription-manage.html +68 -0
  35. data/_includes/components/webmentions.html +48 -0
  36. data/_includes/csp-meta.html +135 -11
  37. data/_includes/footer.html +22 -20
  38. data/_includes/head.html +111 -63
  39. data/_includes/header/navigation.html +12 -15
  40. data/_includes/header.html +28 -19
  41. data/_includes/layouts/default/article.html +9 -7
  42. data/_includes/meta/dynamic-services-config.html +14 -0
  43. data/_includes/meta/math-config.html +18 -10
  44. data/_includes/meta/person-json.html +27 -0
  45. data/_includes/meta/publisher.html +45 -0
  46. data/_includes/meta/schema.html +67 -30
  47. data/_includes/meta/scholarly.html +121 -0
  48. data/_includes/meta/scripts-loader.html +18 -32
  49. data/_includes/meta/webmention-discovery.html +14 -0
  50. data/_includes/post/related-posts.html +4 -7
  51. data/_includes/scripts.html +57 -0
  52. data/_includes/search/index-data.json +9 -34
  53. data/_layouts/dataset.html +5 -3
  54. data/_layouts/default.html +16 -8
  55. data/_layouts/notebook.html +1 -0
  56. data/_layouts/package.html +4 -2
  57. data/_layouts/page.html +15 -0
  58. data/_layouts/portfolio.html +1 -0
  59. data/_layouts/post.html +102 -23
  60. data/_layouts/project.html +4 -3
  61. data/_layouts/research.html +20 -8
  62. data/_plugins/analytics_dashboard.rb +9 -3
  63. data/_plugins/authors.rb +133 -0
  64. data/_plugins/config_validator.rb +231 -23
  65. data/_plugins/critical_css_check.rb +42 -0
  66. data/_plugins/csp_generator.rb +18 -28
  67. data/_plugins/datalog_bibliography.rb +9 -7
  68. data/_plugins/datalog_comments.rb +8 -5
  69. data/_plugins/datalog_slides.rb +9 -8
  70. data/_plugins/i18n.rb +13 -12
  71. data/_plugins/image_optimizer.rb +241 -178
  72. data/_plugins/licenses.rb +135 -0
  73. data/_plugins/math_preprocessor.rb +59 -7
  74. data/_plugins/notebook_converter.rb +23 -4
  75. data/_plugins/plugin_loader.rb +3 -1
  76. data/_plugins/publications_generator.rb +8 -2
  77. data/_plugins/references.rb +238 -0
  78. data/_plugins/reproducibility.rb +150 -0
  79. data/_plugins/revisions.rb +101 -0
  80. data/_plugins/rouge_highlight_filter.rb +42 -0
  81. data/_plugins/scholarly.rb +50 -0
  82. data/_plugins/search_code_blocks.rb +30 -0
  83. data/_plugins/search_normalizer.rb +15 -53
  84. data/_plugins/search_pages.rb +3 -4
  85. data/_plugins/series.rb +104 -0
  86. data/_plugins/statements.rb +87 -0
  87. data/_sass/_academic-dashboard.scss +262 -0
  88. data/_sass/_base.scss +20 -1
  89. data/_sass/_comments-thread.scss +159 -0
  90. data/_sass/_components.scss +64 -1153
  91. data/_sass/_features.scss +17 -0
  92. data/_sass/_layout.scss +385 -1
  93. data/_sass/_mathematical.scss +27 -0
  94. data/_sass/_moderation.scss +222 -0
  95. data/_sass/_notebooks.scss +322 -0
  96. data/_sass/_open-science-badges.scss +56 -0
  97. data/_sass/{_phase1-enhancements.scss → _post-components.scss} +5 -3
  98. data/_sass/_print.scss +291 -0
  99. data/_sass/_reactions.scss +89 -0
  100. data/_sass/_reading-state.scss +290 -0
  101. data/_sass/_search-page.scss +530 -0
  102. data/_sass/_search.scss +46 -0
  103. data/_sass/_service-forms.scss +204 -0
  104. data/_sass/_subscriptions.scss +140 -0
  105. data/_sass/_syntax-highlighting.scss +212 -97
  106. data/_sass/_theme.scss +40 -19
  107. data/_sass/_typography.scss +130 -0
  108. data/_sass/_utilities.scss +5 -0
  109. data/_sass/_variables.scss +6 -0
  110. data/_sass/_webmentions.scss +125 -0
  111. data/assets/css/main.scss +14 -0
  112. data/assets/js/dist/academic.js +1 -1
  113. data/assets/js/dist/analytics-dashboard.js +1 -1
  114. data/assets/js/dist/chunks/chunk-2DYDWUFX.js +1 -0
  115. data/assets/js/dist/chunks/chunk-PATLC23F.js +1 -0
  116. data/assets/js/dist/chunks/chunk-V7734B2G.js +1 -0
  117. data/assets/js/dist/comments.js +2 -0
  118. data/assets/js/dist/contact.js +1 -0
  119. data/assets/js/dist/core.js +1 -1
  120. data/assets/js/dist/corrections.js +1 -0
  121. data/assets/js/dist/loader.js +1 -1
  122. data/assets/js/dist/math.js +1 -1
  123. data/assets/js/dist/moderation.js +1 -0
  124. data/assets/js/dist/notebook.js +1 -1
  125. data/assets/js/dist/reactions.js +1 -0
  126. data/assets/js/dist/reading-state.js +1 -0
  127. data/assets/js/dist/search.js +1 -1
  128. data/assets/js/dist/sources.json +52 -0
  129. data/assets/js/dist/subscriptions.js +1 -0
  130. data/assets/js/dist/visualizations.js +11 -2
  131. data/assets/js/dist/webmentions.js +1 -0
  132. data/assets/js/loader.js +37 -1
  133. data/datalog-theme.gemspec +35 -23
  134. data/lib/datalog/cli.rb +43 -15
  135. data/lib/datalog/critical_css.rb +168 -0
  136. data/lib/datalog/plugin_system/dependency_resolver.rb +0 -2
  137. data/lib/datalog/plugins/comments.rb +33 -3
  138. data/lib/datalog/theme/installed_files.rb +113 -0
  139. data/lib/datalog/theme/package.rb +57 -0
  140. data/lib/datalog/theme/repository_checkout.rb +94 -0
  141. data/lib/datalog/theme/version.rb +5 -1
  142. data/lib/datalog/warning_filter.rb +5 -11
  143. data/lib/datalog-theme.rb +6 -0
  144. metadata +96 -147
  145. data/_data/academic.yml +0 -217
  146. data/_data/config/author.yml +0 -121
  147. data/_data/datasets.yml +0 -28
  148. data/_data/js_meta.json +0 -371
  149. data/_data/navigation.yml +0 -145
  150. data/_data/projects.yml +0 -41
  151. data/_data/publications.yml +0 -28
  152. data/_data/social.yml +0 -73
  153. data/_data/visualizations.yml +0 -51
  154. data/_includes/components/advanced-search.html +0 -682
  155. data/_includes/components/bookmark-system.html +0 -96
  156. data/_includes/components/comments.html +0 -244
  157. data/_includes/components/content-recommendations.html +0 -228
  158. data/_includes/components/email-preferences.html +0 -200
  159. data/_includes/components/enhanced-metadata.html +0 -228
  160. data/_includes/components/language-switcher.html +0 -396
  161. data/_includes/components/navigation-enhancements.html +0 -454
  162. data/_includes/components/newsletter-signup.html +0 -178
  163. data/_includes/components/popular-posts.html +0 -233
  164. data/_includes/components/reading-progress.html +0 -133
  165. data/_includes/components/reading-time.html +0 -121
  166. data/_includes/components/series-navigation.html +0 -124
  167. data/_includes/components/social-proof.html +0 -34
  168. data/_includes/components/user-preferences.html +0 -566
  169. data/_includes/meta/syntax-config.html +0 -19
  170. data/_layouts/archive.html +0 -282
  171. data/_layouts/post-sidebar.html +0 -183
  172. data/_sass/_phase3-enhancements.scss +0 -874
  173. data/_sass/_phase4-enhancements.scss +0 -1214
  174. data/_sass/_phase5-enhancements.scss +0 -414
  175. data/assets/js/academic.js +0 -262
  176. data/assets/js/analytics-dashboard.js +0 -382
  177. data/assets/js/core/dark-mode.js +0 -79
  178. data/assets/js/core/github-cards.js +0 -123
  179. data/assets/js/core/language-filter.js +0 -69
  180. data/assets/js/core/navigation.js +0 -184
  181. data/assets/js/core/scroll-progress.js +0 -45
  182. data/assets/js/core/search-hotkeys.js +0 -62
  183. data/assets/js/core/skip-links.js +0 -62
  184. data/assets/js/dist/manifest.json +0 -22
  185. data/assets/js/dist/meta.json +0 -371
  186. data/assets/js/main.js +0 -23
  187. data/assets/js/math.js +0 -818
  188. data/assets/js/notebook.js +0 -158
  189. data/assets/js/search/analytics.js +0 -91
  190. data/assets/js/search/app.js +0 -271
  191. data/assets/js/search/autocomplete.js +0 -120
  192. data/assets/js/search/engine.js +0 -260
  193. data/assets/js/search/filters.js +0 -45
  194. data/assets/js/search/render.js +0 -217
  195. data/assets/js/search/utils.js +0 -99
  196. data/assets/js/search.js +0 -354
  197. data/assets/js/visualizations.js +0 -816
  198. data/assets/publications/datalog-publications.bib +0 -8
  199. data/assets/publications/datalog-publications.ris +0 -9
  200. data/assets/publications/publications.bib +0 -30
  201. data/assets/templates/diogo-ribeiro-cv.md +0 -31
  202. data/assets/templates/diogo-ribeiro-cv.tex +0 -32
  203. data/lib/datalog/theme/theme.rb +0 -18
@@ -1,396 +0,0 @@
1
- {%- comment -%}
2
- Multi-language Support (i18n) Component
3
- Provides language switching and translated content
4
-
5
- Parameters:
6
- - show_language_switcher: Show language dropdown (default: true)
7
- - show_flags: Show country flags (default: true)
8
- - position: Position (header, footer, floating) (default: header)
9
- - available_languages: Languages to show (default: from config)
10
- {%- endcomment -%}
11
-
12
- {%- assign show_language_switcher = include.show_language_switcher | default: true -%}
13
- {%- assign show_flags = include.show_flags | default: true -%}
14
- {%- assign position = include.position | default: "header" -%}
15
-
16
- {%- comment -%} Get available languages from config or use defaults {%- endcomment -%}
17
- {%- if site.phase5_features.i18n.languages -%}
18
- {%- assign available_languages = site.phase5_features.i18n.languages -%}
19
- {%- else -%}
20
- {%- assign available_languages = "en,es,fr,de,pt" | split: "," -%}
21
- {%- endif -%}
22
-
23
- {%- comment -%} Detect current language from page or site config {%- endcomment -%}
24
- {%- assign current_lang = page.lang | default: site.lang | default: "en" -%}
25
-
26
- {%- comment -%} Language names and flags {%- endcomment -%}
27
- {%- assign lang_names = "English,Español,Français,Deutsch,Português" | split: "," -%}
28
- {%- assign lang_codes = "en,es,fr,de,pt" | split: "," -%}
29
- {%- assign lang_flags = "🇺🇸,🇪🇸,🇫🇷,🇩🇪,🇵🇹" | split: "," -%}
30
-
31
- {%- if show_language_switcher -%}
32
- <div class="language-switcher language-switcher--{{ position }}"
33
- data-language-switcher
34
- role="navigation"
35
- aria-label="Language selection">
36
-
37
- <button type="button"
38
- class="language-switcher__trigger"
39
- data-lang-trigger
40
- aria-haspopup="true"
41
- aria-expanded="false"
42
- aria-controls="language-menu">
43
-
44
- {%- comment -%} Current language display {%- endcomment -%}
45
- {%- for code in lang_codes -%}
46
- {%- if code == current_lang -%}
47
- {%- if show_flags -%}
48
- <span class="language-switcher__flag" aria-hidden="true">
49
- {{ lang_flags[forloop.index0] }}
50
- </span>
51
- {%- endif -%}
52
- <span class="language-switcher__name">
53
- {{ lang_names[forloop.index0] }}
54
- </span>
55
- <svg class="language-switcher__icon"
56
- xmlns="http://www.w3.org/2000/svg"
57
- width="16"
58
- height="16"
59
- viewBox="0 0 24 24"
60
- fill="none"
61
- stroke="currentColor"
62
- stroke-width="2"
63
- aria-hidden="true">
64
- <polyline points="6 9 12 15 18 9"></polyline>
65
- </svg>
66
- {%- endif -%}
67
- {%- endfor -%}
68
- </button>
69
-
70
- <ul class="language-switcher__menu"
71
- id="language-menu"
72
- data-lang-menu
73
- role="menu"
74
- hidden>
75
-
76
- {%- for code in available_languages -%}
77
- {%- assign lang_index = nil -%}
78
- {%- for check_code in lang_codes -%}
79
- {%- if check_code == code -%}
80
- {%- assign lang_index = forloop.index0 -%}
81
- {%- break -%}
82
- {%- endif -%}
83
- {%- endfor -%}
84
-
85
- {%- if lang_index -%}
86
- {%- assign is_current = false -%}
87
- {%- if code == current_lang -%}
88
- {%- assign is_current = true -%}
89
- {%- endif -%}
90
-
91
- <li class="language-switcher__item"
92
- role="none">
93
- <button type="button"
94
- class="language-switcher__option{% if is_current %} language-switcher__option--active{% endif %}"
95
- data-lang="{{ code }}"
96
- role="menuitem"
97
- {% if is_current %}aria-current="true"{% endif %}
98
- lang="{{ code }}">
99
- {%- if show_flags -%}
100
- <span class="language-switcher__flag" aria-hidden="true">
101
- {{ lang_flags[lang_index] }}
102
- </span>
103
- {%- endif -%}
104
- <span class="language-switcher__label">
105
- {{ lang_names[lang_index] }}
106
- </span>
107
- {%- if is_current -%}
108
- <svg class="language-switcher__check"
109
- xmlns="http://www.w3.org/2000/svg"
110
- width="16"
111
- height="16"
112
- viewBox="0 0 24 24"
113
- fill="none"
114
- stroke="currentColor"
115
- stroke-width="2"
116
- aria-hidden="true">
117
- <polyline points="20 6 9 17 4 12"></polyline>
118
- </svg>
119
- {%- endif -%}
120
- </button>
121
- </li>
122
- {%- endif -%}
123
- {%- endfor -%}
124
- </ul>
125
- </div>
126
-
127
- {%- comment -%} Translation strings for common UI elements {%- endcomment -%}
128
- <script type="application/json" data-i18n-strings>
129
- {
130
- "en": {
131
- "readMore": "Read more",
132
- "readLess": "Read less",
133
- "share": "Share",
134
- "bookmark": "Bookmark",
135
- "unbookmark": "Remove bookmark",
136
- "comments": "Comments",
137
- "noComments": "No comments yet",
138
- "loading": "Loading...",
139
- "error": "An error occurred",
140
- "publishedOn": "Published on",
141
- "updatedOn": "Updated on",
142
- "by": "by",
143
- "in": "in",
144
- "minutes": "minutes",
145
- "minRead": "min read",
146
- "relatedPosts": "Related Posts",
147
- "popularPosts": "Popular Posts",
148
- "recentPosts": "Recent Posts",
149
- "categories": "Categories",
150
- "tags": "Tags",
151
- "searchPlaceholder": "Search posts...",
152
- "noResults": "No results found",
153
- "showingResults": "Showing {{count}} results"
154
- },
155
- "es": {
156
- "readMore": "Leer más",
157
- "readLess": "Leer menos",
158
- "share": "Compartir",
159
- "bookmark": "Guardar",
160
- "unbookmark": "Eliminar marcador",
161
- "comments": "Comentarios",
162
- "noComments": "Aún no hay comentarios",
163
- "loading": "Cargando...",
164
- "error": "Ocurrió un error",
165
- "publishedOn": "Publicado el",
166
- "updatedOn": "Actualizado el",
167
- "by": "por",
168
- "in": "en",
169
- "minutes": "minutos",
170
- "minRead": "min de lectura",
171
- "relatedPosts": "Artículos Relacionados",
172
- "popularPosts": "Artículos Populares",
173
- "recentPosts": "Artículos Recientes",
174
- "categories": "Categorías",
175
- "tags": "Etiquetas",
176
- "searchPlaceholder": "Buscar artículos...",
177
- "noResults": "No se encontraron resultados",
178
- "showingResults": "Mostrando {{count}} resultados"
179
- },
180
- "fr": {
181
- "readMore": "Lire la suite",
182
- "readLess": "Lire moins",
183
- "share": "Partager",
184
- "bookmark": "Enregistrer",
185
- "unbookmark": "Supprimer le signet",
186
- "comments": "Commentaires",
187
- "noComments": "Pas encore de commentaires",
188
- "loading": "Chargement...",
189
- "error": "Une erreur s'est produite",
190
- "publishedOn": "Publié le",
191
- "updatedOn": "Mis à jour le",
192
- "by": "par",
193
- "in": "dans",
194
- "minutes": "minutes",
195
- "minRead": "min de lecture",
196
- "relatedPosts": "Articles Connexes",
197
- "popularPosts": "Articles Populaires",
198
- "recentPosts": "Articles Récents",
199
- "categories": "Catégories",
200
- "tags": "Étiquettes",
201
- "searchPlaceholder": "Rechercher des articles...",
202
- "noResults": "Aucun résultat trouvé",
203
- "showingResults": "Affichage de {{count}} résultats"
204
- },
205
- "de": {
206
- "readMore": "Weiterlesen",
207
- "readLess": "Weniger lesen",
208
- "share": "Teilen",
209
- "bookmark": "Lesezeichen",
210
- "unbookmark": "Lesezeichen entfernen",
211
- "comments": "Kommentare",
212
- "noComments": "Noch keine Kommentare",
213
- "loading": "Wird geladen...",
214
- "error": "Ein Fehler ist aufgetreten",
215
- "publishedOn": "Veröffentlicht am",
216
- "updatedOn": "Aktualisiert am",
217
- "by": "von",
218
- "in": "in",
219
- "minutes": "Minuten",
220
- "minRead": "Min. Lesezeit",
221
- "relatedPosts": "Verwandte Beiträge",
222
- "popularPosts": "Beliebte Beiträge",
223
- "recentPosts": "Neueste Beiträge",
224
- "categories": "Kategorien",
225
- "tags": "Schlagwörter",
226
- "searchPlaceholder": "Beiträge durchsuchen...",
227
- "noResults": "Keine Ergebnisse gefunden",
228
- "showingResults": "Zeige {{count}} Ergebnisse"
229
- },
230
- "pt": {
231
- "readMore": "Ler mais",
232
- "readLess": "Ler menos",
233
- "share": "Compartilhar",
234
- "bookmark": "Salvar",
235
- "unbookmark": "Remover marcador",
236
- "comments": "Comentários",
237
- "noComments": "Ainda sem comentários",
238
- "loading": "Carregando...",
239
- "error": "Ocorreu um erro",
240
- "publishedOn": "Publicado em",
241
- "updatedOn": "Atualizado em",
242
- "by": "por",
243
- "in": "em",
244
- "minutes": "minutos",
245
- "minRead": "min de leitura",
246
- "relatedPosts": "Artigos Relacionados",
247
- "popularPosts": "Artigos Populares",
248
- "recentPosts": "Artigos Recentes",
249
- "categories": "Categorias",
250
- "tags": "Tags",
251
- "searchPlaceholder": "Pesquisar artigos...",
252
- "noResults": "Nenhum resultado encontrado",
253
- "showingResults": "Mostrando {{count}} resultados"
254
- }
255
- }
256
- </script>
257
-
258
- <script>
259
- (function() {
260
- 'use strict';
261
-
262
- const STORAGE_KEY = 'preferredLanguage';
263
-
264
- class LanguageSwitcher {
265
- constructor() {
266
- this.currentLang = '{{ current_lang }}';
267
- this.translations = {};
268
- this.init();
269
- }
270
-
271
- init() {
272
- // Load translation strings
273
- const stringsEl = document.querySelector('[data-i18n-strings]');
274
- if (stringsEl) {
275
- try {
276
- this.translations = JSON.parse(stringsEl.textContent);
277
- } catch (e) {
278
- console.error('Failed to parse translation strings:', e);
279
- }
280
- }
281
-
282
- // Restore preferred language
283
- const savedLang = this.getPreferredLanguage();
284
- if (savedLang && savedLang !== this.currentLang) {
285
- this.switchLanguage(savedLang);
286
- }
287
-
288
- this.attachEventListeners();
289
- }
290
-
291
- attachEventListeners() {
292
- const trigger = document.querySelector('[data-lang-trigger]');
293
- const menu = document.querySelector('[data-lang-menu]');
294
- const options = document.querySelectorAll('[data-lang]');
295
-
296
- if (!trigger || !menu) return;
297
-
298
- // Toggle menu
299
- trigger.addEventListener('click', () => {
300
- const isHidden = menu.hidden;
301
- menu.hidden = !isHidden;
302
- trigger.setAttribute('aria-expanded', isHidden);
303
- });
304
-
305
- // Close on outside click
306
- document.addEventListener('click', (e) => {
307
- if (!e.target.closest('[data-language-switcher]')) {
308
- menu.hidden = true;
309
- trigger.setAttribute('aria-expanded', 'false');
310
- }
311
- });
312
-
313
- // Language selection
314
- options.forEach(option => {
315
- option.addEventListener('click', () => {
316
- const lang = option.dataset.lang;
317
- this.switchLanguage(lang);
318
- menu.hidden = true;
319
- trigger.setAttribute('aria-expanded', 'false');
320
- });
321
- });
322
-
323
- // Keyboard navigation
324
- menu.addEventListener('keydown', (e) => {
325
- if (e.key === 'Escape') {
326
- menu.hidden = true;
327
- trigger.setAttribute('aria-expanded', 'false');
328
- trigger.focus();
329
- }
330
- });
331
- }
332
-
333
- switchLanguage(lang) {
334
- this.currentLang = lang;
335
- this.savePreferredLanguage(lang);
336
-
337
- // Update document language
338
- document.documentElement.lang = lang;
339
-
340
- // Reload page with language parameter
341
- const url = new URL(window.location.href);
342
- url.searchParams.set('lang', lang);
343
-
344
- // Store scroll position
345
- sessionStorage.setItem('scrollPosition', window.pageYOffset);
346
-
347
- window.location.href = url.toString();
348
- }
349
-
350
- getPreferredLanguage() {
351
- try {
352
- return localStorage.getItem(STORAGE_KEY);
353
- } catch (e) {
354
- return null;
355
- }
356
- }
357
-
358
- savePreferredLanguage(lang) {
359
- try {
360
- localStorage.setItem(STORAGE_KEY, lang);
361
- } catch (e) {
362
- console.error('Failed to save language preference:', e);
363
- }
364
- }
365
-
366
- // Translate a key
367
- t(key, replacements = {}) {
368
- let text = this.translations[this.currentLang]?.[key] || key;
369
-
370
- // Replace placeholders
371
- Object.keys(replacements).forEach(placeholder => {
372
- text = text.replace(`{{${placeholder}}}`, replacements[placeholder]);
373
- });
374
-
375
- return text;
376
- }
377
- }
378
-
379
- // Initialize
380
- const languageSwitcher = new LanguageSwitcher();
381
-
382
- // Make available globally
383
- window.i18n = {
384
- t: (key, replacements) => languageSwitcher.t(key, replacements),
385
- currentLang: () => languageSwitcher.currentLang
386
- };
387
-
388
- // Restore scroll position
389
- const scrollPos = sessionStorage.getItem('scrollPosition');
390
- if (scrollPos) {
391
- window.scrollTo(0, parseInt(scrollPos, 10));
392
- sessionStorage.removeItem('scrollPosition');
393
- }
394
- })();
395
- </script>
396
- {%- endif -%}