@authrim/setup 0.1.84 → 0.1.89

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.
package/dist/web/ui.js CHANGED
@@ -11,6 +11,10 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
11
11
  // Safely stringify translations for embedding in JavaScript
12
12
  const translationsJson = JSON.stringify(translations);
13
13
  const availableLocalesJson = JSON.stringify(availableLocales);
14
+ // Generate locale options HTML server-side
15
+ const localeOptionsHtml = availableLocales
16
+ .map((l) => `<option value="${l.code}"${l.code === locale ? ' selected' : ''}>${l.nativeName}</option>`)
17
+ .join('');
14
18
  return `<!DOCTYPE html>
15
19
  <html lang="${locale}">
16
20
  <head>
@@ -217,28 +221,42 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
217
221
  /* ========================================
218
222
  THEME TOGGLE
219
223
  ======================================== */
220
- /* Language Selector */
221
- .lang-selector {
224
+ /* Top Controls Container - holds language selector and theme toggle */
225
+ .top-controls {
222
226
  position: fixed;
223
227
  top: 1.25rem;
224
- right: 5rem;
228
+ right: 1.5rem;
225
229
  z-index: 100;
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 0.75rem;
226
233
  }
227
234
 
235
+ /* Language Selector - styled to match theme toggle */
228
236
  .lang-selector select {
229
- padding: 0.5rem 0.75rem;
237
+ height: 44px;
238
+ padding: 0 2.25rem 0 1rem;
230
239
  background: var(--card-bg);
231
240
  border: 1px solid var(--border);
232
- border-radius: 8px;
241
+ border-radius: 12px;
233
242
  color: var(--text);
234
243
  font-family: var(--font-sans);
235
244
  font-size: 0.875rem;
245
+ font-weight: 500;
236
246
  cursor: pointer;
237
- transition: border-color var(--transition-fast);
247
+ transition: all var(--transition-fast);
248
+ appearance: none;
249
+ -webkit-appearance: none;
250
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
251
+ background-repeat: no-repeat;
252
+ background-position: right 0.75rem center;
253
+ box-shadow: var(--shadow-sm);
238
254
  }
239
255
 
240
256
  .lang-selector select:hover {
257
+ background-color: var(--card-bg-hover);
241
258
  border-color: var(--primary);
259
+ transform: scale(1.02);
242
260
  }
243
261
 
244
262
  .lang-selector select:focus {
@@ -247,11 +265,11 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
247
265
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
248
266
  }
249
267
 
268
+ .lang-selector select:active {
269
+ transform: scale(0.98);
270
+ }
271
+
250
272
  .theme-toggle {
251
- position: fixed;
252
- top: 1.25rem;
253
- right: 1.5rem;
254
- z-index: 100;
255
273
  width: 44px;
256
274
  height: 44px;
257
275
  background: var(--card-bg);
@@ -1683,9 +1701,9 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1683
1701
  </style>
1684
1702
  <script>
1685
1703
  // i18n Translation System
1686
- const _translations = ${translationsJson};
1704
+ let _translations = ${translationsJson};
1687
1705
  const _availableLocales = ${availableLocalesJson};
1688
- const _currentLocale = '${locale}';
1706
+ let _currentLocale = '${locale}';
1689
1707
 
1690
1708
  /**
1691
1709
  * Translate a key with optional parameter substitution
@@ -1704,25 +1722,72 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1704
1722
  }
1705
1723
 
1706
1724
  /**
1707
- * Change the current language
1725
+ * Update all elements with data-i18n attribute
1726
+ */
1727
+ function updateAllTranslations() {
1728
+ document.querySelectorAll('[data-i18n]').forEach(el => {
1729
+ const key = el.getAttribute('data-i18n');
1730
+ const params = el.getAttribute('data-i18n-params');
1731
+ if (key) {
1732
+ const parsedParams = params ? JSON.parse(params) : {};
1733
+ el.textContent = t(key, parsedParams);
1734
+ }
1735
+ });
1736
+ // Update placeholders
1737
+ document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
1738
+ const key = el.getAttribute('data-i18n-placeholder');
1739
+ if (key) {
1740
+ el.setAttribute('placeholder', t(key));
1741
+ }
1742
+ });
1743
+ // Update html lang attribute
1744
+ document.documentElement.lang = _currentLocale;
1745
+ }
1746
+
1747
+ /**
1748
+ * Change the current language without page reload
1708
1749
  * @param {string} locale - Locale code (e.g., 'en', 'ja')
1709
1750
  */
1710
- function changeLanguage(locale) {
1711
- localStorage.setItem('authrim_setup_lang', locale);
1712
- const url = new URL(window.location.href);
1713
- url.searchParams.set('lang', locale);
1714
- window.location.href = url.toString();
1751
+ async function changeLanguage(locale) {
1752
+ if (locale === _currentLocale) return;
1753
+
1754
+ try {
1755
+ const response = await fetch('/api/translations/' + locale);
1756
+ if (!response.ok) throw new Error('Failed to fetch translations');
1757
+
1758
+ const data = await response.json();
1759
+ _translations = data.translations;
1760
+ _currentLocale = locale;
1761
+
1762
+ // Save preference
1763
+ localStorage.setItem('authrim_setup_lang', locale);
1764
+
1765
+ // Update URL without reload (for sharing/bookmarking)
1766
+ const url = new URL(window.location.href);
1767
+ url.searchParams.set('lang', locale);
1768
+ window.history.replaceState({}, '', url.toString());
1769
+
1770
+ // Update all translatable elements
1771
+ updateAllTranslations();
1772
+ } catch (error) {
1773
+ console.error('Failed to change language:', error);
1774
+ // Fallback: reload the page
1775
+ localStorage.setItem('authrim_setup_lang', locale);
1776
+ const url = new URL(window.location.href);
1777
+ url.searchParams.set('lang', locale);
1778
+ window.location.href = url.toString();
1779
+ }
1715
1780
  }
1716
1781
 
1717
- // Restore language from localStorage on page load
1782
+ // Restore language from localStorage on page load (only if no query param)
1718
1783
  (function() {
1719
1784
  const savedLang = localStorage.getItem('authrim_setup_lang');
1720
- if (savedLang && savedLang !== _currentLocale) {
1721
- const url = new URL(window.location.href);
1722
- if (!url.searchParams.has('lang')) {
1723
- url.searchParams.set('lang', savedLang);
1724
- window.location.href = url.toString();
1725
- }
1785
+ const url = new URL(window.location.href);
1786
+ if (savedLang && savedLang !== _currentLocale && !url.searchParams.has('lang')) {
1787
+ // Use async language change to avoid full reload
1788
+ url.searchParams.set('lang', savedLang);
1789
+ window.history.replaceState({}, '', url.toString());
1790
+ changeLanguage(savedLang);
1726
1791
  }
1727
1792
  })();
1728
1793
  </script>
@@ -1740,16 +1805,16 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1740
1805
  </div>
1741
1806
  </div>
1742
1807
 
1743
- <!-- Language Selector -->
1744
- <div id="lang-selector" class="lang-selector">
1745
- <select id="lang-select" onchange="changeLanguage(this.value)" aria-label="Select language">
1746
- \${availableLocales.map(l => \`<option value="\${l.code}" \${l.code === locale ? 'selected' : ''}>\${l.flag || ''} \${l.nativeName}</option>\`).join('')}
1747
- </select>
1808
+ <!-- Top Controls: Language Selector + Theme Toggle -->
1809
+ <div class="top-controls">
1810
+ <div class="lang-selector">
1811
+ <select id="lang-select" onchange="changeLanguage(this.value)" aria-label="Select language">
1812
+ ${localeOptionsHtml}
1813
+ </select>
1814
+ </div>
1815
+ <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">🌙</button>
1748
1816
  </div>
1749
1817
 
1750
- <!-- Theme Toggle -->
1751
- <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">🌙</button>
1752
-
1753
1818
  <div class="container">
1754
1819
  <header>
1755
1820
  <h1>Authrim</h1>
@@ -1778,132 +1843,132 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1778
1843
  <!-- Step 1: Prerequisites -->
1779
1844
  <div id="section-prerequisites" class="card">
1780
1845
  <h2 class="card-title">
1781
- Prerequisites
1782
- <span class="status-badge status-running" id="prereq-status">Checking...</span>
1846
+ <span data-i18n="web.prereq.title">Prerequisites</span>
1847
+ <span class="status-badge status-running" id="prereq-status" data-i18n="web.prereq.checking">Checking...</span>
1783
1848
  </h2>
1784
1849
  <div id="prereq-content">
1785
- <p>Checking system requirements...</p>
1850
+ <p data-i18n="web.prereq.checkingRequirements">Checking system requirements...</p>
1786
1851
  </div>
1787
1852
  </div>
1788
1853
 
1789
1854
  <!-- Step 1.5: Top Menu (New Setup / Load Config / Manage) -->
1790
1855
  <div id="section-top-menu" class="card hidden">
1791
- <h2 class="card-title">Get Started</h2>
1792
- <p style="margin-bottom: 1.5rem; color: var(--text-muted);">Choose an option to continue:</p>
1856
+ <h2 class="card-title" data-i18n="web.menu.title">Get Started</h2>
1857
+ <p style="margin-bottom: 1.5rem; color: var(--text-muted);" data-i18n="web.menu.subtitle">Choose an option to continue:</p>
1793
1858
 
1794
1859
  <div class="mode-cards" style="grid-template-columns: repeat(3, 1fr);">
1795
1860
  <div class="mode-card" id="menu-new-setup">
1796
1861
  <div class="mode-icon">🆕</div>
1797
- <h3>New Setup</h3>
1798
- <p>Create a new Authrim deployment from scratch</p>
1862
+ <h3 data-i18n="web.menu.newSetup">New Setup</h3>
1863
+ <p data-i18n="web.menu.newSetupDesc">Create a new Authrim deployment from scratch</p>
1799
1864
  </div>
1800
1865
 
1801
1866
  <div class="mode-card" id="menu-load-config">
1802
1867
  <div class="mode-icon">📂</div>
1803
- <h3>Load Config</h3>
1804
- <p>Resume or redeploy using existing config</p>
1868
+ <h3 data-i18n="web.menu.loadConfig">Load Config</h3>
1869
+ <p data-i18n="web.menu.loadConfigDesc">Resume or redeploy using existing config</p>
1805
1870
  </div>
1806
1871
 
1807
1872
  <div class="mode-card" id="menu-manage-env">
1808
1873
  <div class="mode-icon">⚙️</div>
1809
- <h3>Manage Environments</h3>
1810
- <p>View, inspect, or delete existing environments</p>
1874
+ <h3 data-i18n="web.menu.manageEnv">Manage Environments</h3>
1875
+ <p data-i18n="web.menu.manageEnvDesc">View, inspect, or delete existing environments</p>
1811
1876
  </div>
1812
1877
  </div>
1813
1878
  </div>
1814
1879
 
1815
1880
  <!-- Step 1.6: Setup Mode Selection (Quick / Custom) -->
1816
1881
  <div id="section-mode" class="card hidden">
1817
- <h2 class="card-title">Setup Mode</h2>
1818
- <p style="margin-bottom: 1.5rem; color: var(--text-muted);">Choose how you want to set up Authrim:</p>
1882
+ <h2 class="card-title" data-i18n="web.mode.title">Setup Mode</h2>
1883
+ <p style="margin-bottom: 1.5rem; color: var(--text-muted);" data-i18n="web.mode.subtitle">Choose how you want to set up Authrim:</p>
1819
1884
 
1820
1885
  <div class="mode-cards">
1821
1886
  <div class="mode-card" id="mode-quick">
1822
1887
  <div class="mode-icon">⚡</div>
1823
- <h3>Quick Setup</h3>
1824
- <p>Get started in ~5 minutes</p>
1888
+ <h3 data-i18n="web.mode.quick">Quick Setup</h3>
1889
+ <p data-i18n="web.mode.quickDesc">Get started in ~5 minutes</p>
1825
1890
  <ul>
1826
- <li>Environment selection</li>
1827
- <li>Optional custom domain</li>
1828
- <li>Default components</li>
1891
+ <li data-i18n="web.mode.quickEnv">Environment selection</li>
1892
+ <li data-i18n="web.mode.quickDomain">Optional custom domain</li>
1893
+ <li data-i18n="web.mode.quickDefault">Default components</li>
1829
1894
  </ul>
1830
- <span class="mode-badge">Recommended</span>
1895
+ <span class="mode-badge" data-i18n="web.mode.recommended">Recommended</span>
1831
1896
  </div>
1832
1897
 
1833
1898
  <div class="mode-card" id="mode-custom">
1834
1899
  <div class="mode-icon">🔧</div>
1835
- <h3>Custom Setup</h3>
1836
- <p>Full control over configuration</p>
1900
+ <h3 data-i18n="web.mode.custom">Custom Setup</h3>
1901
+ <p data-i18n="web.mode.customDesc">Full control over configuration</p>
1837
1902
  <ul>
1838
- <li>Component selection</li>
1839
- <li>URL configuration</li>
1840
- <li>Advanced settings</li>
1903
+ <li data-i18n="web.mode.customComp">Component selection</li>
1904
+ <li data-i18n="web.mode.customUrl">URL configuration</li>
1905
+ <li data-i18n="web.mode.customAdvanced">Advanced settings</li>
1841
1906
  </ul>
1842
1907
  </div>
1843
1908
  </div>
1844
1909
 
1845
1910
  <div class="button-group">
1846
- <button class="btn-secondary" id="btn-back-top">Back</button>
1911
+ <button class="btn-secondary" id="btn-back-top" data-i18n="web.btn.back">Back</button>
1847
1912
  </div>
1848
1913
  </div>
1849
1914
 
1850
1915
  <!-- Step 1.7: Load Config -->
1851
1916
  <div id="section-load-config" class="card hidden">
1852
- <h2 class="card-title">Load Configuration</h2>
1853
- <p style="margin-bottom: 1rem; color: var(--text-muted);">Select your authrim-config.json file:</p>
1917
+ <h2 class="card-title" data-i18n="web.loadConfig.title">Load Configuration</h2>
1918
+ <p style="margin-bottom: 1rem; color: var(--text-muted);" data-i18n="web.loadConfig.subtitle">Select your authrim-config.json file:</p>
1854
1919
 
1855
1920
  <div class="form-group">
1856
1921
  <div class="file-input-wrapper">
1857
- <span class="file-input-btn">📁 Choose File</span>
1922
+ <span class="file-input-btn" data-i18n="web.loadConfig.chooseFile">📁 Choose File</span>
1858
1923
  <input type="file" id="config-file" accept=".json">
1859
1924
  </div>
1860
1925
  <span id="config-file-name" style="margin-left: 1rem; color: var(--text-muted);"></span>
1861
1926
  </div>
1862
1927
 
1863
1928
  <div id="config-preview-section" class="hidden">
1864
- <h3 style="font-size: 1rem; margin-bottom: 0.5rem;">Configuration Preview</h3>
1929
+ <h3 style="font-size: 1rem; margin-bottom: 0.5rem;" data-i18n="web.loadConfig.preview">Configuration Preview</h3>
1865
1930
  <div class="config-preview" id="config-preview-content"></div>
1866
1931
  </div>
1867
1932
 
1868
1933
  <div id="config-validation-error" class="hidden" style="margin-top: 1rem; padding: 1rem; background: #fee2e2; border: 1px solid #fca5a5; border-radius: 8px;">
1869
1934
  <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
1870
1935
  <span style="font-size: 1.25rem;">⚠️</span>
1871
- <strong style="color: #b91c1c;">Configuration Validation Failed</strong>
1936
+ <strong style="color: #b91c1c;" data-i18n="web.loadConfig.validationFailed">Configuration Validation Failed</strong>
1872
1937
  </div>
1873
1938
  <ul id="config-validation-errors" style="margin: 0; padding-left: 1.5rem; color: #991b1b; font-size: 0.875rem;"></ul>
1874
1939
  </div>
1875
1940
 
1876
1941
  <div id="config-validation-success" class="hidden" style="margin-top: 1rem; padding: 0.75rem 1rem; background: #d1fae5; border: 1px solid #6ee7b7; border-radius: 8px;">
1877
- <span style="color: #065f46;">✓ Configuration is valid</span>
1942
+ <span style="color: #065f46;" data-i18n="web.loadConfig.valid">✓ Configuration is valid</span>
1878
1943
  </div>
1879
1944
 
1880
1945
  <div class="button-group">
1881
- <button class="btn-secondary" id="btn-back-top-2">Back</button>
1882
- <button class="btn-primary" id="btn-load-config" disabled>Load & Continue</button>
1946
+ <button class="btn-secondary" id="btn-back-top-2" data-i18n="web.btn.back">Back</button>
1947
+ <button class="btn-primary" id="btn-load-config" disabled data-i18n="web.loadConfig.loadContinue">Load & Continue</button>
1883
1948
  </div>
1884
1949
  </div>
1885
1950
 
1886
1951
  <!-- Step 2: Configuration -->
1887
1952
  <div id="section-config" class="card hidden">
1888
- <h2 class="card-title">Configuration</h2>
1953
+ <h2 class="card-title" data-i18n="web.config.title">Configuration</h2>
1889
1954
 
1890
1955
  <!-- 1. Components (shown in custom mode) -->
1891
1956
  <div id="advanced-options" class="hidden">
1892
- <h3 style="margin: 0 0 1rem; font-size: 1rem;">📦 Components</h3>
1957
+ <h3 style="margin: 0 0 1rem; font-size: 1rem;">📦 <span data-i18n="web.config.components">Components</span></h3>
1893
1958
 
1894
1959
  <!-- API Component (required) -->
1895
1960
  <div class="component-card">
1896
1961
  <label class="checkbox-item" style="font-weight: 600; margin-bottom: 0.25rem;">
1897
1962
  <input type="checkbox" id="comp-api" checked disabled>
1898
- 🔐 API (required)
1963
+ 🔐 <span data-i18n="web.config.apiRequired">API (required)</span>
1899
1964
  </label>
1900
- <p style="margin: 0.25rem 0 0.5rem 1.5rem; font-size: 0.85rem;">
1965
+ <p style="margin: 0.25rem 0 0.5rem 1.5rem; font-size: 0.85rem;" data-i18n="web.config.apiDesc">
1901
1966
  OIDC Provider endpoints: authorize, token, userinfo, discovery, management APIs.
1902
1967
  </p>
1903
1968
  <div style="margin-left: 1.5rem; display: flex; flex-wrap: wrap; gap: 0.75rem;">
1904
1969
  <label class="checkbox-item" style="font-size: 0.9rem;">
1905
1970
  <input type="checkbox" id="comp-saml">
1906
- SAML IdP
1971
+ <span data-i18n="web.config.saml">SAML IdP</span>
1907
1972
  </label>
1908
1973
  <label class="checkbox-item" style="font-size: 0.9rem;">
1909
1974
  <input type="checkbox" id="comp-async">
@@ -1920,9 +1985,9 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1920
1985
  <div class="component-card">
1921
1986
  <label class="checkbox-item" style="font-weight: 600; margin-bottom: 0.25rem;">
1922
1987
  <input type="checkbox" id="comp-login-ui" checked>
1923
- 🖥️ Login UI
1988
+ 🖥️ <span data-i18n="web.comp.loginUi">Login UI</span>
1924
1989
  </label>
1925
- <p style="margin: 0.25rem 0 0 1.5rem; font-size: 0.85rem;">
1990
+ <p style="margin: 0.25rem 0 0 1.5rem; font-size: 0.85rem;" data-i18n="web.comp.loginUiDesc">
1926
1991
  User-facing login, registration, consent, and account management pages.
1927
1992
  </p>
1928
1993
  </div>
@@ -1931,9 +1996,9 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1931
1996
  <div class="component-card">
1932
1997
  <label class="checkbox-item" style="font-weight: 600; margin-bottom: 0.25rem;">
1933
1998
  <input type="checkbox" id="comp-admin-ui" checked>
1934
- ⚙️ Admin UI
1999
+ ⚙️ <span data-i18n="web.comp.adminUi">Admin UI</span>
1935
2000
  </label>
1936
- <p style="margin: 0.25rem 0 0 1.5rem; font-size: 0.85rem;">
2001
+ <p style="margin: 0.25rem 0 0 1.5rem; font-size: 0.85rem;" data-i18n="web.comp.adminUiDesc">
1937
2002
  Admin dashboard for managing tenants, clients, users, and system settings.
1938
2003
  </p>
1939
2004
  </div>
@@ -1943,28 +2008,28 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1943
2008
 
1944
2009
  <!-- 2. Environment Name -->
1945
2010
  <div class="form-group">
1946
- <label for="env">Environment Name <span style="color: var(--error);">*</span></label>
1947
- <input type="text" id="env" placeholder="e.g., prod, staging, dev" required>
1948
- <small style="color: var(--text-muted)">Lowercase letters, numbers, and hyphens only</small>
2011
+ <label for="env"><span data-i18n="web.form.envName">Environment Name</span> <span style="color: var(--error);">*</span></label>
2012
+ <input type="text" id="env" placeholder="e.g., prod, staging, dev" data-i18n-placeholder="web.form.envNamePlaceholder" required>
2013
+ <small style="color: var(--text-muted)" data-i18n="web.form.envNameHint">Lowercase letters, numbers, and hyphens only</small>
1949
2014
  </div>
1950
2015
 
1951
2016
  <!-- 3. Domain Configuration -->
1952
2017
  <!-- 3.1 API / Issuer Domain -->
1953
2018
  <div class="domain-section">
1954
- <h4>🌐 API / Issuer Domain</h4>
2019
+ <h4>🌐 <span data-i18n="web.section.apiDomain">API / Issuer Domain</span></h4>
1955
2020
 
1956
2021
  <div class="form-group" style="margin-bottom: 0.75rem;">
1957
- <label for="base-domain">Base Domain (API Domain)</label>
1958
- <input type="text" id="base-domain" placeholder="oidc.example.com">
1959
- <small style="color: var(--text-muted)">Custom domain for Authrim. Leave empty to use workers.dev</small>
2022
+ <label for="base-domain" data-i18n="web.form.baseDomain">Base Domain (API Domain)</label>
2023
+ <input type="text" id="base-domain" placeholder="oidc.example.com" data-i18n-placeholder="web.form.baseDomainPlaceholder">
2024
+ <small style="color: var(--text-muted)" data-i18n="web.form.baseDomainHint">Custom domain for Authrim. Leave empty to use workers.dev</small>
1960
2025
  <label class="checkbox-item" id="naked-domain-label" style="display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem;">
1961
2026
  <input type="checkbox" id="naked-domain">
1962
- <span>Exclude tenant name from URL</span>
2027
+ <span data-i18n="web.form.nakedDomain">Exclude tenant name from URL</span>
1963
2028
  </label>
1964
- <small id="naked-domain-hint" style="color: var(--text-muted); margin-left: 1.5rem;">
2029
+ <small id="naked-domain-hint" style="color: var(--text-muted); margin-left: 1.5rem;" data-i18n="web.form.nakedDomainHint">
1965
2030
  Use https://example.com instead of https://{tenant}.example.com
1966
2031
  </small>
1967
- <small id="workers-dev-note" style="color: #d97706; margin-left: 1.5rem; display: none;">
2032
+ <small id="workers-dev-note" style="color: #d97706; margin-left: 1.5rem; display: none;" data-i18n="web.form.nakedDomainWarning">
1968
2033
  ⚠️ Tenant subdomains require a custom domain. Workers.dev does not support wildcard subdomains.
1969
2034
  </small>
1970
2035
  </div>
@@ -1972,146 +2037,146 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1972
2037
  <!-- Default Tenant (hidden when naked domain is checked or using workers.dev) -->
1973
2038
  <div id="tenant-fields">
1974
2039
  <div class="form-group" style="margin-bottom: 0.5rem;">
1975
- <label for="tenant-name">Default Tenant ID</label>
1976
- <input type="text" id="tenant-name" placeholder="default" value="default">
1977
- <small style="color: var(--text-muted)">First tenant identifier (lowercase, no spaces)</small>
1978
- <small id="tenant-workers-note" style="color: #6b7280; display: none;">
2040
+ <label for="tenant-name" data-i18n="web.form.tenantId">Default Tenant ID</label>
2041
+ <input type="text" id="tenant-name" placeholder="default" value="default" data-i18n-placeholder="web.form.tenantIdPlaceholder">
2042
+ <small style="color: var(--text-muted)" data-i18n="web.form.tenantIdHint">First tenant identifier (lowercase, no spaces)</small>
2043
+ <small id="tenant-workers-note" style="color: #6b7280; display: none;" data-i18n="web.form.tenantIdWorkerNote">
1979
2044
  (Tenant ID is used internally. URL subdomain requires custom domain.)
1980
2045
  </small>
1981
2046
  </div>
1982
2047
  </div>
1983
2048
 
1984
2049
  <div class="form-group" style="margin-bottom: 0;">
1985
- <label for="tenant-display">Tenant Display Name</label>
1986
- <input type="text" id="tenant-display" placeholder="My Company" value="Default Tenant">
1987
- <small style="color: var(--text-muted)">Name shown on login page and consent screen</small>
2050
+ <label for="tenant-display" data-i18n="web.form.tenantDisplay">Tenant Display Name</label>
2051
+ <input type="text" id="tenant-display" placeholder="My Company" value="Default Tenant" data-i18n-placeholder="web.form.tenantDisplayPlaceholder">
2052
+ <small style="color: var(--text-muted)" data-i18n="web.form.tenantDisplayHint">Name shown on login page and consent screen</small>
1988
2053
  </div>
1989
2054
  </div>
1990
2055
 
1991
2056
  <!-- 3.2 UI Domains -->
1992
2057
  <div class="domain-section" id="ui-domains-section">
1993
- <h4>🖥️ UI Domains (Optional)</h4>
1994
- <div class="section-hint">
2058
+ <h4>🖥️ <span data-i18n="web.section.uiDomains">UI Domains (Optional)</span></h4>
2059
+ <div class="section-hint" data-i18n="web.section.uiDomainsHint">
1995
2060
  Custom domains for Login/Admin UIs. Each can be set independently.
1996
2061
  Leave empty to use Cloudflare Pages default.
1997
2062
  </div>
1998
2063
 
1999
2064
  <div class="domain-row" id="login-domain-row">
2000
- <span class="domain-label">Login UI</span>
2065
+ <span class="domain-label" data-i18n="web.domain.loginUi">Login UI</span>
2001
2066
  <div class="domain-input-wrapper">
2002
- <input type="text" id="login-domain" placeholder="login.example.com">
2067
+ <input type="text" id="login-domain" placeholder="login.example.com" data-i18n-placeholder="web.form.loginDomainPlaceholder">
2003
2068
  <span class="domain-default" id="login-default">{env}-ar-ui.pages.dev</span>
2004
2069
  </div>
2005
2070
  </div>
2006
2071
 
2007
2072
  <div class="domain-row" id="admin-domain-row">
2008
- <span class="domain-label">Admin UI</span>
2073
+ <span class="domain-label" data-i18n="web.domain.adminUi">Admin UI</span>
2009
2074
  <div class="domain-input-wrapper">
2010
- <input type="text" id="admin-domain" placeholder="admin.example.com">
2075
+ <input type="text" id="admin-domain" placeholder="admin.example.com" data-i18n-placeholder="web.form.adminDomainPlaceholder">
2011
2076
  <span class="domain-default" id="admin-default">{env}-ar-ui.pages.dev/admin</span>
2012
2077
  </div>
2013
2078
  </div>
2014
2079
 
2015
- <div class="section-hint hint-box" style="margin-top: 0.75rem;">
2080
+ <div class="section-hint hint-box" style="margin-top: 0.75rem;" data-i18n="web.section.corsHint">
2016
2081
  💡 CORS: Cross-origin requests from Login/Admin UI to API are automatically allowed.
2017
2082
  </div>
2018
2083
  </div>
2019
2084
 
2020
2085
  <!-- 4. Preview Section (at the bottom) -->
2021
2086
  <div class="infra-section" id="config-preview">
2022
- <h4>📋 Configuration Preview</h4>
2087
+ <h4>📋 <span data-i18n="web.section.configPreview">Configuration Preview</span></h4>
2023
2088
  <div class="infra-item">
2024
- <span class="infra-label">Components:</span>
2089
+ <span class="infra-label" data-i18n="web.preview.components">Components:</span>
2025
2090
  <span class="infra-value" id="preview-components">API, Login UI, Admin UI</span>
2026
2091
  </div>
2027
2092
  <div class="infra-item">
2028
- <span class="infra-label">Workers:</span>
2093
+ <span class="infra-label" data-i18n="web.preview.workers">Workers:</span>
2029
2094
  <span class="infra-value" id="preview-workers">{env}-ar-router, {env}-ar-auth, ...</span>
2030
2095
  </div>
2031
2096
  <div class="infra-item">
2032
- <span class="infra-label">Issuer URL:</span>
2097
+ <span class="infra-label" data-i18n="web.preview.issuerUrl">Issuer URL:</span>
2033
2098
  <span class="infra-value" id="preview-issuer">https://{tenant}.{base-domain}</span>
2034
2099
  </div>
2035
2100
  <div class="infra-item">
2036
- <span class="infra-label">Login UI:</span>
2101
+ <span class="infra-label" data-i18n="web.preview.loginUi">Login UI:</span>
2037
2102
  <span class="infra-value" id="preview-login">{env}-ar-ui.pages.dev</span>
2038
2103
  </div>
2039
2104
  <div class="infra-item">
2040
- <span class="infra-label">Admin UI:</span>
2105
+ <span class="infra-label" data-i18n="web.preview.adminUi">Admin UI:</span>
2041
2106
  <span class="infra-value" id="preview-admin">{env}-ar-ui.pages.dev/admin</span>
2042
2107
  </div>
2043
2108
  </div>
2044
2109
 
2045
2110
  <div class="button-group">
2046
- <button class="btn-secondary" id="btn-back-mode">Back</button>
2047
- <button class="btn-primary" id="btn-configure">Continue</button>
2111
+ <button class="btn-secondary" id="btn-back-mode" data-i18n="web.btn.back">Back</button>
2112
+ <button class="btn-primary" id="btn-configure" data-i18n="web.btn.continue">Continue</button>
2048
2113
  </div>
2049
2114
  </div>
2050
2115
 
2051
2116
  <!-- Step 3: Database Configuration -->
2052
2117
  <div id="section-database" class="card hidden">
2053
- <h2 class="card-title">🗄️ Database Configuration</h2>
2118
+ <h2 class="card-title" data-i18n="web.db.title">🗄️ Database Configuration</h2>
2054
2119
 
2055
- <p style="margin-bottom: 1rem; color: var(--text-muted);">
2120
+ <p style="margin-bottom: 1rem; color: var(--text-muted);" data-i18n="web.db.introDesc">
2056
2121
  Authrim uses two separate D1 databases to isolate personal data from application data.
2057
2122
  </p>
2058
2123
 
2059
- <p style="margin-bottom: 1.5rem; font-size: 0.85rem; color: var(--text-muted);">
2124
+ <p style="margin-bottom: 1.5rem; font-size: 0.85rem; color: var(--text-muted);" data-i18n="web.db.regionNote">
2060
2125
  Note: Database region cannot be changed after creation.
2061
2126
  </p>
2062
2127
 
2063
2128
  <div class="database-config-stack">
2064
2129
  <!-- Core Database (Non-PII) -->
2065
2130
  <div class="database-card">
2066
- <h3>🗄️ Core Database <span style="font-size: 0.8rem; font-weight: normal; color: var(--text-muted);">(Non-PII)</span></h3>
2131
+ <h3>🗄️ <span data-i18n="web.db.coreTitle">Core Database</span> <span style="font-size: 0.8rem; font-weight: normal; color: var(--text-muted);">(<span data-i18n="web.db.coreNonPii">Non-PII</span>)</span></h3>
2067
2132
  <div class="db-description">
2068
- <p>Stores non-personal application data including:</p>
2133
+ <p data-i18n="web.db.coreDataDesc">Stores non-personal application data including:</p>
2069
2134
  <ul>
2070
- <li>OAuth clients and their configurations</li>
2071
- <li>Authorization codes and access tokens</li>
2072
- <li>User sessions and login state</li>
2073
- <li>Tenant settings and configurations</li>
2074
- <li>Audit logs and security events</li>
2135
+ <li data-i18n="web.db.coreData1">OAuth clients and their configurations</li>
2136
+ <li data-i18n="web.db.coreData2">Authorization codes and access tokens</li>
2137
+ <li data-i18n="web.db.coreData3">User sessions and login state</li>
2138
+ <li data-i18n="web.db.coreData4">Tenant settings and configurations</li>
2139
+ <li data-i18n="web.db.coreData5">Audit logs and security events</li>
2075
2140
  </ul>
2076
- <p class="db-hint">This database handles all authentication flows and should be placed close to your primary user base.</p>
2141
+ <p class="db-hint" data-i18n="web.db.coreHint">This database handles all authentication flows and should be placed close to your primary user base.</p>
2077
2142
  </div>
2078
2143
 
2079
2144
  <div class="region-selection">
2080
- <h4>Region</h4>
2145
+ <h4 data-i18n="web.db.region">Region</h4>
2081
2146
  <div class="radio-group">
2082
2147
  <label class="radio-item">
2083
2148
  <input type="radio" name="db-core-location" value="auto" checked>
2084
- <span>Automatic (nearest to you)</span>
2149
+ <span data-i18n="web.db.autoNearest">Automatic (nearest to you)</span>
2085
2150
  </label>
2086
- <div class="radio-separator">Location Hints</div>
2151
+ <div class="radio-separator" data-i18n="web.db.locationHints">Location Hints</div>
2087
2152
  <label class="radio-item">
2088
2153
  <input type="radio" name="db-core-location" value="wnam">
2089
- <span>North America (West)</span>
2154
+ <span data-i18n="web.db.northAmericaWest">North America (West)</span>
2090
2155
  </label>
2091
2156
  <label class="radio-item">
2092
2157
  <input type="radio" name="db-core-location" value="enam">
2093
- <span>North America (East)</span>
2158
+ <span data-i18n="web.db.northAmericaEast">North America (East)</span>
2094
2159
  </label>
2095
2160
  <label class="radio-item">
2096
2161
  <input type="radio" name="db-core-location" value="weur">
2097
- <span>Europe (West)</span>
2162
+ <span data-i18n="web.db.europeWest">Europe (West)</span>
2098
2163
  </label>
2099
2164
  <label class="radio-item">
2100
2165
  <input type="radio" name="db-core-location" value="eeur">
2101
- <span>Europe (East)</span>
2166
+ <span data-i18n="web.db.europeEast">Europe (East)</span>
2102
2167
  </label>
2103
2168
  <label class="radio-item">
2104
2169
  <input type="radio" name="db-core-location" value="apac">
2105
- <span>Asia Pacific</span>
2170
+ <span data-i18n="web.db.asiaPacific">Asia Pacific</span>
2106
2171
  </label>
2107
2172
  <label class="radio-item">
2108
2173
  <input type="radio" name="db-core-location" value="oc">
2109
- <span>Oceania</span>
2174
+ <span data-i18n="web.db.oceania">Oceania</span>
2110
2175
  </label>
2111
- <div class="radio-separator">Jurisdiction (Compliance)</div>
2176
+ <div class="radio-separator" data-i18n="web.db.jurisdiction">Jurisdiction (Compliance)</div>
2112
2177
  <label class="radio-item">
2113
2178
  <input type="radio" name="db-core-location" value="eu">
2114
- <span>EU Jurisdiction (GDPR compliance)</span>
2179
+ <span data-i18n="web.db.euJurisdiction">EU Jurisdiction (GDPR compliance)</span>
2115
2180
  </label>
2116
2181
  </div>
2117
2182
  </div>
@@ -2119,54 +2184,54 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2119
2184
 
2120
2185
  <!-- PII Database -->
2121
2186
  <div class="database-card">
2122
- <h3>🔒 PII Database <span style="font-size: 0.8rem; font-weight: normal; color: var(--text-muted);">(Personal Identifiable Information)</span></h3>
2187
+ <h3>🔒 <span data-i18n="web.db.piiTitle">PII Database</span> <span style="font-size: 0.8rem; font-weight: normal; color: var(--text-muted);">(<span data-i18n="web.db.piiLabel">Personal Identifiable Information</span>)</span></h3>
2123
2188
  <div class="db-description">
2124
- <p>Stores personal user data including:</p>
2189
+ <p data-i18n="web.db.piiDataDesc">Stores personal user data including:</p>
2125
2190
  <ul>
2126
- <li>User profiles (name, email, phone)</li>
2127
- <li>Passkey/WebAuthn credentials</li>
2128
- <li>User preferences and settings</li>
2129
- <li>Any custom user attributes</li>
2191
+ <li data-i18n="web.db.piiData1">User profiles (name, email, phone)</li>
2192
+ <li data-i18n="web.db.piiData2">Passkey/WebAuthn credentials</li>
2193
+ <li data-i18n="web.db.piiData3">User preferences and settings</li>
2194
+ <li data-i18n="web.db.piiData4">Any custom user attributes</li>
2130
2195
  </ul>
2131
- <p class="db-hint">This database contains personal data. Consider placing it in a region that complies with your data protection requirements.</p>
2196
+ <p class="db-hint" data-i18n="web.db.piiHint">This database contains personal data. Consider placing it in a region that complies with your data protection requirements.</p>
2132
2197
  </div>
2133
2198
 
2134
2199
  <div class="region-selection">
2135
- <h4>Region</h4>
2200
+ <h4 data-i18n="web.db.region">Region</h4>
2136
2201
  <div class="radio-group">
2137
2202
  <label class="radio-item">
2138
2203
  <input type="radio" name="db-pii-location" value="auto" checked>
2139
- <span>Automatic (nearest to you)</span>
2204
+ <span data-i18n="web.db.autoNearest">Automatic (nearest to you)</span>
2140
2205
  </label>
2141
- <div class="radio-separator">Location Hints</div>
2206
+ <div class="radio-separator" data-i18n="web.db.locationHints">Location Hints</div>
2142
2207
  <label class="radio-item">
2143
2208
  <input type="radio" name="db-pii-location" value="wnam">
2144
- <span>North America (West)</span>
2209
+ <span data-i18n="web.db.northAmericaWest">North America (West)</span>
2145
2210
  </label>
2146
2211
  <label class="radio-item">
2147
2212
  <input type="radio" name="db-pii-location" value="enam">
2148
- <span>North America (East)</span>
2213
+ <span data-i18n="web.db.northAmericaEast">North America (East)</span>
2149
2214
  </label>
2150
2215
  <label class="radio-item">
2151
2216
  <input type="radio" name="db-pii-location" value="weur">
2152
- <span>Europe (West)</span>
2217
+ <span data-i18n="web.db.europeWest">Europe (West)</span>
2153
2218
  </label>
2154
2219
  <label class="radio-item">
2155
2220
  <input type="radio" name="db-pii-location" value="eeur">
2156
- <span>Europe (East)</span>
2221
+ <span data-i18n="web.db.europeEast">Europe (East)</span>
2157
2222
  </label>
2158
2223
  <label class="radio-item">
2159
2224
  <input type="radio" name="db-pii-location" value="apac">
2160
- <span>Asia Pacific</span>
2225
+ <span data-i18n="web.db.asiaPacific">Asia Pacific</span>
2161
2226
  </label>
2162
2227
  <label class="radio-item">
2163
2228
  <input type="radio" name="db-pii-location" value="oc">
2164
- <span>Oceania</span>
2229
+ <span data-i18n="web.db.oceania">Oceania</span>
2165
2230
  </label>
2166
- <div class="radio-separator">Jurisdiction (Compliance)</div>
2231
+ <div class="radio-separator" data-i18n="web.db.jurisdiction">Jurisdiction (Compliance)</div>
2167
2232
  <label class="radio-item">
2168
2233
  <input type="radio" name="db-pii-location" value="eu">
2169
- <span>EU Jurisdiction (GDPR compliance)</span>
2234
+ <span data-i18n="web.db.euJurisdiction">EU Jurisdiction (GDPR compliance)</span>
2170
2235
  </label>
2171
2236
  </div>
2172
2237
  </div>
@@ -2174,16 +2239,16 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2174
2239
  </div>
2175
2240
 
2176
2241
  <div class="button-group">
2177
- <button class="btn-secondary" id="btn-back-database">Back</button>
2178
- <button class="btn-primary" id="btn-continue-database">Continue</button>
2242
+ <button class="btn-secondary" id="btn-back-database" data-i18n="web.btn.back">Back</button>
2243
+ <button class="btn-primary" id="btn-continue-database" data-i18n="web.btn.continue">Continue</button>
2179
2244
  </div>
2180
2245
  </div>
2181
2246
 
2182
2247
  <!-- Step 4: Email Provider Configuration -->
2183
2248
  <div id="section-email" class="card hidden">
2184
- <h2 class="card-title">📧 Email Provider</h2>
2249
+ <h2 class="card-title" data-i18n="web.email.title">📧 Email Provider</h2>
2185
2250
 
2186
- <p style="margin-bottom: 1rem; color: var(--text-muted);">
2251
+ <p style="margin-bottom: 1rem; color: var(--text-muted);" data-i18n="web.email.introDesc">
2187
2252
  Used for sending Mail OTP and email address verification.
2188
2253
  You can configure this later if you prefer.
2189
2254
  </p>
@@ -2192,88 +2257,90 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2192
2257
  <label class="radio-item" style="padding: 0.75rem; border: 1px solid var(--border); border-radius: 8px;">
2193
2258
  <input type="radio" name="email-setup-choice" value="later" checked>
2194
2259
  <span style="display: flex; flex-direction: column; gap: 0.25rem;">
2195
- <strong>Configure later</strong>
2196
- <small style="color: var(--text-muted);">Skip for now and configure later.</small>
2260
+ <strong data-i18n="web.email.configureLater">Configure later</strong>
2261
+ <small style="color: var(--text-muted);" data-i18n="web.email.configureLaterHint">Skip for now and configure later.</small>
2197
2262
  </span>
2198
2263
  </label>
2199
2264
  <label class="radio-item" style="padding: 0.75rem; border: 1px solid var(--border); border-radius: 8px; margin-top: 0.5rem;">
2200
2265
  <input type="radio" name="email-setup-choice" value="configure">
2201
2266
  <span style="display: flex; flex-direction: column; gap: 0.25rem;">
2202
- <strong>Configure Resend</strong>
2203
- <small style="color: var(--text-muted);">Set up email sending with Resend (recommended for production).</small>
2267
+ <strong data-i18n="web.email.configureResend">Configure Resend</strong>
2268
+ <small style="color: var(--text-muted);" data-i18n="web.email.configureResendHint">Set up email sending with Resend (recommended for production).</small>
2204
2269
  </span>
2205
2270
  </label>
2206
2271
  </div>
2207
2272
 
2208
2273
  <!-- Resend Configuration Form (hidden by default) -->
2209
2274
  <div id="resend-config-form" class="hidden" style="background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem;">
2210
- <h3 style="margin: 0 0 1rem 0; font-size: 1rem;">🔑 Resend Configuration</h3>
2275
+ <h3 style="margin: 0 0 1rem 0; font-size: 1rem;">🔑 <span data-i18n="web.email.resendSetup">Resend Configuration</span></h3>
2211
2276
 
2212
2277
  <div class="alert alert-info" style="margin-bottom: 1rem;">
2213
- <strong>📋 Before you begin:</strong>
2278
+ <strong>📋 <span data-i18n="web.email.beforeBegin">Before you begin:</span></strong>
2214
2279
  <ol style="margin: 0.5rem 0 0 1rem; padding: 0;">
2215
- <li>Create a Resend account at <a href="https://resend.com" target="_blank" style="color: var(--primary);">resend.com</a></li>
2216
- <li>Add and verify your domain at <a href="https://resend.com/domains" target="_blank" style="color: var(--primary);">Domains Dashboard</a></li>
2217
- <li>Create an API key at <a href="https://resend.com/api-keys" target="_blank" style="color: var(--primary);">API Keys</a></li>
2280
+ <li><span data-i18n="web.email.step1">Create a Resend account at</span> <a href="https://resend.com" target="_blank" style="color: var(--primary);">resend.com</a></li>
2281
+ <li><span data-i18n="web.email.step2">Add and verify your domain at</span> <a href="https://resend.com/domains" target="_blank" style="color: var(--primary);">Domains Dashboard</a></li>
2282
+ <li><span data-i18n="web.email.step3">Create an API key at</span> <a href="https://resend.com/api-keys" target="_blank" style="color: var(--primary);">API Keys</a></li>
2218
2283
  </ol>
2219
2284
  </div>
2220
2285
 
2221
2286
  <div class="form-group">
2222
- <label for="resend-api-key">Resend API Key</label>
2287
+ <label for="resend-api-key" data-i18n="web.email.resendApiKey">Resend API Key</label>
2223
2288
  <input type="password" id="resend-api-key" placeholder="re_xxxxxxxxxx" autocomplete="off">
2224
- <small style="color: var(--text-muted);">Your API key starts with "re_"</small>
2289
+ <small style="color: var(--text-muted);" data-i18n="web.email.resendApiKeyHint">Your API key starts with "re_"</small>
2225
2290
  </div>
2226
2291
 
2227
2292
  <div class="form-group">
2228
- <label for="email-from-address">From Email Address</label>
2293
+ <label for="email-from-address" data-i18n="web.email.fromEmailAddress">From Email Address</label>
2229
2294
  <input type="email" id="email-from-address" placeholder="noreply@yourdomain.com" autocomplete="off">
2230
- <small style="color: var(--text-muted);">Must be from a verified domain in your Resend account</small>
2295
+ <small style="color: var(--text-muted);" data-i18n="web.email.fromEmailHint">Must be from a verified domain in your Resend account</small>
2231
2296
  </div>
2232
2297
 
2233
2298
  <div class="form-group">
2234
- <label for="email-from-name">From Display Name (optional)</label>
2299
+ <label for="email-from-name" data-i18n="web.email.fromDisplayName">From Display Name (optional)</label>
2235
2300
  <input type="text" id="email-from-name" placeholder="Authrim" autocomplete="off">
2236
- <small style="color: var(--text-muted);">Displayed as the sender name in email clients</small>
2301
+ <small style="color: var(--text-muted);" data-i18n="web.email.fromDisplayHint">Displayed as the sender name in email clients</small>
2237
2302
  </div>
2238
2303
 
2239
2304
  <div class="alert alert-warning" style="margin-top: 1rem;">
2240
- <strong>⚠️ Domain Verification Required</strong>
2305
+ <strong>⚠️ <span data-i18n="web.email.domainVerificationTitle">Domain Verification Required</span></strong>
2306
+ <p style="margin: 0.25rem 0 0 0; font-size: 0.875rem;" data-i18n="web.email.domainVerificationDesc">
2307
+ Before your domain is verified, emails can only be sent from onboarding@resend.dev (for testing).
2308
+ </p>
2241
2309
  <p style="margin: 0.25rem 0 0 0; font-size: 0.875rem;">
2242
- Before your domain is verified, emails can only be sent from <code>onboarding@resend.dev</code> (for testing).
2243
- <a href="https://resend.com/docs/dashboard/domains/introduction" target="_blank" style="color: var(--primary);">Learn more about domain verification →</a>
2310
+ <a href="https://resend.com/docs/dashboard/domains/introduction" target="_blank" style="color: var(--primary);" data-i18n="web.email.learnMore">Learn more about domain verification →</a>
2244
2311
  </p>
2245
2312
  </div>
2246
2313
  </div>
2247
2314
 
2248
2315
  <div class="button-group">
2249
- <button class="btn-secondary" id="btn-back-email">Back</button>
2250
- <button class="btn-primary" id="btn-continue-email">Continue</button>
2316
+ <button class="btn-secondary" id="btn-back-email" data-i18n="web.btn.back">Back</button>
2317
+ <button class="btn-primary" id="btn-continue-email" data-i18n="web.btn.continue">Continue</button>
2251
2318
  </div>
2252
2319
  </div>
2253
2320
 
2254
2321
  <!-- Step 5: Provisioning -->
2255
2322
  <div id="section-provision" class="card hidden">
2256
2323
  <h2 class="card-title">
2257
- Resource Provisioning
2258
- <span class="status-badge status-pending" id="provision-status">Ready</span>
2324
+ <span data-i18n="web.provision.title">Resource Provisioning</span>
2325
+ <span class="status-badge status-pending" id="provision-status" data-i18n="web.provision.ready">Ready</span>
2259
2326
  </h2>
2260
2327
 
2261
- <p style="margin-bottom: 1rem;">The following resources will be created:</p>
2328
+ <p style="margin-bottom: 1rem;" data-i18n="web.provision.desc">The following resources will be created:</p>
2262
2329
 
2263
2330
  <!-- Resource names preview -->
2264
2331
  <div id="resource-preview" class="resource-preview">
2265
- <h4 style="font-size: 0.9rem; margin-bottom: 0.75rem; color: var(--text-muted);">📋 Resource Names:</h4>
2332
+ <h4 style="font-size: 0.9rem; margin-bottom: 0.75rem; color: var(--text-muted);">📋 <span data-i18n="web.provision.resourcePreview">Resource Names:</span></h4>
2266
2333
  <div class="resource-list">
2267
2334
  <div class="resource-category">
2268
- <strong>D1 Databases:</strong>
2335
+ <strong data-i18n="web.provision.d1Databases">D1 Databases:</strong>
2269
2336
  <ul id="preview-d1"></ul>
2270
2337
  </div>
2271
2338
  <div class="resource-category">
2272
- <strong>KV Namespaces:</strong>
2339
+ <strong data-i18n="web.provision.kvNamespaces">KV Namespaces:</strong>
2273
2340
  <ul id="preview-kv"></ul>
2274
2341
  </div>
2275
2342
  <div class="resource-category">
2276
- <strong>Cryptographic Keys:</strong>
2343
+ <strong data-i18n="web.provision.cryptoKeys">Cryptographic Keys:</strong>
2277
2344
  <ul id="preview-keys"></ul>
2278
2345
  </div>
2279
2346
  </div>
@@ -2283,7 +2350,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2283
2350
  <div id="provision-progress-ui" class="progress-container hidden">
2284
2351
  <div class="progress-status">
2285
2352
  <div class="spinner" id="provision-spinner"></div>
2286
- <span id="provision-current-task">Initializing...</span>
2353
+ <span id="provision-current-task" data-i18n="web.provision.initializing">Initializing...</span>
2287
2354
  </div>
2288
2355
  <div class="progress-bar-wrapper">
2289
2356
  <div class="progress-bar" id="provision-progress-bar" style="width: 0%"></div>
@@ -2292,7 +2359,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2292
2359
 
2293
2360
  <div class="log-toggle" id="provision-log-toggle">
2294
2361
  <span class="arrow">▶</span>
2295
- <span>Show detailed log</span>
2362
+ <span data-i18n="web.provision.showLog">Show detailed log</span>
2296
2363
  </div>
2297
2364
  </div>
2298
2365
 
@@ -2302,35 +2369,35 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2302
2369
 
2303
2370
  <!-- Keys saved location (shown after completion) -->
2304
2371
  <div id="keys-saved-info" class="alert alert-info hidden" style="margin-top: 1rem;">
2305
- <strong>🔑 Keys saved to:</strong>
2372
+ <strong>🔑 <span data-i18n="web.provision.keysSavedTo">Keys saved to:</span></strong>
2306
2373
  <code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: #f1f5f9; border-radius: 4px;" id="keys-path"></code>
2307
- <p style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-muted);">
2374
+ <p style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-muted);" data-i18n="web.provision.keepSafe">
2308
2375
  ⚠️ Keep this directory safe and add it to .gitignore
2309
2376
  </p>
2310
2377
  </div>
2311
2378
 
2312
2379
  <div class="button-group">
2313
- <button class="btn-secondary" id="btn-back-config">Back</button>
2314
- <button class="btn-primary" id="btn-provision">Create Resources</button>
2315
- <button class="btn-secondary hidden" id="btn-save-config-provision" title="Save configuration to file">💾 Save Config</button>
2316
- <button class="btn-primary hidden" id="btn-goto-deploy">Continue to Deploy →</button>
2380
+ <button class="btn-secondary" id="btn-back-config" data-i18n="web.btn.back">Back</button>
2381
+ <button class="btn-primary" id="btn-provision" data-i18n="web.provision.createResources">Create Resources</button>
2382
+ <button class="btn-secondary hidden" id="btn-save-config-provision" title="Save configuration to file" data-i18n="web.provision.saveConfig">💾 Save Config</button>
2383
+ <button class="btn-primary hidden" id="btn-goto-deploy" data-i18n="web.provision.continueDeploy">Continue to Deploy →</button>
2317
2384
  </div>
2318
2385
  </div>
2319
2386
 
2320
2387
  <!-- Step 4: Deployment -->
2321
2388
  <div id="section-deploy" class="card hidden">
2322
2389
  <h2 class="card-title">
2323
- Deployment
2324
- <span class="status-badge status-pending" id="deploy-status">Ready</span>
2390
+ <span data-i18n="web.deploy.title">Deployment</span>
2391
+ <span class="status-badge status-pending" id="deploy-status" data-i18n="web.provision.ready">Ready</span>
2325
2392
  </h2>
2326
2393
 
2327
- <p id="deploy-ready-text" style="margin-bottom: 1rem;">Ready to deploy Authrim workers to Cloudflare.</p>
2394
+ <p id="deploy-ready-text" style="margin-bottom: 1rem;" data-i18n="web.deploy.readyText">Ready to deploy Authrim workers to Cloudflare.</p>
2328
2395
 
2329
2396
  <!-- Progress UI (shown during deployment) -->
2330
2397
  <div id="deploy-progress-ui" class="progress-container hidden">
2331
2398
  <div class="progress-status">
2332
2399
  <div class="spinner" id="deploy-spinner"></div>
2333
- <span id="deploy-current-task">Initializing...</span>
2400
+ <span id="deploy-current-task" data-i18n="web.provision.initializing">Initializing...</span>
2334
2401
  </div>
2335
2402
  <div class="progress-bar-wrapper">
2336
2403
  <div class="progress-bar" id="deploy-progress-bar" style="width: 0%"></div>
@@ -2339,7 +2406,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2339
2406
 
2340
2407
  <div class="log-toggle" id="deploy-log-toggle">
2341
2408
  <span class="arrow">▶</span>
2342
- <span>Show detailed log</span>
2409
+ <span data-i18n="web.provision.showLog">Show detailed log</span>
2343
2410
  </div>
2344
2411
  </div>
2345
2412
 
@@ -2348,50 +2415,50 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2348
2415
  </div>
2349
2416
 
2350
2417
  <div class="button-group">
2351
- <button class="btn-secondary" id="btn-back-provision">Back</button>
2352
- <button class="btn-primary" id="btn-deploy">Start Deploy</button>
2418
+ <button class="btn-secondary" id="btn-back-provision" data-i18n="web.btn.back">Back</button>
2419
+ <button class="btn-primary" id="btn-deploy" data-i18n="web.deploy.startDeploy">Start Deploy</button>
2353
2420
  </div>
2354
2421
  </div>
2355
2422
 
2356
2423
  <!-- Complete -->
2357
2424
  <div id="section-complete" class="card hidden">
2358
- <h2 class="card-title" style="color: var(--success);">
2425
+ <h2 class="card-title" style="color: var(--success);" data-i18n="web.complete.title">
2359
2426
  ✅ Setup Complete!
2360
2427
  </h2>
2361
2428
 
2362
- <p>Authrim has been successfully deployed.</p>
2429
+ <p data-i18n="web.complete.desc">Authrim has been successfully deployed.</p>
2363
2430
 
2364
2431
  <div class="url-display" id="urls">
2365
2432
  <!-- URLs will be inserted here -->
2366
2433
  </div>
2367
2434
 
2368
2435
  <div class="alert alert-info" style="margin-top: 1rem;">
2369
- <strong>Next Steps:</strong>
2436
+ <strong data-i18n="web.complete.nextSteps">Next Steps:</strong>
2370
2437
  <ol style="margin-left: 1.5rem; margin-top: 0.5rem;">
2371
- <li>Visit the <strong>Admin Setup</strong> URL above to register your first admin with Passkey</li>
2372
- <li>Log in to the Admin UI to create OAuth clients</li>
2373
- <li>Configure your application to use the OIDC endpoints</li>
2438
+ <li data-i18n="web.complete.step1">Visit the <strong>Admin Setup</strong> URL above to register your first admin with Passkey</li>
2439
+ <li data-i18n="web.complete.step2">Log in to the Admin UI to create OAuth clients</li>
2440
+ <li data-i18n="web.complete.step3">Configure your application to use the OIDC endpoints</li>
2374
2441
  </ol>
2375
2442
  </div>
2376
2443
 
2377
2444
  <div class="button-group" style="margin-top: 1.5rem; justify-content: center;">
2378
- <button class="btn-secondary" id="btn-save-config-complete" title="Save configuration to file">💾 Save Configuration</button>
2379
- <button class="btn-secondary" id="btn-back-to-main" title="Return to main screen">🏠 Back to Main</button>
2445
+ <button class="btn-secondary" id="btn-save-config-complete" title="Save configuration to file" data-i18n="web.complete.saveConfig">💾 Save Configuration</button>
2446
+ <button class="btn-secondary" id="btn-back-to-main" title="Return to main screen" data-i18n="web.complete.backToMain">🏠 Back to Main</button>
2380
2447
  </div>
2381
2448
 
2382
2449
  <p style="text-align: center; margin-top: 1.5rem; color: var(--text-muted); font-size: 0.9rem;">
2383
- ✅ Setup is complete. You can safely close this window.
2450
+ <span data-i18n="web.complete.canClose">Setup is complete. You can safely close this window.</span>
2384
2451
  </p>
2385
2452
  </div>
2386
2453
 
2387
2454
  <!-- Environment Management: List -->
2388
2455
  <div id="section-env-list" class="card hidden">
2389
2456
  <h2 class="card-title">
2390
- Manage Environments
2391
- <span class="status-badge status-pending" id="env-list-status">Loading...</span>
2457
+ <span data-i18n="web.env.title">Manage Environments</span>
2458
+ <span class="status-badge status-pending" id="env-list-status" data-i18n="web.env.loading">Loading...</span>
2392
2459
  </h2>
2393
2460
 
2394
- <p style="margin-bottom: 1rem; color: var(--text-muted);">
2461
+ <p style="margin-bottom: 1rem; color: var(--text-muted);" data-i18n="web.env.detectedDesc">
2395
2462
  Detected Authrim environments in your Cloudflare account:
2396
2463
  </p>
2397
2464
 
@@ -2404,21 +2471,21 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2404
2471
  <!-- Environment cards will be inserted here -->
2405
2472
  </div>
2406
2473
 
2407
- <div id="no-envs-message" class="alert alert-info hidden">
2474
+ <div id="no-envs-message" class="alert alert-info hidden" data-i18n="web.env.noEnvsDetected">
2408
2475
  No Authrim environments detected in this Cloudflare account.
2409
2476
  </div>
2410
2477
  </div>
2411
2478
 
2412
2479
  <div class="button-group">
2413
- <button class="btn-secondary" id="btn-back-env-list">Back</button>
2414
- <button class="btn-secondary" id="btn-refresh-env-list">🔄 Refresh</button>
2480
+ <button class="btn-secondary" id="btn-back-env-list" data-i18n="web.btn.back">Back</button>
2481
+ <button class="btn-secondary" id="btn-refresh-env-list">🔄 <span data-i18n="web.env.refresh">Refresh</span></button>
2415
2482
  </div>
2416
2483
  </div>
2417
2484
 
2418
2485
  <!-- Environment Management: Details -->
2419
2486
  <div id="section-env-detail" class="card hidden">
2420
2487
  <h2 class="card-title">
2421
- 📋 Environment Details
2488
+ 📋 <span data-i18n="web.envDetail.title">Environment Details</span>
2422
2489
  <code id="detail-env-name" style="background: var(--bg); padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 1rem;"></code>
2423
2490
  </h2>
2424
2491
 
@@ -2427,23 +2494,23 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2427
2494
  <div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem;">
2428
2495
  <span style="font-size: 1.5rem;">⚠️</span>
2429
2496
  <div>
2430
- <div style="font-weight: 600;">Admin Account Not Configured</div>
2431
- <div style="font-size: 0.875rem; opacity: 0.85;">Initial administrator has not been set up for this environment.</div>
2497
+ <div style="font-weight: 600;" data-i18n="web.envDetail.adminNotConfigured">Admin Account Not Configured</div>
2498
+ <div style="font-size: 0.875rem; opacity: 0.85;" data-i18n="web.envDetail.adminNotConfiguredDesc">Initial administrator has not been set up for this environment.</div>
2432
2499
  </div>
2433
2500
  </div>
2434
- <button class="btn-primary" id="btn-start-admin-setup" style="margin-top: 0.5rem;">
2501
+ <button class="btn-primary" id="btn-start-admin-setup" style="margin-top: 0.5rem;" data-i18n="web.envDetail.startPasskey">
2435
2502
  🔐 Start Admin Account Setup with Passkey
2436
2503
  </button>
2437
2504
  <div id="admin-setup-result" class="hidden" style="margin-top: 1rem; padding: 0.75rem; background: var(--card-bg); border-radius: 6px;">
2438
- <div style="font-weight: 500; margin-bottom: 0.5rem;">Setup URL Generated:</div>
2505
+ <div style="font-weight: 500; margin-bottom: 0.5rem;" data-i18n="web.envDetail.setupUrlGenerated">Setup URL Generated:</div>
2439
2506
  <div style="display: flex; gap: 0.5rem; align-items: center;">
2440
2507
  <input type="text" id="admin-setup-url" readonly style="flex: 1; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; font-family: monospace; font-size: 0.875rem; background: var(--bg); color: var(--text);">
2441
- <button class="btn-secondary" id="btn-copy-setup-url" style="white-space: nowrap;">📋 Copy</button>
2508
+ <button class="btn-secondary" id="btn-copy-setup-url" style="white-space: nowrap;">📋 <span data-i18n="web.envDetail.copyBtn">Copy</span></button>
2442
2509
  </div>
2443
2510
  <div style="text-align: center; margin-top: 1rem;">
2444
- <a id="btn-open-setup-url" href="#" target="_blank" class="btn-primary">🔑 Open Setup</a>
2511
+ <a id="btn-open-setup-url" href="#" target="_blank" class="btn-primary">🔑 <span data-i18n="web.envDetail.openSetup">Open Setup</span></a>
2445
2512
  </div>
2446
- <div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.75rem; text-align: center;">
2513
+ <div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.75rem; text-align: center;" data-i18n="web.envDetail.urlValidFor">
2447
2514
  This URL is valid for 1 hour. Open it in a browser to register the first admin account.
2448
2515
  </div>
2449
2516
  </div>
@@ -2453,7 +2520,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2453
2520
  <!-- Workers -->
2454
2521
  <div class="resource-section">
2455
2522
  <div class="resource-section-title">
2456
- 🔧 Workers <span class="count" id="detail-workers-count">(0)</span>
2523
+ 🔧 <span data-i18n="web.envDetail.workers">Workers</span> <span class="count" id="detail-workers-count">(0)</span>
2457
2524
  </div>
2458
2525
  <div class="resource-list" id="detail-workers-list"></div>
2459
2526
  </div>
@@ -2461,7 +2528,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2461
2528
  <!-- D1 Databases -->
2462
2529
  <div class="resource-section">
2463
2530
  <div class="resource-section-title">
2464
- 📊 D1 Databases <span class="count" id="detail-d1-count">(0)</span>
2531
+ 📊 <span data-i18n="web.envDetail.d1Databases">D1 Databases</span> <span class="count" id="detail-d1-count">(0)</span>
2465
2532
  </div>
2466
2533
  <div class="resource-list" id="detail-d1-list"></div>
2467
2534
  </div>
@@ -2469,7 +2536,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2469
2536
  <!-- KV Namespaces -->
2470
2537
  <div class="resource-section">
2471
2538
  <div class="resource-section-title">
2472
- 🗄️ KV Namespaces <span class="count" id="detail-kv-count">(0)</span>
2539
+ 🗄️ <span data-i18n="web.envDetail.kvNamespaces">KV Namespaces</span> <span class="count" id="detail-kv-count">(0)</span>
2473
2540
  </div>
2474
2541
  <div class="resource-list" id="detail-kv-list"></div>
2475
2542
  </div>
@@ -2477,7 +2544,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2477
2544
  <!-- Queues -->
2478
2545
  <div class="resource-section" id="detail-queues-section">
2479
2546
  <div class="resource-section-title">
2480
- 📨 Queues <span class="count" id="detail-queues-count">(0)</span>
2547
+ 📨 <span data-i18n="web.envDetail.queues">Queues</span> <span class="count" id="detail-queues-count">(0)</span>
2481
2548
  </div>
2482
2549
  <div class="resource-list" id="detail-queues-list"></div>
2483
2550
  </div>
@@ -2485,7 +2552,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2485
2552
  <!-- R2 Buckets -->
2486
2553
  <div class="resource-section" id="detail-r2-section">
2487
2554
  <div class="resource-section-title">
2488
- 📁 R2 Buckets <span class="count" id="detail-r2-count">(0)</span>
2555
+ 📁 <span data-i18n="web.envDetail.r2Buckets">R2 Buckets</span> <span class="count" id="detail-r2-count">(0)</span>
2489
2556
  </div>
2490
2557
  <div class="resource-list" id="detail-r2-list"></div>
2491
2558
  </div>
@@ -2493,41 +2560,41 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2493
2560
  <!-- Pages Projects -->
2494
2561
  <div class="resource-section" id="detail-pages-section">
2495
2562
  <div class="resource-section-title">
2496
- 📄 Pages Projects <span class="count" id="detail-pages-count">(0)</span>
2563
+ 📄 <span data-i18n="web.envDetail.pagesProjects">Pages Projects</span> <span class="count" id="detail-pages-count">(0)</span>
2497
2564
  </div>
2498
2565
  <div class="resource-list" id="detail-pages-list"></div>
2499
2566
  </div>
2500
2567
  </div>
2501
2568
 
2502
2569
  <div class="button-group">
2503
- <button class="btn-secondary" id="btn-back-env-detail">← Back to List</button>
2504
- <button class="btn-danger" id="btn-delete-from-detail">🗑️ Delete Environment...</button>
2570
+ <button class="btn-secondary" id="btn-back-env-detail" data-i18n="web.env.backToList">← Back to List</button>
2571
+ <button class="btn-danger" id="btn-delete-from-detail">🗑️ <span data-i18n="web.env.deleteEnv">Delete Environment...</span></button>
2505
2572
  </div>
2506
2573
  </div>
2507
2574
 
2508
2575
  <!-- Environment Management: Delete Confirmation -->
2509
2576
  <div id="section-env-delete" class="card hidden">
2510
2577
  <h2 class="card-title" style="color: var(--error);">
2511
- ⚠️ Delete Environment
2578
+ ⚠️ <span data-i18n="web.delete.title">Delete Environment</span>
2512
2579
  </h2>
2513
2580
 
2514
2581
  <div class="alert alert-warning">
2515
- <strong>Warning:</strong> This action is irreversible. All selected resources will be permanently deleted.
2582
+ <strong>Warning:</strong> <span data-i18n="web.delete.warning">This action is irreversible. All selected resources will be permanently deleted.</span>
2516
2583
  </div>
2517
2584
 
2518
2585
  <div style="margin: 1.5rem 0;">
2519
2586
  <h3 style="font-size: 1.1rem; margin-bottom: 1rem;">
2520
- Environment: <code id="delete-env-name" style="background: var(--bg); padding: 0.25rem 0.5rem; border-radius: 4px;"></code>
2587
+ <span data-i18n="web.delete.environment">Environment:</span> <code id="delete-env-name" style="background: var(--bg); padding: 0.25rem 0.5rem; border-radius: 4px;"></code>
2521
2588
  </h3>
2522
2589
 
2523
2590
  <div id="delete-options-section">
2524
- <p style="margin-bottom: 1rem; color: var(--text-muted);">Select resources to delete:</p>
2591
+ <p style="margin-bottom: 1rem; color: var(--text-muted);" data-i18n="web.delete.selectResources">Select resources to delete:</p>
2525
2592
 
2526
2593
  <div class="delete-options">
2527
2594
  <label class="checkbox-item delete-option">
2528
2595
  <input type="checkbox" id="delete-workers" checked>
2529
2596
  <span>
2530
- <strong>Workers</strong>
2597
+ <strong data-i18n="web.delete.workers">Workers</strong>
2531
2598
  <small id="delete-workers-count">(0 workers)</small>
2532
2599
  </span>
2533
2600
  </label>
@@ -2535,7 +2602,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2535
2602
  <label class="checkbox-item delete-option">
2536
2603
  <input type="checkbox" id="delete-d1" checked>
2537
2604
  <span>
2538
- <strong>D1 Databases</strong>
2605
+ <strong data-i18n="web.delete.d1Databases">D1 Databases</strong>
2539
2606
  <small id="delete-d1-count">(0 databases)</small>
2540
2607
  </span>
2541
2608
  </label>
@@ -2543,7 +2610,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2543
2610
  <label class="checkbox-item delete-option">
2544
2611
  <input type="checkbox" id="delete-kv" checked>
2545
2612
  <span>
2546
- <strong>KV Namespaces</strong>
2613
+ <strong data-i18n="web.delete.kvNamespaces">KV Namespaces</strong>
2547
2614
  <small id="delete-kv-count">(0 namespaces)</small>
2548
2615
  </span>
2549
2616
  </label>
@@ -2551,7 +2618,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2551
2618
  <label class="checkbox-item delete-option">
2552
2619
  <input type="checkbox" id="delete-queues" checked>
2553
2620
  <span>
2554
- <strong>Queues</strong>
2621
+ <strong data-i18n="web.delete.queues">Queues</strong>
2555
2622
  <small id="delete-queues-count">(0 queues)</small>
2556
2623
  </span>
2557
2624
  </label>
@@ -2559,7 +2626,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2559
2626
  <label class="checkbox-item delete-option">
2560
2627
  <input type="checkbox" id="delete-r2" checked>
2561
2628
  <span>
2562
- <strong>R2 Buckets</strong>
2629
+ <strong data-i18n="web.delete.r2Buckets">R2 Buckets</strong>
2563
2630
  <small id="delete-r2-count">(0 buckets)</small>
2564
2631
  </span>
2565
2632
  </label>
@@ -2567,7 +2634,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2567
2634
  <label class="checkbox-item delete-option">
2568
2635
  <input type="checkbox" id="delete-pages" checked>
2569
2636
  <span>
2570
- <strong>Pages Projects</strong>
2637
+ <strong data-i18n="web.delete.pagesProjects">Pages Projects</strong>
2571
2638
  <small id="delete-pages-count">(0 projects)</small>
2572
2639
  </span>
2573
2640
  </label>
@@ -2579,7 +2646,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2579
2646
  <div id="delete-progress-ui" class="progress-container hidden">
2580
2647
  <div class="progress-status">
2581
2648
  <div class="spinner" id="delete-spinner"></div>
2582
- <span id="delete-current-task">Initializing...</span>
2649
+ <span id="delete-current-task" data-i18n="web.provision.initializing">Initializing...</span>
2583
2650
  </div>
2584
2651
  <div class="progress-bar-wrapper">
2585
2652
  <div class="progress-bar" id="delete-progress-bar" style="width: 0%"></div>
@@ -2588,7 +2655,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2588
2655
 
2589
2656
  <div class="log-toggle" id="delete-log-toggle">
2590
2657
  <span class="arrow">▶</span>
2591
- <span>Show detailed log</span>
2658
+ <span data-i18n="web.provision.showLog">Show detailed log</span>
2592
2659
  </div>
2593
2660
  </div>
2594
2661
 
@@ -2599,8 +2666,8 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2599
2666
  <div id="delete-result" class="hidden"></div>
2600
2667
 
2601
2668
  <div class="button-group">
2602
- <button class="btn-secondary" id="btn-back-env-delete">Cancel</button>
2603
- <button class="btn-primary" id="btn-confirm-delete" style="background: var(--error);">🗑️ Delete Selected</button>
2669
+ <button class="btn-secondary" id="btn-back-env-delete" data-i18n="web.delete.cancelBtn">Cancel</button>
2670
+ <button class="btn-primary" id="btn-confirm-delete" style="background: var(--error);">🗑️ <span data-i18n="web.delete.confirmBtn">Delete Selected</span></button>
2604
2671
  </div>
2605
2672
  </div>
2606
2673
  </div>
@@ -2609,14 +2676,16 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2609
2676
  <div id="save-config-modal" class="modal hidden">
2610
2677
  <div class="modal-backdrop"></div>
2611
2678
  <div class="modal-content">
2612
- <h3 style="margin: 0 0 1rem 0;">💾 Save Configuration?</h3>
2613
- <p style="color: var(--text-muted); margin-bottom: 1.5rem;">
2679
+ <h3 style="margin: 0 0 1rem 0;">💾 <span data-i18n="web.modal.saveTitle">Save Configuration?</span></h3>
2680
+ <p style="color: var(--text-muted); margin-bottom: 1.5rem;" data-i18n="web.modal.saveQuestion">
2614
2681
  Would you like to save your configuration to a file before proceeding?
2682
+ </p>
2683
+ <p style="color: var(--text-muted); margin-bottom: 1.5rem; font-size: 0.9rem;" data-i18n="web.modal.saveReason">
2615
2684
  This allows you to resume setup later or use the same settings for another deployment.
2616
2685
  </p>
2617
2686
  <div class="button-group" style="justify-content: flex-end;">
2618
- <button class="btn-secondary" id="modal-skip-save">Skip</button>
2619
- <button class="btn-primary" id="modal-save-config">Save Configuration</button>
2687
+ <button class="btn-secondary" id="modal-skip-save" data-i18n="web.modal.skipBtn">Skip</button>
2688
+ <button class="btn-primary" id="modal-save-config" data-i18n="web.modal.saveBtn">Save Configuration</button>
2620
2689
  </div>
2621
2690
  </div>
2622
2691
  </div>
@@ -2777,11 +2846,11 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2777
2846
  if (isHidden) {
2778
2847
  log.classList.remove('hidden');
2779
2848
  toggle.classList.add('open');
2780
- toggle.querySelector('span:last-child').textContent = 'Hide detailed log';
2849
+ toggle.querySelector('span:last-child').textContent = t('web.provision.hideLog');
2781
2850
  } else {
2782
2851
  log.classList.add('hidden');
2783
2852
  toggle.classList.remove('open');
2784
- toggle.querySelector('span:last-child').textContent = 'Show detailed log';
2853
+ toggle.querySelector('span:last-child').textContent = t('web.provision.showLog');
2785
2854
  }
2786
2855
  });
2787
2856
  }
@@ -2921,18 +2990,18 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2921
2990
  prereqContent.textContent = '';
2922
2991
 
2923
2992
  if (!result.wranglerInstalled) {
2924
- prereqStatus.textContent = 'Error';
2993
+ prereqStatus.textContent = t('web.error');
2925
2994
  prereqStatus.className = 'status-badge status-error';
2926
2995
 
2927
2996
  const alertDiv = document.createElement('div');
2928
2997
  alertDiv.className = 'alert alert-error';
2929
2998
 
2930
2999
  const title = document.createElement('strong');
2931
- title.textContent = 'Wrangler not installed';
3000
+ title.textContent = t('web.error.wranglerNotInstalled');
2932
3001
  alertDiv.appendChild(title);
2933
3002
 
2934
3003
  const para = document.createElement('p');
2935
- para.textContent = 'Please install wrangler first:';
3004
+ para.textContent = t('web.error.pleaseInstall');
2936
3005
  alertDiv.appendChild(para);
2937
3006
 
2938
3007
  const code = document.createElement('code');
@@ -2949,18 +3018,18 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2949
3018
  }
2950
3019
 
2951
3020
  if (!result.auth.isLoggedIn) {
2952
- prereqStatus.textContent = 'Login Required';
3021
+ prereqStatus.textContent = t('web.error.notLoggedIn');
2953
3022
  prereqStatus.className = 'status-badge status-warning';
2954
3023
 
2955
3024
  const alertDiv = document.createElement('div');
2956
3025
  alertDiv.className = 'alert alert-warning';
2957
3026
 
2958
3027
  const title = document.createElement('strong');
2959
- title.textContent = 'Not logged in to Cloudflare';
3028
+ title.textContent = t('web.error.notLoggedIn');
2960
3029
  alertDiv.appendChild(title);
2961
3030
 
2962
3031
  const para1 = document.createElement('p');
2963
- para1.textContent = 'Please run this command in your terminal:';
3032
+ para1.textContent = t('web.error.runCommand');
2964
3033
  alertDiv.appendChild(para1);
2965
3034
 
2966
3035
  const code = document.createElement('code');
@@ -2974,14 +3043,14 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2974
3043
 
2975
3044
  const para2 = document.createElement('p');
2976
3045
  para2.style.marginTop = '0.5rem';
2977
- para2.textContent = 'Then refresh this page.';
3046
+ para2.textContent = t('web.error.thenRefresh');
2978
3047
  alertDiv.appendChild(para2);
2979
3048
 
2980
3049
  prereqContent.appendChild(alertDiv);
2981
3050
  return false;
2982
3051
  }
2983
3052
 
2984
- prereqStatus.textContent = 'Ready';
3053
+ prereqStatus.textContent = t('web.prereq.ready');
2985
3054
  prereqStatus.className = 'status-badge status-success';
2986
3055
 
2987
3056
  // Store working directory and workers subdomain for later use
@@ -2992,11 +3061,12 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2992
3061
  alertDiv.className = 'alert alert-success';
2993
3062
 
2994
3063
  const p1 = document.createElement('p');
2995
- p1.textContent = '✓ Wrangler installed';
3064
+ p1.textContent = '✓ ' + t('web.prereq.wranglerInstalled');
3065
+ p1.setAttribute('data-i18n', 'web.prereq.wranglerInstalled');
2996
3066
  alertDiv.appendChild(p1);
2997
3067
 
2998
3068
  const p2 = document.createElement('p');
2999
- p2.textContent = '✓ Logged in as ' + (result.auth.email || 'Unknown');
3069
+ p2.textContent = '✓ ' + t('web.prereq.loggedInAs', { email: result.auth.email || 'Unknown' });
3000
3070
  alertDiv.appendChild(p2);
3001
3071
 
3002
3072
  prereqContent.appendChild(alertDiv);
@@ -3006,7 +3076,8 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3006
3076
 
3007
3077
  const btn = document.createElement('button');
3008
3078
  btn.className = 'btn-primary';
3009
- btn.textContent = 'Continue';
3079
+ btn.textContent = t('common.continue');
3080
+ btn.setAttribute('data-i18n', 'common.continue');
3010
3081
  btn.addEventListener('click', showTopMenu);
3011
3082
  buttonGroup.appendChild(btn);
3012
3083
 
@@ -3014,10 +3085,10 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3014
3085
 
3015
3086
  return true;
3016
3087
  } catch (error) {
3017
- prereqStatus.textContent = 'Error';
3088
+ prereqStatus.textContent = t('web.error');
3018
3089
  prereqStatus.className = 'status-badge status-error';
3019
3090
  prereqContent.textContent = '';
3020
- prereqContent.appendChild(createAlert('error', 'Error checking prerequisites: ' + error.message));
3091
+ prereqContent.appendChild(createAlert('error', t('web.error.checkingPrereq') + ' ' + error.message));
3021
3092
  return false;
3022
3093
  }
3023
3094
  }
@@ -3088,7 +3159,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3088
3159
  const errorList = document.getElementById('config-validation-errors');
3089
3160
  while (errorList.firstChild) errorList.removeChild(errorList.firstChild);
3090
3161
  const li = document.createElement('li');
3091
- li.textContent = 'Invalid JSON: ' + err.message;
3162
+ li.textContent = t('web.error.invalidJson') + ' ' + err.message;
3092
3163
  errorList.appendChild(li);
3093
3164
  loadedConfig = null;
3094
3165
  return;
@@ -3133,7 +3204,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3133
3204
  const errorList = document.getElementById('config-validation-errors');
3134
3205
  while (errorList.firstChild) errorList.removeChild(errorList.firstChild);
3135
3206
  const li = document.createElement('li');
3136
- li.textContent = 'Validation request failed: ' + err.message;
3207
+ li.textContent = t('web.error.validationFailed') + ' ' + err.message;
3137
3208
  errorList.appendChild(li);
3138
3209
  loadedConfig = null;
3139
3210
  }
@@ -3420,7 +3491,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3420
3491
  // Check if environment already exists
3421
3492
  const configureBtn = document.getElementById('btn-configure');
3422
3493
  const originalText = configureBtn.textContent;
3423
- configureBtn.textContent = 'Checking...';
3494
+ configureBtn.textContent = t('web.status.checking');
3424
3495
  configureBtn.disabled = true;
3425
3496
 
3426
3497
  try {
@@ -3584,7 +3655,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3584
3655
 
3585
3656
  // Save email configuration to server
3586
3657
  btn.disabled = true;
3587
- btn.textContent = 'Saving...';
3658
+ btn.textContent = t('web.status.saving');
3588
3659
 
3589
3660
  try {
3590
3661
  const result = await api('/email/configure', {
@@ -3612,12 +3683,12 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3612
3683
  } catch (error) {
3613
3684
  alert('Failed to save email configuration: ' + error.message);
3614
3685
  btn.disabled = false;
3615
- btn.textContent = 'Continue';
3686
+ btn.textContent = t('web.btn.continue');
3616
3687
  return;
3617
3688
  }
3618
3689
 
3619
3690
  btn.disabled = false;
3620
- btn.textContent = 'Continue';
3691
+ btn.textContent = t('web.btn.continue');
3621
3692
  } else {
3622
3693
  // Configure later - no email provider
3623
3694
  config.email = {
@@ -3640,18 +3711,18 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3640
3711
  const modal = document.getElementById('save-config-modal');
3641
3712
  const btn = document.getElementById('modal-save-config');
3642
3713
  btn.disabled = true;
3643
- btn.textContent = 'Saving...';
3714
+ btn.textContent = t('web.status.saving');
3644
3715
 
3645
3716
  try {
3646
3717
  await saveConfigToFile();
3647
3718
  modal.classList.add('hidden');
3648
3719
  btn.disabled = false;
3649
- btn.textContent = 'Save Configuration';
3720
+ btn.textContent = t('web.btn.saveConfiguration');
3650
3721
  proceedToProvision();
3651
3722
  } catch (error) {
3652
3723
  alert('Failed to save configuration: ' + error.message);
3653
3724
  btn.disabled = false;
3654
- btn.textContent = 'Save Configuration';
3725
+ btn.textContent = t('web.btn.saveConfiguration');
3655
3726
  }
3656
3727
  });
3657
3728
 
@@ -3697,7 +3768,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3697
3768
  btn.disabled = true;
3698
3769
  btn.classList.add('hidden');
3699
3770
  btnGotoDeploy.classList.add('hidden');
3700
- status.textContent = 'Running...';
3771
+ status.textContent = t('web.status.running');
3701
3772
  status.className = 'status-badge status-running';
3702
3773
  progressUI.classList.remove('hidden');
3703
3774
  log.classList.add('hidden'); // Log is hidden by default, toggled via button
@@ -3707,7 +3778,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3707
3778
 
3708
3779
  let provisionCompleted = 0;
3709
3780
  const totalResources = 8; // D1 Core, D1 PII, KV Settings, KV Cache, KV Tokens, R2 (optional), Queues (optional), Keys
3710
- updateProgressUI('provision', 0, totalResources, 'Initializing...');
3781
+ updateProgressUI('provision', 0, totalResources, t('web.status.initializing'));
3711
3782
 
3712
3783
  // Start polling for progress
3713
3784
  let lastProgressLength = 0;
@@ -3784,7 +3855,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3784
3855
  updateProgressUI('provision', totalResources, totalResources, '✅ Provisioning complete!');
3785
3856
  output.textContent += '\\n✅ Provisioning complete!\\n';
3786
3857
  scrollToBottom(log);
3787
- status.textContent = 'Complete';
3858
+ status.textContent = t('web.status.complete');
3788
3859
  status.className = 'status-badge status-success';
3789
3860
 
3790
3861
  // Mark provisioning as completed
@@ -3794,7 +3865,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3794
3865
  keysSavedInfo.classList.remove('hidden');
3795
3866
 
3796
3867
  // Update buttons - change to warning style for re-provision
3797
- btn.textContent = 'Re-provision (Delete & Create)';
3868
+ btn.textContent = t('web.btn.reprovision');
3798
3869
  btn.classList.remove('hidden', 'btn-primary');
3799
3870
  btn.classList.add('btn-warning');
3800
3871
  btn.disabled = false;
@@ -3811,7 +3882,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3811
3882
 
3812
3883
  output.textContent += '\\n❌ Error: ' + error.message + '\\n';
3813
3884
  scrollToBottom(log);
3814
- status.textContent = 'Error';
3885
+ status.textContent = t('web.status.error');
3815
3886
  status.className = 'status-badge status-error';
3816
3887
  btn.classList.remove('hidden');
3817
3888
  btn.disabled = false;
@@ -3847,18 +3918,18 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3847
3918
 
3848
3919
  btn.disabled = true;
3849
3920
  btn.classList.add('hidden');
3850
- status.textContent = 'Deploying...';
3921
+ status.textContent = t('web.status.deploying');
3851
3922
  status.className = 'status-badge status-running';
3852
3923
  readyText.classList.add('hidden');
3853
3924
  progressUI.classList.remove('hidden');
3854
3925
  log.classList.add('hidden'); // Log is hidden by default, toggled via button
3855
- output.textContent = 'Starting deployment...\\n\\n';
3926
+ output.textContent = t('web.status.startingDeploy') + '\\n\\n';
3856
3927
 
3857
3928
  let completedCount = 0;
3858
3929
  // Use indeterminate progress - actual step count varies based on components
3859
3930
  // We'll update the total dynamically based on actual progress
3860
3931
  let totalComponents = 0; // Will be calculated from actual progress
3861
- updateProgressUI('deploy', 0, 100, 'Initializing...');
3932
+ updateProgressUI('deploy', 0, 100, t('web.status.initializing'));
3862
3933
 
3863
3934
  try {
3864
3935
  // Generate wrangler configs first
@@ -3980,7 +4051,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3980
4051
  }
3981
4052
  scrollToBottom(log);
3982
4053
 
3983
- status.textContent = 'Complete';
4054
+ status.textContent = t('web.status.complete');
3984
4055
  status.className = 'status-badge status-success';
3985
4056
 
3986
4057
  // Show completion with setup URL and debug info
@@ -3995,7 +4066,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3995
4066
  } catch (error) {
3996
4067
  output.textContent += '\\n✗ Error: ' + error.message + '\\n';
3997
4068
  scrollToBottom(log);
3998
- status.textContent = 'Error';
4069
+ status.textContent = t('web.status.error');
3999
4070
  status.className = 'status-badge status-error';
4000
4071
  btn.disabled = false;
4001
4072
  }
@@ -4188,14 +4259,14 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4188
4259
  const btnSaveConfig = document.getElementById('btn-save-config-provision');
4189
4260
 
4190
4261
  if (provisioningCompleted) {
4191
- btnProvision.textContent = 'Re-provision (Delete & Create)';
4262
+ btnProvision.textContent = t('web.btn.reprovision');
4192
4263
  btnProvision.classList.remove('btn-primary');
4193
4264
  btnProvision.classList.add('btn-warning');
4194
4265
  btnProvision.disabled = false;
4195
4266
  btnGotoDeploy.classList.remove('hidden');
4196
4267
  btnSaveConfig.classList.remove('hidden');
4197
4268
  } else {
4198
- btnProvision.textContent = 'Create Resources';
4269
+ btnProvision.textContent = t('web.btn.createResources');
4199
4270
  btnProvision.classList.remove('btn-warning');
4200
4271
  btnProvision.classList.add('btn-primary');
4201
4272
  btnProvision.disabled = false;
@@ -4334,7 +4405,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4334
4405
  const output = document.getElementById('env-scan-output');
4335
4406
  const noEnvsMessage = document.getElementById('no-envs-message');
4336
4407
 
4337
- status.textContent = 'Scanning...';
4408
+ status.textContent = t('web.status.scanning');
4338
4409
  status.className = 'status-badge status-running';
4339
4410
  loading.classList.remove('hidden');
4340
4411
  content.classList.add('hidden');
@@ -4362,7 +4433,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4362
4433
  if (result.success) {
4363
4434
  detectedEnvironments = result.environments || [];
4364
4435
 
4365
- status.textContent = detectedEnvironments.length + ' found';
4436
+ status.textContent = t('web.status.found', { count: detectedEnvironments.length });
4366
4437
  status.className = 'status-badge status-success';
4367
4438
  loading.classList.add('hidden');
4368
4439
  content.classList.remove('hidden');
@@ -4373,7 +4444,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4373
4444
  }
4374
4445
  } catch (error) {
4375
4446
  clearInterval(pollInterval);
4376
- status.textContent = 'Error';
4447
+ status.textContent = t('web.status.error');
4377
4448
  status.className = 'status-badge status-error';
4378
4449
  output.textContent += '\\n❌ Error: ' + error.message;
4379
4450
  }
@@ -4468,7 +4539,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4468
4539
  const badge = document.createElement('span');
4469
4540
  badge.className = 'status-badge status-warning';
4470
4541
  badge.style.cssText = 'font-size: 0.75rem; padding: 0.125rem 0.5rem;';
4471
- badge.textContent = 'Admin未設定';
4542
+ badge.textContent = t('web.status.adminNotConfigured');
4472
4543
  badgeContainer.appendChild(badge);
4473
4544
  }
4474
4545
  }
@@ -4547,7 +4618,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4547
4618
  if (resources.length === 0) {
4548
4619
  const empty = document.createElement('div');
4549
4620
  empty.className = 'resource-empty';
4550
- empty.textContent = 'None';
4621
+ empty.textContent = t('web.status.none');
4551
4622
  list.appendChild(empty);
4552
4623
  return;
4553
4624
  }
@@ -4566,7 +4637,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4566
4637
  if (resourceType === 'd1' || resourceType === 'worker') {
4567
4638
  const detailsDiv = document.createElement('div');
4568
4639
  detailsDiv.className = 'resource-item-details resource-item-loading';
4569
- detailsDiv.textContent = 'Loading...';
4640
+ detailsDiv.textContent = t('web.status.loading');
4570
4641
  item.appendChild(detailsDiv);
4571
4642
  }
4572
4643
 
@@ -4618,7 +4689,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4618
4689
  }
4619
4690
  } else {
4620
4691
  detailsDiv.className = 'resource-item-details resource-item-error';
4621
- detailsDiv.textContent = 'Failed to load';
4692
+ detailsDiv.textContent = t('web.status.failedToLoad');
4622
4693
  }
4623
4694
  } catch (e) {
4624
4695
  console.error('Failed to load D1 details:', e);