create-portfoliable 1.0.3

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 (62) hide show
  1. package/bin/create-portfoliable.mjs +425 -0
  2. package/cli/portfoliable.mjs +836 -0
  3. package/index.html +18 -0
  4. package/package.json +102 -0
  5. package/public/favicon.ico +0 -0
  6. package/public/favicon.png +0 -0
  7. package/scripts/check-updates.mjs +31 -0
  8. package/scripts/ensure-valence-index-css.mjs +245 -0
  9. package/scripts/generate-password-hash.mjs +25 -0
  10. package/scripts/release-orchestrator.mjs +363 -0
  11. package/scripts/scaffold-case.mjs +460 -0
  12. package/scripts/scaffold-consumer.mjs +87 -0
  13. package/scripts/smoke-homeview.mjs +133 -0
  14. package/scripts/smoke-initializer.mjs +129 -0
  15. package/scripts/smoke-packed.mjs +184 -0
  16. package/scripts/switch-valence-source.mjs +162 -0
  17. package/scripts/sync-template-shared.mjs +136 -0
  18. package/scripts/terminal-ui.mjs +274 -0
  19. package/scripts/verify-integration.mjs +72 -0
  20. package/src/App.js +10015 -0
  21. package/src/header-visibility.contract.js +31 -0
  22. package/src/header-visibility.contract.test.js +41 -0
  23. package/src/main.js +12 -0
  24. package/src/parser/markdown.js +855 -0
  25. package/src/style.css +88 -0
  26. package/templates/README.md +242 -0
  27. package/templates/configs/i18n/i18n.config.js +230 -0
  28. package/templates/configs/i18n/i18n.labels.js +188 -0
  29. package/templates/configs/portfoliable.a11y.config.js +262 -0
  30. package/templates/configs/portfoliable.design.config.js +651 -0
  31. package/templates/gitignore +9 -0
  32. package/templates/index.html +20 -0
  33. package/templates/public/api/.htaccess +4 -0
  34. package/templates/public/api/README.md +27 -0
  35. package/templates/public/api/password.config.example.json +16 -0
  36. package/templates/public/api/password.config.json +16 -0
  37. package/templates/public/api/unlock-case.php +56 -0
  38. package/templates/public/favicon.ico +0 -0
  39. package/templates/public/favicon.png +0 -0
  40. package/templates/public/portfoliable-logo.svg +3 -0
  41. package/templates/scripts/add-language.mjs +253 -0
  42. package/templates/scripts/convert-video-to-mp4.mjs +207 -0
  43. package/templates/scripts/convert-wav-to-mp3.mjs +184 -0
  44. package/templates/scripts/delete-case.mjs +320 -0
  45. package/templates/scripts/delete-language.mjs +195 -0
  46. package/templates/scripts/generate-password-hash.mjs +98 -0
  47. package/templates/scripts/i18n-config-utils.mjs +123 -0
  48. package/templates/scripts/scaffold-case.mjs +444 -0
  49. package/templates/scripts/sync-locales.mjs +1048 -0
  50. package/templates/scripts/validate-content.mjs +257 -0
  51. package/templates/scripts/validate-protection.mjs +187 -0
  52. package/templates/src/cases/index.js +188 -0
  53. package/templates/src/content/about/ABOUTME.md +202 -0
  54. package/templates/src/content/cases/template-case/case.md +479 -0
  55. package/templates/src/content/cases/template-case/cover.png +0 -0
  56. package/templates/src/content/cases/template-case/he-cover.png +0 -0
  57. package/templates/src/content/cases/template-case/template-audio.mp3 +0 -0
  58. package/templates/src/content/cases/template-case/template-video.mp4 +0 -0
  59. package/templates/src/content/thumbnail-options.generated.json +1044 -0
  60. package/templates/src/main.js +23 -0
  61. package/templates/src/parser/markdown.js +805 -0
  62. package/vite.config.js +111 -0
package/src/style.css ADDED
@@ -0,0 +1,88 @@
1
+ /* File: create-portfoliable/src/style.css */
2
+ /* Purpose: Define global runtime style resets and baseline viewport/body behavior. */
3
+ /* Author: Lio Schimanko */
4
+
5
+ /* MARK: NOTES */
6
+ /* This stylesheet intentionally avoids local @import dependencies. */
7
+
8
+ /* MARK: GLOBAL RESET */
9
+ *, *::before, *::after {
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ /* MARK: ROOT VIEWPORT SURFACE */
14
+ body {
15
+ margin: 0;
16
+ background-color: var(--color-bg, var(--color-player-bg, #101218));
17
+ color: var(--color-black);
18
+ font-family: var(--font-family);
19
+ -webkit-font-smoothing: antialiased;
20
+ overflow: hidden;
21
+ height: 100vh;
22
+ width: 100vw;
23
+ transition: background-color 280ms ease;
24
+ }
25
+
26
+ html {
27
+ background-color: var(--color-bg, var(--color-player-bg, #101218));
28
+ transition: background-color 280ms ease;
29
+ }
30
+
31
+ @media (prefers-reduced-motion: reduce) {
32
+ html,
33
+ body {
34
+ transition: none;
35
+ }
36
+ }
37
+
38
+ /* MARK: A11Y DARK THEME TOKENS */
39
+ /* Mirrors Valence accessibility token roles so dark-mode toggles always affect the full app shell. */
40
+ html.a11y-dark-mode {
41
+ --color-bg: var(--color-player-bg);
42
+ --color-surface: #1C1C1E;
43
+ --color-surface-border: rgba(255, 255, 255, 0.12);
44
+
45
+ --color-black: #FFFFFF;
46
+ --color-text-primary: #FFFFFF;
47
+ --color-text-secondary: #F5F5F5;
48
+ --color-text-muted: #B2B2B2;
49
+
50
+ --color-card-bg: var(--color-surface);
51
+ --color-card-border: var(--color-surface-border);
52
+ --color-kbd-border: rgba(255, 255, 255, 0.2);
53
+ }
54
+
55
+ /* MARK: A11Y HIGH CONTRAST TOKENS */
56
+ /* Forces maximum contrast token values for users who opt into high-contrast mode. */
57
+ html.a11y-high-contrast {
58
+ --color-bg: #000000;
59
+ --color-surface: #000000;
60
+ --color-surface-border: #FFFFFF;
61
+
62
+ --color-black: #FFFFFF;
63
+ --color-text-primary: #FFFFFF;
64
+ --color-text-secondary: #FFFFFF;
65
+ --color-text-muted: #FFFFFF;
66
+
67
+ --color-gray-dark: #FFFFFF;
68
+ --color-gray-med: #FFFFFF;
69
+ --color-gray-light: #FFFFFF;
70
+
71
+ --color-card-bg: #000000;
72
+ --color-card-border: #FFFFFF;
73
+ --color-kbd-border: #FFFFFF;
74
+ }
75
+
76
+ /* MARK: A11Y FORCED COLORS TOKENS */
77
+ /* Maps runtime tokens to system color keywords when forced-colors is active. */
78
+ html.a11y-forced-colors {
79
+ --color-bg: Canvas;
80
+ --color-surface: Canvas;
81
+ --color-text-primary: CanvasText;
82
+ --color-text-secondary: CanvasText;
83
+ --color-text-muted: GrayText;
84
+
85
+ --color-card-bg: Canvas;
86
+ --color-card-border: CanvasText;
87
+ --color-surface-border: CanvasText;
88
+ }
@@ -0,0 +1,242 @@
1
+ # Portfoliable user manual
2
+
3
+ This is the short end-user guide for a project created with:
4
+
5
+ ```bash
6
+ npm create portfoliable@latest
7
+ # or
8
+ npm create portfoliable
9
+ ```
10
+
11
+ Use it after scaffolding to run, edit, validate, and publish your portfolio.
12
+
13
+ For a more detailed user-manual, please visit the official [Portfoliable documentation](https://www.portfoliable.js.org/docs/)
14
+
15
+ ## 1. Create and run a project
16
+
17
+ ```bash
18
+ npm create portfoliable@latest my-portfolio
19
+ # or
20
+ npm create portfoliable my-portfolio
21
+ cd my-portfolio
22
+ npm run portfoliable
23
+ ```
24
+
25
+ The app starts in local development mode and opens the browser by default.
26
+
27
+ ## 2. Main commands
28
+
29
+ ```bash
30
+ npm run portfoliable
31
+ npm run build
32
+ npm run preview
33
+ npm run validate:content
34
+ npm run convert:audio
35
+ npm run convert:video
36
+ npm run portfoliable-create-case -- --name "My First Case"
37
+ npm run add:language -- --code es --name Español --html-lang es-ES
38
+ npm run portfoliable-thumbnail-options
39
+ ```
40
+
41
+ ## 3. Generated project structure
42
+
43
+ A generated project usually includes:
44
+
45
+ - `src/content/cases/` for case markdown files
46
+ - `src/content/about/` for About content
47
+ - `configs/` for runtime and locale configuration
48
+ - `public/` for static assets and backend endpoints
49
+
50
+ ## 4. Create and edit a case
51
+
52
+ ```bash
53
+ npm run portfoliable-create-case -- --name "My Case"
54
+ ```
55
+
56
+ Each case should include:
57
+
58
+ - a stable `id`
59
+ - localized `title` and `shortDesc`
60
+ - valid thumbnail metadata
61
+ - readable localized markdown sections
62
+ - correct share and link metadata when needed
63
+
64
+ Example:
65
+
66
+ ```md
67
+ <!-- lang:en -->
68
+ ## Project context
69
+ This is the English case body.
70
+
71
+ <!-- lang:pt -->
72
+ ## Contexto do projeto
73
+ Este é o corpo do caso em português.
74
+ ```
75
+
76
+ ## 5. Add and remove languages
77
+
78
+ To add a language, you can use the following command:
79
+
80
+ ```bash
81
+ npm run add:language -- --code es --name Español --html-lang es-ES
82
+ ```
83
+
84
+ To add locale-specific languages, you can use `--rtl` or `--ltf`, which will set the `dir` to left-to-right or right-to-left.
85
+
86
+ ```bash
87
+ npm run add:language -- --code ar --name العربية --html-lang ar-SA --rtl
88
+ npm run add:language -- --code it --name Italiano --html-lang it-IT --ltr
89
+ ```
90
+
91
+ To remove a locale, use:
92
+
93
+ ```bash
94
+ npm run delete:language -- --code es
95
+ ```
96
+
97
+ You can also delete a default locale using:
98
+
99
+ ```bash
100
+ npx portfoliable delete-language --code es --force
101
+ ```
102
+
103
+ Then fill in translated metadata and content blocks for the new locale.
104
+
105
+ ## 6. Thumbnail device catalog
106
+
107
+ Inspect supported device combinations:
108
+
109
+ ```bash
110
+ npm run portfoliable-thumbnail-options
111
+ npm run portfoliable-thumbnail-options -- --full
112
+ npm run portfoliable-thumbnail-options -- --json
113
+ ```
114
+
115
+ Use the generated tuple in your case config, for example:
116
+
117
+ ```text
118
+ thumbCategory=mobile thumbBrand=apple thumbModel=Apple iPhone 15 thumbColor=Black
119
+ ```
120
+
121
+ ## 7. Validate before shipping
122
+
123
+ ```bash
124
+ npm run validate:content
125
+ npm run build
126
+ npm run preview
127
+ ```
128
+
129
+ Validate early so missing metadata and broken locale sections are caught before production builds.
130
+
131
+ ## 8. Protected cases
132
+
133
+ If a case needs a secure unlock flow, configure the backend endpoint in your app config and keep the real secret server-side.
134
+
135
+ Do not store passwords directly in markdown files.
136
+
137
+ ### The setup flow
138
+
139
+ 1. Mark the case as protected in its frontmatter:
140
+
141
+ ```js
142
+ {
143
+ id: 'mobile-product-launch',
144
+ isProtected: true
145
+ }
146
+ ```
147
+
148
+ 2. Keep the unlock endpoint in the generated project config:
149
+
150
+ ```js
151
+ export default {
152
+ protection: {
153
+ unlockEndpoint: '/api/unlock-case.php'
154
+ }
155
+ };
156
+ ```
157
+
158
+ 3. Copy the example backend config into your project and add a hash for each protected case:
159
+
160
+ ```bash
161
+ cp public/api/password.config.example.json public/api/password.config.json
162
+ ```
163
+
164
+ Then add a record like:
165
+
166
+ ```json
167
+ {
168
+ "cases": {
169
+ "mobile-product-launch": {
170
+ "hash": "$argon2id$v=19$m=65536,t=4,p=1$...generated-hash..."
171
+ }
172
+ }
173
+ }
174
+ ```
175
+
176
+ 4. Generate a secure password hash:
177
+
178
+ ```bash
179
+ npm run password:hash -- --case-id mobile-product-launch --password "your-secret"
180
+ ```
181
+
182
+ 5. Keep the real file private and protected by the server rules in `public/api/.htaccess`.
183
+
184
+ ### Important protections
185
+
186
+ - `public/api/password.config.json` should stay out of git
187
+ - the app should unlock the case only through the PHP endpoint
188
+ - the unlock response should be treated as non-cacheable
189
+ - frontend markdown is never the place to keep the real secret
190
+
191
+ When a protected case loads, the app prompts for a passcode before revealing the locked content.
192
+
193
+ ## 9. Useful behavior to know
194
+
195
+ - the home view renders case cards from metadata
196
+ - cases support localized routes and localized metadata
197
+ - the app supports visibility rules for web, crawler, and AI exposure
198
+ - the reader can include summaries, audio, table of contents, and navigation controls
199
+
200
+ ## 10. Convert WAV files to MP3
201
+
202
+ If you keep narration or voice assets as WAV while editing, you can convert them to MP3 before shipping:
203
+
204
+ ```bash
205
+ npm run convert:audio
206
+ ```
207
+
208
+ This command scans your project for `.wav` files and writes `.mp3` files in the same folders.
209
+
210
+ Use this optional flag to target one directory:
211
+
212
+ ```bash
213
+ npm run convert:audio -- --dir public/audio
214
+ ```
215
+
216
+ `ffmpeg` is required (on macOS: `brew install ffmpeg`).
217
+
218
+ ## 11. Convert videos to MP4
219
+
220
+ If you keep source footage in mixed formats while editing, you can convert videos to MP4 before shipping:
221
+
222
+ ```bash
223
+ npm run convert:video
224
+ ```
225
+
226
+ This command scans your project for video files (for example `.mov`, `.mkv`, `.avi`, `.webm`) and writes `.mp4` files in the same folders.
227
+
228
+ Use this optional flag to target one directory:
229
+
230
+ ```bash
231
+ npm run convert:video -- --dir public/video
232
+ ```
233
+
234
+ Use `--force` to overwrite existing `.mp4` outputs:
235
+
236
+ ```bash
237
+ npm run convert:video -- --dir public/video --force
238
+ ```
239
+
240
+ `ffmpeg` is required (on macOS: `brew install ffmpeg`).
241
+
242
+ This is the user-facing contract. For maintainer internals, use the repository root README instead.
@@ -0,0 +1,230 @@
1
+ // File: templates/configs/i18n/i18n.config.js
2
+ // Purpose: Provide runtime language configuration, translation resolution, and i18n helpers.
3
+ // Author: Lio Schimanko
4
+
5
+ // MARK: IMPORTS
6
+ // Loads static translation labels and keyboard shortcut bindings used for runtime labels.
7
+ import labelsByLocale from './i18n.labels.js';
8
+ import { getShortcutDisplay, shortcutLabelBindings } from '../portfoliable.a11y.config.js';
9
+
10
+ // MARK: LOCALE SOURCE OF TRUTH
11
+ // Add/remove locales here. Keep keys quoted so Node scripts can parse this object reliably.
12
+ export const LANGUAGE_CONFIG = Object.freeze({
13
+ "en": {
14
+ "name": "English",
15
+ "htmlLang": "en-US"
16
+ },
17
+ "he": {
18
+ "name": "עברית",
19
+ "htmlLang": "he-IL"
20
+ }
21
+ });
22
+
23
+ export const DEFAULT_LOCALE = 'en';
24
+ export const SUPPORTED_LOCALES = Object.freeze(Object.keys(LANGUAGE_CONFIG));
25
+
26
+ // Canonical list of RTL locale codes for direction detection.
27
+ export const RTL_LOCALE_CODES = Object.freeze([
28
+ "arc",
29
+ "az-arab",
30
+ "azb",
31
+ "bal-arab",
32
+ "ckb",
33
+ "dv",
34
+ "fa",
35
+ "ff-adlm",
36
+ "glk",
37
+ "ha-arab",
38
+ "he",
39
+ "khw",
40
+ "ks-arab",
41
+ "ku-arab",
42
+ "lrc",
43
+ "mzn",
44
+ "nqo",
45
+ "pa-arab",
46
+ "pnb",
47
+ "ps",
48
+ "sd",
49
+ "syr",
50
+ "ug",
51
+ "ur",
52
+ "yi"
53
+ ]);
54
+
55
+ // MARK: LOCALE NORMALIZATION
56
+ // Normalizes arbitrary locale input into a lowercased canonical code.
57
+ export function normalizeLocaleCode(value) {
58
+ return String(value || '').trim().toLowerCase();
59
+ }
60
+
61
+ // Resolves a locale to one of SUPPORTED_LOCALES or falls back to DEFAULT_LOCALE.
62
+ export function resolveLocaleCode(value, fallback = DEFAULT_LOCALE) {
63
+ const normalized = normalizeLocaleCode(value);
64
+ if (SUPPORTED_LOCALES.includes(normalized)) {
65
+ return normalized;
66
+ }
67
+ return fallback;
68
+ }
69
+
70
+ // Returns true when a locale or its base language code is RTL.
71
+ export function isRtlLocaleCode(value) {
72
+ const normalized = normalizeLocaleCode(value);
73
+ if (!normalized) return false;
74
+ if (RTL_LOCALE_CODES.includes(normalized)) return true;
75
+ const baseLocale = normalized.split('-')[0];
76
+ return RTL_LOCALE_CODES.includes(baseLocale);
77
+ }
78
+
79
+ // Resolves text direction for a locale with safe fallback behavior.
80
+ export function resolveLocaleDirection(value, fallback = 'ltr') {
81
+ const normalized = normalizeLocaleCode(value);
82
+ if (isRtlLocaleCode(normalized)) {
83
+ return 'rtl';
84
+ }
85
+ if (SUPPORTED_LOCALES.includes(normalized)) {
86
+ return 'ltr';
87
+ }
88
+ return fallback === 'rtl' ? 'rtl' : 'ltr';
89
+ }
90
+
91
+ // MARK: LABEL RESOLUTION
92
+ // Returns a human-readable language name from config, Intl, or locale code fallback.
93
+ export function getLanguageDisplayName(localeCode) {
94
+ const normalized = resolveLocaleCode(localeCode, DEFAULT_LOCALE);
95
+ const configured = LANGUAGE_CONFIG[normalized]?.name;
96
+ if (typeof configured === 'string' && configured.trim().length > 0) {
97
+ return configured.trim();
98
+ }
99
+
100
+ try {
101
+ if (typeof Intl !== 'undefined' && typeof Intl.DisplayNames === 'function') {
102
+ const displayNames = new Intl.DisplayNames([normalized], { type: 'language' });
103
+ const resolved = displayNames.of(normalized);
104
+ if (resolved && resolved !== normalized) {
105
+ return resolved;
106
+ }
107
+ }
108
+ } catch {
109
+ // Uses locale code fallback below.
110
+ }
111
+
112
+ return normalized;
113
+ }
114
+
115
+ // MARK: ACTIVE LOCALE RESOLUTION
116
+ // Resolves current locale from path segment, then query param, then default.
117
+ function resolveActiveLanguage() {
118
+ const pathSegments = String(window.location.pathname || '')
119
+ .split('/')
120
+ .filter(Boolean)
121
+ .map((segment) => normalizeLocaleCode(segment));
122
+ const pathLocale = pathSegments[0] || '';
123
+ if (SUPPORTED_LOCALES.includes(pathLocale)) {
124
+ return pathLocale;
125
+ }
126
+
127
+ const urlParams = new URLSearchParams(window.location.search);
128
+ const urlLang = normalizeLocaleCode(urlParams.get('lang'));
129
+ if (SUPPORTED_LOCALES.includes(urlLang)) {
130
+ return urlLang;
131
+ }
132
+
133
+ return DEFAULT_LOCALE;
134
+ }
135
+
136
+ // MARK: SHORTCUT MARKUP HELPERS
137
+ // Appends kbd markup to a base label when a shortcut is available.
138
+ function appendShortcutLabel(baseLabel, shortcutValue, kbdClass = '') {
139
+ if (!baseLabel || !shortcutValue) return baseLabel;
140
+ const classMarkup = kbdClass ? ` class="${kbdClass}"` : '';
141
+ return `${baseLabel} <kbd${classMarkup}>${shortcutValue}</kbd>`;
142
+ }
143
+
144
+ // Keys that remain plain text without runtime kbd markup.
145
+ const SKIP_SHORTCUT_MARKUP_KEYS = new Set([
146
+ 'btn_return',
147
+ 'a11y_size',
148
+ 'a11y_dark',
149
+ 'a11y_contrast',
150
+ 'a11y_motion',
151
+ 'a11y_tab',
152
+ 'a11y_dyslexia'
153
+ ]);
154
+
155
+ // MARK: TRANSLATION MAP BOOTSTRAP
156
+ // Builds localized runtime labels by merging default-locale labels with per-locale overrides.
157
+ function buildRuntimeTranslations() {
158
+ const baseTranslations = labelsByLocale && typeof labelsByLocale === 'object'
159
+ ? labelsByLocale
160
+ : {};
161
+ const runtime = {};
162
+
163
+ SUPPORTED_LOCALES.forEach((localeCode) => {
164
+ runtime[localeCode] = {
165
+ ...(baseTranslations[DEFAULT_LOCALE] || {}),
166
+ ...(baseTranslations[localeCode] || {})
167
+ };
168
+
169
+ // Always expose explicit localized language labels for contextual menu rendering.
170
+ runtime[localeCode][`lang_${localeCode}`] = getLanguageDisplayName(localeCode);
171
+ });
172
+
173
+ Object.entries(shortcutLabelBindings).forEach(([translationKey, binding]) => {
174
+ if (SKIP_SHORTCUT_MARKUP_KEYS.has(translationKey)) return;
175
+
176
+ const shortcutValue = getShortcutDisplay(binding.shortcutId);
177
+ if (!shortcutValue) return;
178
+
179
+ SUPPORTED_LOCALES.forEach((localeCode) => {
180
+ const baseLabel = runtime[localeCode]?.[translationKey];
181
+ runtime[localeCode][translationKey] = appendShortcutLabel(baseLabel, shortcutValue, binding.kbdClass || '');
182
+ });
183
+ });
184
+
185
+ return runtime;
186
+ }
187
+
188
+ // Builds the immutable runtime translation cache once at module load.
189
+ const runtimeTranslations = buildRuntimeTranslations();
190
+
191
+ // MARK: DOCUMENT LANGUAGE BOOTSTRAP
192
+ // Initializes the active locale and applies lang/dir metadata to the document element.
193
+ window.currentLang = resolveActiveLanguage();
194
+ const htmlLangTag = LANGUAGE_CONFIG[window.currentLang]?.htmlLang || window.currentLang;
195
+ document.documentElement.lang = htmlLangTag;
196
+ document.documentElement.dir = resolveLocaleDirection(window.currentLang, 'ltr');
197
+
198
+ // MARK: PUBLIC TRANSLATION API
199
+ // Exposes runtime translation maps and keys requiring plain text insertion.
200
+ export const translations = runtimeTranslations;
201
+ const PLAIN_TEXT_I18N_KEYS = new Set(['h1_title']);
202
+
203
+ // Returns translated value for the active locale with fallback to default locale and key.
204
+ export function t(key) {
205
+ const activeLocale = resolveLocaleCode(window.currentLang, DEFAULT_LOCALE);
206
+ return translations[activeLocale]?.[key] || translations[DEFAULT_LOCALE]?.[key] || key;
207
+ }
208
+
209
+ // MARK: DOM TRANSLATION APPLIER
210
+ // Applies translations to data-i18n nodes.
211
+ export function applyTranslations() {
212
+ // Applies runtime labels to any document node annotated with data-i18n.
213
+ document.querySelectorAll('[data-i18n]').forEach((el) => {
214
+ const key = el.getAttribute('data-i18n');
215
+ if (!key) return;
216
+
217
+ const activeLocale = resolveLocaleCode(window.currentLang, DEFAULT_LOCALE);
218
+ if (translations[activeLocale] && translations[activeLocale][key]) {
219
+ const localizedValue = translations[activeLocale][key];
220
+ if (PLAIN_TEXT_I18N_KEYS.has(key)) {
221
+ el.textContent = localizedValue;
222
+ } else {
223
+ el.innerHTML = localizedValue;
224
+ }
225
+ }
226
+ });
227
+ }
228
+
229
+ // Runs one translation pass once the initial document is ready.
230
+ document.addEventListener('DOMContentLoaded', applyTranslations);