@authrim/setup 0.1.85 → 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
@@ -221,14 +221,18 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
221
221
  /* ========================================
222
222
  THEME TOGGLE
223
223
  ======================================== */
224
- /* Language Selector - styled to match theme toggle */
225
- .lang-selector {
224
+ /* Top Controls Container - holds language selector and theme toggle */
225
+ .top-controls {
226
226
  position: fixed;
227
227
  top: 1.25rem;
228
- right: 4.5rem;
228
+ right: 1.5rem;
229
229
  z-index: 100;
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 0.75rem;
230
233
  }
231
234
 
235
+ /* Language Selector - styled to match theme toggle */
232
236
  .lang-selector select {
233
237
  height: 44px;
234
238
  padding: 0 2.25rem 0 1rem;
@@ -246,6 +250,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
246
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");
247
251
  background-repeat: no-repeat;
248
252
  background-position: right 0.75rem center;
253
+ box-shadow: var(--shadow-sm);
249
254
  }
250
255
 
251
256
  .lang-selector select:hover {
@@ -265,10 +270,6 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
265
270
  }
266
271
 
267
272
  .theme-toggle {
268
- position: fixed;
269
- top: 1.25rem;
270
- right: 1.5rem;
271
- z-index: 100;
272
273
  width: 44px;
273
274
  height: 44px;
274
275
  background: var(--card-bg);
@@ -1700,9 +1701,9 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1700
1701
  </style>
1701
1702
  <script>
1702
1703
  // i18n Translation System
1703
- const _translations = ${translationsJson};
1704
+ let _translations = ${translationsJson};
1704
1705
  const _availableLocales = ${availableLocalesJson};
1705
- const _currentLocale = '${locale}';
1706
+ let _currentLocale = '${locale}';
1706
1707
 
1707
1708
  /**
1708
1709
  * Translate a key with optional parameter substitution
@@ -1721,25 +1722,72 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1721
1722
  }
1722
1723
 
1723
1724
  /**
1724
- * 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
1725
1749
  * @param {string} locale - Locale code (e.g., 'en', 'ja')
1726
1750
  */
1727
- function changeLanguage(locale) {
1728
- localStorage.setItem('authrim_setup_lang', locale);
1729
- const url = new URL(window.location.href);
1730
- url.searchParams.set('lang', locale);
1731
- 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
+ }
1732
1780
  }
1733
1781
 
1734
- // Restore language from localStorage on page load
1782
+ // Restore language from localStorage on page load (only if no query param)
1735
1783
  (function() {
1736
1784
  const savedLang = localStorage.getItem('authrim_setup_lang');
1737
- if (savedLang && savedLang !== _currentLocale) {
1738
- const url = new URL(window.location.href);
1739
- if (!url.searchParams.has('lang')) {
1740
- url.searchParams.set('lang', savedLang);
1741
- window.location.href = url.toString();
1742
- }
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);
1743
1791
  }
1744
1792
  })();
1745
1793
  </script>
@@ -1757,16 +1805,16 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1757
1805
  </div>
1758
1806
  </div>
1759
1807
 
1760
- <!-- Language Selector -->
1761
- <div id="lang-selector" class="lang-selector">
1762
- <select id="lang-select" onchange="changeLanguage(this.value)" aria-label="Select language">
1763
- ${localeOptionsHtml}
1764
- </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>
1765
1816
  </div>
1766
1817
 
1767
- <!-- Theme Toggle -->
1768
- <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">🌙</button>
1769
-
1770
1818
  <div class="container">
1771
1819
  <header>
1772
1820
  <h1>Authrim</h1>
@@ -1795,132 +1843,132 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1795
1843
  <!-- Step 1: Prerequisites -->
1796
1844
  <div id="section-prerequisites" class="card">
1797
1845
  <h2 class="card-title">
1798
- Prerequisites
1799
- <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>
1800
1848
  </h2>
1801
1849
  <div id="prereq-content">
1802
- <p>Checking system requirements...</p>
1850
+ <p data-i18n="web.prereq.checkingRequirements">Checking system requirements...</p>
1803
1851
  </div>
1804
1852
  </div>
1805
1853
 
1806
1854
  <!-- Step 1.5: Top Menu (New Setup / Load Config / Manage) -->
1807
1855
  <div id="section-top-menu" class="card hidden">
1808
- <h2 class="card-title">Get Started</h2>
1809
- <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>
1810
1858
 
1811
1859
  <div class="mode-cards" style="grid-template-columns: repeat(3, 1fr);">
1812
1860
  <div class="mode-card" id="menu-new-setup">
1813
1861
  <div class="mode-icon">🆕</div>
1814
- <h3>New Setup</h3>
1815
- <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>
1816
1864
  </div>
1817
1865
 
1818
1866
  <div class="mode-card" id="menu-load-config">
1819
1867
  <div class="mode-icon">📂</div>
1820
- <h3>Load Config</h3>
1821
- <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>
1822
1870
  </div>
1823
1871
 
1824
1872
  <div class="mode-card" id="menu-manage-env">
1825
1873
  <div class="mode-icon">⚙️</div>
1826
- <h3>Manage Environments</h3>
1827
- <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>
1828
1876
  </div>
1829
1877
  </div>
1830
1878
  </div>
1831
1879
 
1832
1880
  <!-- Step 1.6: Setup Mode Selection (Quick / Custom) -->
1833
1881
  <div id="section-mode" class="card hidden">
1834
- <h2 class="card-title">Setup Mode</h2>
1835
- <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>
1836
1884
 
1837
1885
  <div class="mode-cards">
1838
1886
  <div class="mode-card" id="mode-quick">
1839
1887
  <div class="mode-icon">⚡</div>
1840
- <h3>Quick Setup</h3>
1841
- <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>
1842
1890
  <ul>
1843
- <li>Environment selection</li>
1844
- <li>Optional custom domain</li>
1845
- <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>
1846
1894
  </ul>
1847
- <span class="mode-badge">Recommended</span>
1895
+ <span class="mode-badge" data-i18n="web.mode.recommended">Recommended</span>
1848
1896
  </div>
1849
1897
 
1850
1898
  <div class="mode-card" id="mode-custom">
1851
1899
  <div class="mode-icon">🔧</div>
1852
- <h3>Custom Setup</h3>
1853
- <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>
1854
1902
  <ul>
1855
- <li>Component selection</li>
1856
- <li>URL configuration</li>
1857
- <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>
1858
1906
  </ul>
1859
1907
  </div>
1860
1908
  </div>
1861
1909
 
1862
1910
  <div class="button-group">
1863
- <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>
1864
1912
  </div>
1865
1913
  </div>
1866
1914
 
1867
1915
  <!-- Step 1.7: Load Config -->
1868
1916
  <div id="section-load-config" class="card hidden">
1869
- <h2 class="card-title">Load Configuration</h2>
1870
- <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>
1871
1919
 
1872
1920
  <div class="form-group">
1873
1921
  <div class="file-input-wrapper">
1874
- <span class="file-input-btn">📁 Choose File</span>
1922
+ <span class="file-input-btn" data-i18n="web.loadConfig.chooseFile">📁 Choose File</span>
1875
1923
  <input type="file" id="config-file" accept=".json">
1876
1924
  </div>
1877
1925
  <span id="config-file-name" style="margin-left: 1rem; color: var(--text-muted);"></span>
1878
1926
  </div>
1879
1927
 
1880
1928
  <div id="config-preview-section" class="hidden">
1881
- <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>
1882
1930
  <div class="config-preview" id="config-preview-content"></div>
1883
1931
  </div>
1884
1932
 
1885
1933
  <div id="config-validation-error" class="hidden" style="margin-top: 1rem; padding: 1rem; background: #fee2e2; border: 1px solid #fca5a5; border-radius: 8px;">
1886
1934
  <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
1887
1935
  <span style="font-size: 1.25rem;">⚠️</span>
1888
- <strong style="color: #b91c1c;">Configuration Validation Failed</strong>
1936
+ <strong style="color: #b91c1c;" data-i18n="web.loadConfig.validationFailed">Configuration Validation Failed</strong>
1889
1937
  </div>
1890
1938
  <ul id="config-validation-errors" style="margin: 0; padding-left: 1.5rem; color: #991b1b; font-size: 0.875rem;"></ul>
1891
1939
  </div>
1892
1940
 
1893
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;">
1894
- <span style="color: #065f46;">✓ Configuration is valid</span>
1942
+ <span style="color: #065f46;" data-i18n="web.loadConfig.valid">✓ Configuration is valid</span>
1895
1943
  </div>
1896
1944
 
1897
1945
  <div class="button-group">
1898
- <button class="btn-secondary" id="btn-back-top-2">Back</button>
1899
- <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>
1900
1948
  </div>
1901
1949
  </div>
1902
1950
 
1903
1951
  <!-- Step 2: Configuration -->
1904
1952
  <div id="section-config" class="card hidden">
1905
- <h2 class="card-title">Configuration</h2>
1953
+ <h2 class="card-title" data-i18n="web.config.title">Configuration</h2>
1906
1954
 
1907
1955
  <!-- 1. Components (shown in custom mode) -->
1908
1956
  <div id="advanced-options" class="hidden">
1909
- <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>
1910
1958
 
1911
1959
  <!-- API Component (required) -->
1912
1960
  <div class="component-card">
1913
1961
  <label class="checkbox-item" style="font-weight: 600; margin-bottom: 0.25rem;">
1914
1962
  <input type="checkbox" id="comp-api" checked disabled>
1915
- 🔐 API (required)
1963
+ 🔐 <span data-i18n="web.config.apiRequired">API (required)</span>
1916
1964
  </label>
1917
- <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">
1918
1966
  OIDC Provider endpoints: authorize, token, userinfo, discovery, management APIs.
1919
1967
  </p>
1920
1968
  <div style="margin-left: 1.5rem; display: flex; flex-wrap: wrap; gap: 0.75rem;">
1921
1969
  <label class="checkbox-item" style="font-size: 0.9rem;">
1922
1970
  <input type="checkbox" id="comp-saml">
1923
- SAML IdP
1971
+ <span data-i18n="web.config.saml">SAML IdP</span>
1924
1972
  </label>
1925
1973
  <label class="checkbox-item" style="font-size: 0.9rem;">
1926
1974
  <input type="checkbox" id="comp-async">
@@ -1937,9 +1985,9 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1937
1985
  <div class="component-card">
1938
1986
  <label class="checkbox-item" style="font-weight: 600; margin-bottom: 0.25rem;">
1939
1987
  <input type="checkbox" id="comp-login-ui" checked>
1940
- 🖥️ Login UI
1988
+ 🖥️ <span data-i18n="web.comp.loginUi">Login UI</span>
1941
1989
  </label>
1942
- <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">
1943
1991
  User-facing login, registration, consent, and account management pages.
1944
1992
  </p>
1945
1993
  </div>
@@ -1948,9 +1996,9 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1948
1996
  <div class="component-card">
1949
1997
  <label class="checkbox-item" style="font-weight: 600; margin-bottom: 0.25rem;">
1950
1998
  <input type="checkbox" id="comp-admin-ui" checked>
1951
- ⚙️ Admin UI
1999
+ ⚙️ <span data-i18n="web.comp.adminUi">Admin UI</span>
1952
2000
  </label>
1953
- <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">
1954
2002
  Admin dashboard for managing tenants, clients, users, and system settings.
1955
2003
  </p>
1956
2004
  </div>
@@ -1960,28 +2008,28 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1960
2008
 
1961
2009
  <!-- 2. Environment Name -->
1962
2010
  <div class="form-group">
1963
- <label for="env">Environment Name <span style="color: var(--error);">*</span></label>
1964
- <input type="text" id="env" placeholder="e.g., prod, staging, dev" required>
1965
- <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>
1966
2014
  </div>
1967
2015
 
1968
2016
  <!-- 3. Domain Configuration -->
1969
2017
  <!-- 3.1 API / Issuer Domain -->
1970
2018
  <div class="domain-section">
1971
- <h4>🌐 API / Issuer Domain</h4>
2019
+ <h4>🌐 <span data-i18n="web.section.apiDomain">API / Issuer Domain</span></h4>
1972
2020
 
1973
2021
  <div class="form-group" style="margin-bottom: 0.75rem;">
1974
- <label for="base-domain">Base Domain (API Domain)</label>
1975
- <input type="text" id="base-domain" placeholder="oidc.example.com">
1976
- <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>
1977
2025
  <label class="checkbox-item" id="naked-domain-label" style="display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem;">
1978
2026
  <input type="checkbox" id="naked-domain">
1979
- <span>Exclude tenant name from URL</span>
2027
+ <span data-i18n="web.form.nakedDomain">Exclude tenant name from URL</span>
1980
2028
  </label>
1981
- <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">
1982
2030
  Use https://example.com instead of https://{tenant}.example.com
1983
2031
  </small>
1984
- <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">
1985
2033
  ⚠️ Tenant subdomains require a custom domain. Workers.dev does not support wildcard subdomains.
1986
2034
  </small>
1987
2035
  </div>
@@ -1989,146 +2037,146 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
1989
2037
  <!-- Default Tenant (hidden when naked domain is checked or using workers.dev) -->
1990
2038
  <div id="tenant-fields">
1991
2039
  <div class="form-group" style="margin-bottom: 0.5rem;">
1992
- <label for="tenant-name">Default Tenant ID</label>
1993
- <input type="text" id="tenant-name" placeholder="default" value="default">
1994
- <small style="color: var(--text-muted)">First tenant identifier (lowercase, no spaces)</small>
1995
- <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">
1996
2044
  (Tenant ID is used internally. URL subdomain requires custom domain.)
1997
2045
  </small>
1998
2046
  </div>
1999
2047
  </div>
2000
2048
 
2001
2049
  <div class="form-group" style="margin-bottom: 0;">
2002
- <label for="tenant-display">Tenant Display Name</label>
2003
- <input type="text" id="tenant-display" placeholder="My Company" value="Default Tenant">
2004
- <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>
2005
2053
  </div>
2006
2054
  </div>
2007
2055
 
2008
2056
  <!-- 3.2 UI Domains -->
2009
2057
  <div class="domain-section" id="ui-domains-section">
2010
- <h4>🖥️ UI Domains (Optional)</h4>
2011
- <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">
2012
2060
  Custom domains for Login/Admin UIs. Each can be set independently.
2013
2061
  Leave empty to use Cloudflare Pages default.
2014
2062
  </div>
2015
2063
 
2016
2064
  <div class="domain-row" id="login-domain-row">
2017
- <span class="domain-label">Login UI</span>
2065
+ <span class="domain-label" data-i18n="web.domain.loginUi">Login UI</span>
2018
2066
  <div class="domain-input-wrapper">
2019
- <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">
2020
2068
  <span class="domain-default" id="login-default">{env}-ar-ui.pages.dev</span>
2021
2069
  </div>
2022
2070
  </div>
2023
2071
 
2024
2072
  <div class="domain-row" id="admin-domain-row">
2025
- <span class="domain-label">Admin UI</span>
2073
+ <span class="domain-label" data-i18n="web.domain.adminUi">Admin UI</span>
2026
2074
  <div class="domain-input-wrapper">
2027
- <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">
2028
2076
  <span class="domain-default" id="admin-default">{env}-ar-ui.pages.dev/admin</span>
2029
2077
  </div>
2030
2078
  </div>
2031
2079
 
2032
- <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">
2033
2081
  💡 CORS: Cross-origin requests from Login/Admin UI to API are automatically allowed.
2034
2082
  </div>
2035
2083
  </div>
2036
2084
 
2037
2085
  <!-- 4. Preview Section (at the bottom) -->
2038
2086
  <div class="infra-section" id="config-preview">
2039
- <h4>📋 Configuration Preview</h4>
2087
+ <h4>📋 <span data-i18n="web.section.configPreview">Configuration Preview</span></h4>
2040
2088
  <div class="infra-item">
2041
- <span class="infra-label">Components:</span>
2089
+ <span class="infra-label" data-i18n="web.preview.components">Components:</span>
2042
2090
  <span class="infra-value" id="preview-components">API, Login UI, Admin UI</span>
2043
2091
  </div>
2044
2092
  <div class="infra-item">
2045
- <span class="infra-label">Workers:</span>
2093
+ <span class="infra-label" data-i18n="web.preview.workers">Workers:</span>
2046
2094
  <span class="infra-value" id="preview-workers">{env}-ar-router, {env}-ar-auth, ...</span>
2047
2095
  </div>
2048
2096
  <div class="infra-item">
2049
- <span class="infra-label">Issuer URL:</span>
2097
+ <span class="infra-label" data-i18n="web.preview.issuerUrl">Issuer URL:</span>
2050
2098
  <span class="infra-value" id="preview-issuer">https://{tenant}.{base-domain}</span>
2051
2099
  </div>
2052
2100
  <div class="infra-item">
2053
- <span class="infra-label">Login UI:</span>
2101
+ <span class="infra-label" data-i18n="web.preview.loginUi">Login UI:</span>
2054
2102
  <span class="infra-value" id="preview-login">{env}-ar-ui.pages.dev</span>
2055
2103
  </div>
2056
2104
  <div class="infra-item">
2057
- <span class="infra-label">Admin UI:</span>
2105
+ <span class="infra-label" data-i18n="web.preview.adminUi">Admin UI:</span>
2058
2106
  <span class="infra-value" id="preview-admin">{env}-ar-ui.pages.dev/admin</span>
2059
2107
  </div>
2060
2108
  </div>
2061
2109
 
2062
2110
  <div class="button-group">
2063
- <button class="btn-secondary" id="btn-back-mode">Back</button>
2064
- <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>
2065
2113
  </div>
2066
2114
  </div>
2067
2115
 
2068
2116
  <!-- Step 3: Database Configuration -->
2069
2117
  <div id="section-database" class="card hidden">
2070
- <h2 class="card-title">🗄️ Database Configuration</h2>
2118
+ <h2 class="card-title" data-i18n="web.db.title">🗄️ Database Configuration</h2>
2071
2119
 
2072
- <p style="margin-bottom: 1rem; color: var(--text-muted);">
2120
+ <p style="margin-bottom: 1rem; color: var(--text-muted);" data-i18n="web.db.introDesc">
2073
2121
  Authrim uses two separate D1 databases to isolate personal data from application data.
2074
2122
  </p>
2075
2123
 
2076
- <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">
2077
2125
  Note: Database region cannot be changed after creation.
2078
2126
  </p>
2079
2127
 
2080
2128
  <div class="database-config-stack">
2081
2129
  <!-- Core Database (Non-PII) -->
2082
2130
  <div class="database-card">
2083
- <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>
2084
2132
  <div class="db-description">
2085
- <p>Stores non-personal application data including:</p>
2133
+ <p data-i18n="web.db.coreDataDesc">Stores non-personal application data including:</p>
2086
2134
  <ul>
2087
- <li>OAuth clients and their configurations</li>
2088
- <li>Authorization codes and access tokens</li>
2089
- <li>User sessions and login state</li>
2090
- <li>Tenant settings and configurations</li>
2091
- <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>
2092
2140
  </ul>
2093
- <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>
2094
2142
  </div>
2095
2143
 
2096
2144
  <div class="region-selection">
2097
- <h4>Region</h4>
2145
+ <h4 data-i18n="web.db.region">Region</h4>
2098
2146
  <div class="radio-group">
2099
2147
  <label class="radio-item">
2100
2148
  <input type="radio" name="db-core-location" value="auto" checked>
2101
- <span>Automatic (nearest to you)</span>
2149
+ <span data-i18n="web.db.autoNearest">Automatic (nearest to you)</span>
2102
2150
  </label>
2103
- <div class="radio-separator">Location Hints</div>
2151
+ <div class="radio-separator" data-i18n="web.db.locationHints">Location Hints</div>
2104
2152
  <label class="radio-item">
2105
2153
  <input type="radio" name="db-core-location" value="wnam">
2106
- <span>North America (West)</span>
2154
+ <span data-i18n="web.db.northAmericaWest">North America (West)</span>
2107
2155
  </label>
2108
2156
  <label class="radio-item">
2109
2157
  <input type="radio" name="db-core-location" value="enam">
2110
- <span>North America (East)</span>
2158
+ <span data-i18n="web.db.northAmericaEast">North America (East)</span>
2111
2159
  </label>
2112
2160
  <label class="radio-item">
2113
2161
  <input type="radio" name="db-core-location" value="weur">
2114
- <span>Europe (West)</span>
2162
+ <span data-i18n="web.db.europeWest">Europe (West)</span>
2115
2163
  </label>
2116
2164
  <label class="radio-item">
2117
2165
  <input type="radio" name="db-core-location" value="eeur">
2118
- <span>Europe (East)</span>
2166
+ <span data-i18n="web.db.europeEast">Europe (East)</span>
2119
2167
  </label>
2120
2168
  <label class="radio-item">
2121
2169
  <input type="radio" name="db-core-location" value="apac">
2122
- <span>Asia Pacific</span>
2170
+ <span data-i18n="web.db.asiaPacific">Asia Pacific</span>
2123
2171
  </label>
2124
2172
  <label class="radio-item">
2125
2173
  <input type="radio" name="db-core-location" value="oc">
2126
- <span>Oceania</span>
2174
+ <span data-i18n="web.db.oceania">Oceania</span>
2127
2175
  </label>
2128
- <div class="radio-separator">Jurisdiction (Compliance)</div>
2176
+ <div class="radio-separator" data-i18n="web.db.jurisdiction">Jurisdiction (Compliance)</div>
2129
2177
  <label class="radio-item">
2130
2178
  <input type="radio" name="db-core-location" value="eu">
2131
- <span>EU Jurisdiction (GDPR compliance)</span>
2179
+ <span data-i18n="web.db.euJurisdiction">EU Jurisdiction (GDPR compliance)</span>
2132
2180
  </label>
2133
2181
  </div>
2134
2182
  </div>
@@ -2136,54 +2184,54 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2136
2184
 
2137
2185
  <!-- PII Database -->
2138
2186
  <div class="database-card">
2139
- <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>
2140
2188
  <div class="db-description">
2141
- <p>Stores personal user data including:</p>
2189
+ <p data-i18n="web.db.piiDataDesc">Stores personal user data including:</p>
2142
2190
  <ul>
2143
- <li>User profiles (name, email, phone)</li>
2144
- <li>Passkey/WebAuthn credentials</li>
2145
- <li>User preferences and settings</li>
2146
- <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>
2147
2195
  </ul>
2148
- <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>
2149
2197
  </div>
2150
2198
 
2151
2199
  <div class="region-selection">
2152
- <h4>Region</h4>
2200
+ <h4 data-i18n="web.db.region">Region</h4>
2153
2201
  <div class="radio-group">
2154
2202
  <label class="radio-item">
2155
2203
  <input type="radio" name="db-pii-location" value="auto" checked>
2156
- <span>Automatic (nearest to you)</span>
2204
+ <span data-i18n="web.db.autoNearest">Automatic (nearest to you)</span>
2157
2205
  </label>
2158
- <div class="radio-separator">Location Hints</div>
2206
+ <div class="radio-separator" data-i18n="web.db.locationHints">Location Hints</div>
2159
2207
  <label class="radio-item">
2160
2208
  <input type="radio" name="db-pii-location" value="wnam">
2161
- <span>North America (West)</span>
2209
+ <span data-i18n="web.db.northAmericaWest">North America (West)</span>
2162
2210
  </label>
2163
2211
  <label class="radio-item">
2164
2212
  <input type="radio" name="db-pii-location" value="enam">
2165
- <span>North America (East)</span>
2213
+ <span data-i18n="web.db.northAmericaEast">North America (East)</span>
2166
2214
  </label>
2167
2215
  <label class="radio-item">
2168
2216
  <input type="radio" name="db-pii-location" value="weur">
2169
- <span>Europe (West)</span>
2217
+ <span data-i18n="web.db.europeWest">Europe (West)</span>
2170
2218
  </label>
2171
2219
  <label class="radio-item">
2172
2220
  <input type="radio" name="db-pii-location" value="eeur">
2173
- <span>Europe (East)</span>
2221
+ <span data-i18n="web.db.europeEast">Europe (East)</span>
2174
2222
  </label>
2175
2223
  <label class="radio-item">
2176
2224
  <input type="radio" name="db-pii-location" value="apac">
2177
- <span>Asia Pacific</span>
2225
+ <span data-i18n="web.db.asiaPacific">Asia Pacific</span>
2178
2226
  </label>
2179
2227
  <label class="radio-item">
2180
2228
  <input type="radio" name="db-pii-location" value="oc">
2181
- <span>Oceania</span>
2229
+ <span data-i18n="web.db.oceania">Oceania</span>
2182
2230
  </label>
2183
- <div class="radio-separator">Jurisdiction (Compliance)</div>
2231
+ <div class="radio-separator" data-i18n="web.db.jurisdiction">Jurisdiction (Compliance)</div>
2184
2232
  <label class="radio-item">
2185
2233
  <input type="radio" name="db-pii-location" value="eu">
2186
- <span>EU Jurisdiction (GDPR compliance)</span>
2234
+ <span data-i18n="web.db.euJurisdiction">EU Jurisdiction (GDPR compliance)</span>
2187
2235
  </label>
2188
2236
  </div>
2189
2237
  </div>
@@ -2191,16 +2239,16 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2191
2239
  </div>
2192
2240
 
2193
2241
  <div class="button-group">
2194
- <button class="btn-secondary" id="btn-back-database">Back</button>
2195
- <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>
2196
2244
  </div>
2197
2245
  </div>
2198
2246
 
2199
2247
  <!-- Step 4: Email Provider Configuration -->
2200
2248
  <div id="section-email" class="card hidden">
2201
- <h2 class="card-title">📧 Email Provider</h2>
2249
+ <h2 class="card-title" data-i18n="web.email.title">📧 Email Provider</h2>
2202
2250
 
2203
- <p style="margin-bottom: 1rem; color: var(--text-muted);">
2251
+ <p style="margin-bottom: 1rem; color: var(--text-muted);" data-i18n="web.email.introDesc">
2204
2252
  Used for sending Mail OTP and email address verification.
2205
2253
  You can configure this later if you prefer.
2206
2254
  </p>
@@ -2209,88 +2257,90 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2209
2257
  <label class="radio-item" style="padding: 0.75rem; border: 1px solid var(--border); border-radius: 8px;">
2210
2258
  <input type="radio" name="email-setup-choice" value="later" checked>
2211
2259
  <span style="display: flex; flex-direction: column; gap: 0.25rem;">
2212
- <strong>Configure later</strong>
2213
- <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>
2214
2262
  </span>
2215
2263
  </label>
2216
2264
  <label class="radio-item" style="padding: 0.75rem; border: 1px solid var(--border); border-radius: 8px; margin-top: 0.5rem;">
2217
2265
  <input type="radio" name="email-setup-choice" value="configure">
2218
2266
  <span style="display: flex; flex-direction: column; gap: 0.25rem;">
2219
- <strong>Configure Resend</strong>
2220
- <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>
2221
2269
  </span>
2222
2270
  </label>
2223
2271
  </div>
2224
2272
 
2225
2273
  <!-- Resend Configuration Form (hidden by default) -->
2226
2274
  <div id="resend-config-form" class="hidden" style="background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem;">
2227
- <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>
2228
2276
 
2229
2277
  <div class="alert alert-info" style="margin-bottom: 1rem;">
2230
- <strong>📋 Before you begin:</strong>
2278
+ <strong>📋 <span data-i18n="web.email.beforeBegin">Before you begin:</span></strong>
2231
2279
  <ol style="margin: 0.5rem 0 0 1rem; padding: 0;">
2232
- <li>Create a Resend account at <a href="https://resend.com" target="_blank" style="color: var(--primary);">resend.com</a></li>
2233
- <li>Add and verify your domain at <a href="https://resend.com/domains" target="_blank" style="color: var(--primary);">Domains Dashboard</a></li>
2234
- <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>
2235
2283
  </ol>
2236
2284
  </div>
2237
2285
 
2238
2286
  <div class="form-group">
2239
- <label for="resend-api-key">Resend API Key</label>
2287
+ <label for="resend-api-key" data-i18n="web.email.resendApiKey">Resend API Key</label>
2240
2288
  <input type="password" id="resend-api-key" placeholder="re_xxxxxxxxxx" autocomplete="off">
2241
- <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>
2242
2290
  </div>
2243
2291
 
2244
2292
  <div class="form-group">
2245
- <label for="email-from-address">From Email Address</label>
2293
+ <label for="email-from-address" data-i18n="web.email.fromEmailAddress">From Email Address</label>
2246
2294
  <input type="email" id="email-from-address" placeholder="noreply@yourdomain.com" autocomplete="off">
2247
- <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>
2248
2296
  </div>
2249
2297
 
2250
2298
  <div class="form-group">
2251
- <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>
2252
2300
  <input type="text" id="email-from-name" placeholder="Authrim" autocomplete="off">
2253
- <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>
2254
2302
  </div>
2255
2303
 
2256
2304
  <div class="alert alert-warning" style="margin-top: 1rem;">
2257
- <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>
2258
2309
  <p style="margin: 0.25rem 0 0 0; font-size: 0.875rem;">
2259
- Before your domain is verified, emails can only be sent from <code>onboarding@resend.dev</code> (for testing).
2260
- <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>
2261
2311
  </p>
2262
2312
  </div>
2263
2313
  </div>
2264
2314
 
2265
2315
  <div class="button-group">
2266
- <button class="btn-secondary" id="btn-back-email">Back</button>
2267
- <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>
2268
2318
  </div>
2269
2319
  </div>
2270
2320
 
2271
2321
  <!-- Step 5: Provisioning -->
2272
2322
  <div id="section-provision" class="card hidden">
2273
2323
  <h2 class="card-title">
2274
- Resource Provisioning
2275
- <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>
2276
2326
  </h2>
2277
2327
 
2278
- <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>
2279
2329
 
2280
2330
  <!-- Resource names preview -->
2281
2331
  <div id="resource-preview" class="resource-preview">
2282
- <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>
2283
2333
  <div class="resource-list">
2284
2334
  <div class="resource-category">
2285
- <strong>D1 Databases:</strong>
2335
+ <strong data-i18n="web.provision.d1Databases">D1 Databases:</strong>
2286
2336
  <ul id="preview-d1"></ul>
2287
2337
  </div>
2288
2338
  <div class="resource-category">
2289
- <strong>KV Namespaces:</strong>
2339
+ <strong data-i18n="web.provision.kvNamespaces">KV Namespaces:</strong>
2290
2340
  <ul id="preview-kv"></ul>
2291
2341
  </div>
2292
2342
  <div class="resource-category">
2293
- <strong>Cryptographic Keys:</strong>
2343
+ <strong data-i18n="web.provision.cryptoKeys">Cryptographic Keys:</strong>
2294
2344
  <ul id="preview-keys"></ul>
2295
2345
  </div>
2296
2346
  </div>
@@ -2300,7 +2350,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2300
2350
  <div id="provision-progress-ui" class="progress-container hidden">
2301
2351
  <div class="progress-status">
2302
2352
  <div class="spinner" id="provision-spinner"></div>
2303
- <span id="provision-current-task">Initializing...</span>
2353
+ <span id="provision-current-task" data-i18n="web.provision.initializing">Initializing...</span>
2304
2354
  </div>
2305
2355
  <div class="progress-bar-wrapper">
2306
2356
  <div class="progress-bar" id="provision-progress-bar" style="width: 0%"></div>
@@ -2309,7 +2359,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2309
2359
 
2310
2360
  <div class="log-toggle" id="provision-log-toggle">
2311
2361
  <span class="arrow">▶</span>
2312
- <span>Show detailed log</span>
2362
+ <span data-i18n="web.provision.showLog">Show detailed log</span>
2313
2363
  </div>
2314
2364
  </div>
2315
2365
 
@@ -2319,35 +2369,35 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2319
2369
 
2320
2370
  <!-- Keys saved location (shown after completion) -->
2321
2371
  <div id="keys-saved-info" class="alert alert-info hidden" style="margin-top: 1rem;">
2322
- <strong>🔑 Keys saved to:</strong>
2372
+ <strong>🔑 <span data-i18n="web.provision.keysSavedTo">Keys saved to:</span></strong>
2323
2373
  <code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: #f1f5f9; border-radius: 4px;" id="keys-path"></code>
2324
- <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">
2325
2375
  ⚠️ Keep this directory safe and add it to .gitignore
2326
2376
  </p>
2327
2377
  </div>
2328
2378
 
2329
2379
  <div class="button-group">
2330
- <button class="btn-secondary" id="btn-back-config">Back</button>
2331
- <button class="btn-primary" id="btn-provision">Create Resources</button>
2332
- <button class="btn-secondary hidden" id="btn-save-config-provision" title="Save configuration to file">💾 Save Config</button>
2333
- <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>
2334
2384
  </div>
2335
2385
  </div>
2336
2386
 
2337
2387
  <!-- Step 4: Deployment -->
2338
2388
  <div id="section-deploy" class="card hidden">
2339
2389
  <h2 class="card-title">
2340
- Deployment
2341
- <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>
2342
2392
  </h2>
2343
2393
 
2344
- <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>
2345
2395
 
2346
2396
  <!-- Progress UI (shown during deployment) -->
2347
2397
  <div id="deploy-progress-ui" class="progress-container hidden">
2348
2398
  <div class="progress-status">
2349
2399
  <div class="spinner" id="deploy-spinner"></div>
2350
- <span id="deploy-current-task">Initializing...</span>
2400
+ <span id="deploy-current-task" data-i18n="web.provision.initializing">Initializing...</span>
2351
2401
  </div>
2352
2402
  <div class="progress-bar-wrapper">
2353
2403
  <div class="progress-bar" id="deploy-progress-bar" style="width: 0%"></div>
@@ -2356,7 +2406,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2356
2406
 
2357
2407
  <div class="log-toggle" id="deploy-log-toggle">
2358
2408
  <span class="arrow">▶</span>
2359
- <span>Show detailed log</span>
2409
+ <span data-i18n="web.provision.showLog">Show detailed log</span>
2360
2410
  </div>
2361
2411
  </div>
2362
2412
 
@@ -2365,50 +2415,50 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2365
2415
  </div>
2366
2416
 
2367
2417
  <div class="button-group">
2368
- <button class="btn-secondary" id="btn-back-provision">Back</button>
2369
- <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>
2370
2420
  </div>
2371
2421
  </div>
2372
2422
 
2373
2423
  <!-- Complete -->
2374
2424
  <div id="section-complete" class="card hidden">
2375
- <h2 class="card-title" style="color: var(--success);">
2425
+ <h2 class="card-title" style="color: var(--success);" data-i18n="web.complete.title">
2376
2426
  ✅ Setup Complete!
2377
2427
  </h2>
2378
2428
 
2379
- <p>Authrim has been successfully deployed.</p>
2429
+ <p data-i18n="web.complete.desc">Authrim has been successfully deployed.</p>
2380
2430
 
2381
2431
  <div class="url-display" id="urls">
2382
2432
  <!-- URLs will be inserted here -->
2383
2433
  </div>
2384
2434
 
2385
2435
  <div class="alert alert-info" style="margin-top: 1rem;">
2386
- <strong>Next Steps:</strong>
2436
+ <strong data-i18n="web.complete.nextSteps">Next Steps:</strong>
2387
2437
  <ol style="margin-left: 1.5rem; margin-top: 0.5rem;">
2388
- <li>Visit the <strong>Admin Setup</strong> URL above to register your first admin with Passkey</li>
2389
- <li>Log in to the Admin UI to create OAuth clients</li>
2390
- <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>
2391
2441
  </ol>
2392
2442
  </div>
2393
2443
 
2394
2444
  <div class="button-group" style="margin-top: 1.5rem; justify-content: center;">
2395
- <button class="btn-secondary" id="btn-save-config-complete" title="Save configuration to file">💾 Save Configuration</button>
2396
- <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>
2397
2447
  </div>
2398
2448
 
2399
2449
  <p style="text-align: center; margin-top: 1.5rem; color: var(--text-muted); font-size: 0.9rem;">
2400
- ✅ 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>
2401
2451
  </p>
2402
2452
  </div>
2403
2453
 
2404
2454
  <!-- Environment Management: List -->
2405
2455
  <div id="section-env-list" class="card hidden">
2406
2456
  <h2 class="card-title">
2407
- Manage Environments
2408
- <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>
2409
2459
  </h2>
2410
2460
 
2411
- <p style="margin-bottom: 1rem; color: var(--text-muted);">
2461
+ <p style="margin-bottom: 1rem; color: var(--text-muted);" data-i18n="web.env.detectedDesc">
2412
2462
  Detected Authrim environments in your Cloudflare account:
2413
2463
  </p>
2414
2464
 
@@ -2421,21 +2471,21 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2421
2471
  <!-- Environment cards will be inserted here -->
2422
2472
  </div>
2423
2473
 
2424
- <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">
2425
2475
  No Authrim environments detected in this Cloudflare account.
2426
2476
  </div>
2427
2477
  </div>
2428
2478
 
2429
2479
  <div class="button-group">
2430
- <button class="btn-secondary" id="btn-back-env-list">Back</button>
2431
- <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>
2432
2482
  </div>
2433
2483
  </div>
2434
2484
 
2435
2485
  <!-- Environment Management: Details -->
2436
2486
  <div id="section-env-detail" class="card hidden">
2437
2487
  <h2 class="card-title">
2438
- 📋 Environment Details
2488
+ 📋 <span data-i18n="web.envDetail.title">Environment Details</span>
2439
2489
  <code id="detail-env-name" style="background: var(--bg); padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 1rem;"></code>
2440
2490
  </h2>
2441
2491
 
@@ -2444,23 +2494,23 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2444
2494
  <div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem;">
2445
2495
  <span style="font-size: 1.5rem;">⚠️</span>
2446
2496
  <div>
2447
- <div style="font-weight: 600;">Admin Account Not Configured</div>
2448
- <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>
2449
2499
  </div>
2450
2500
  </div>
2451
- <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">
2452
2502
  🔐 Start Admin Account Setup with Passkey
2453
2503
  </button>
2454
2504
  <div id="admin-setup-result" class="hidden" style="margin-top: 1rem; padding: 0.75rem; background: var(--card-bg); border-radius: 6px;">
2455
- <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>
2456
2506
  <div style="display: flex; gap: 0.5rem; align-items: center;">
2457
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);">
2458
- <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>
2459
2509
  </div>
2460
2510
  <div style="text-align: center; margin-top: 1rem;">
2461
- <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>
2462
2512
  </div>
2463
- <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">
2464
2514
  This URL is valid for 1 hour. Open it in a browser to register the first admin account.
2465
2515
  </div>
2466
2516
  </div>
@@ -2470,7 +2520,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2470
2520
  <!-- Workers -->
2471
2521
  <div class="resource-section">
2472
2522
  <div class="resource-section-title">
2473
- 🔧 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>
2474
2524
  </div>
2475
2525
  <div class="resource-list" id="detail-workers-list"></div>
2476
2526
  </div>
@@ -2478,7 +2528,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2478
2528
  <!-- D1 Databases -->
2479
2529
  <div class="resource-section">
2480
2530
  <div class="resource-section-title">
2481
- 📊 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>
2482
2532
  </div>
2483
2533
  <div class="resource-list" id="detail-d1-list"></div>
2484
2534
  </div>
@@ -2486,7 +2536,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2486
2536
  <!-- KV Namespaces -->
2487
2537
  <div class="resource-section">
2488
2538
  <div class="resource-section-title">
2489
- 🗄️ 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>
2490
2540
  </div>
2491
2541
  <div class="resource-list" id="detail-kv-list"></div>
2492
2542
  </div>
@@ -2494,7 +2544,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2494
2544
  <!-- Queues -->
2495
2545
  <div class="resource-section" id="detail-queues-section">
2496
2546
  <div class="resource-section-title">
2497
- 📨 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>
2498
2548
  </div>
2499
2549
  <div class="resource-list" id="detail-queues-list"></div>
2500
2550
  </div>
@@ -2502,7 +2552,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2502
2552
  <!-- R2 Buckets -->
2503
2553
  <div class="resource-section" id="detail-r2-section">
2504
2554
  <div class="resource-section-title">
2505
- 📁 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>
2506
2556
  </div>
2507
2557
  <div class="resource-list" id="detail-r2-list"></div>
2508
2558
  </div>
@@ -2510,41 +2560,41 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2510
2560
  <!-- Pages Projects -->
2511
2561
  <div class="resource-section" id="detail-pages-section">
2512
2562
  <div class="resource-section-title">
2513
- 📄 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>
2514
2564
  </div>
2515
2565
  <div class="resource-list" id="detail-pages-list"></div>
2516
2566
  </div>
2517
2567
  </div>
2518
2568
 
2519
2569
  <div class="button-group">
2520
- <button class="btn-secondary" id="btn-back-env-detail">← Back to List</button>
2521
- <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>
2522
2572
  </div>
2523
2573
  </div>
2524
2574
 
2525
2575
  <!-- Environment Management: Delete Confirmation -->
2526
2576
  <div id="section-env-delete" class="card hidden">
2527
2577
  <h2 class="card-title" style="color: var(--error);">
2528
- ⚠️ Delete Environment
2578
+ ⚠️ <span data-i18n="web.delete.title">Delete Environment</span>
2529
2579
  </h2>
2530
2580
 
2531
2581
  <div class="alert alert-warning">
2532
- <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>
2533
2583
  </div>
2534
2584
 
2535
2585
  <div style="margin: 1.5rem 0;">
2536
2586
  <h3 style="font-size: 1.1rem; margin-bottom: 1rem;">
2537
- 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>
2538
2588
  </h3>
2539
2589
 
2540
2590
  <div id="delete-options-section">
2541
- <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>
2542
2592
 
2543
2593
  <div class="delete-options">
2544
2594
  <label class="checkbox-item delete-option">
2545
2595
  <input type="checkbox" id="delete-workers" checked>
2546
2596
  <span>
2547
- <strong>Workers</strong>
2597
+ <strong data-i18n="web.delete.workers">Workers</strong>
2548
2598
  <small id="delete-workers-count">(0 workers)</small>
2549
2599
  </span>
2550
2600
  </label>
@@ -2552,7 +2602,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2552
2602
  <label class="checkbox-item delete-option">
2553
2603
  <input type="checkbox" id="delete-d1" checked>
2554
2604
  <span>
2555
- <strong>D1 Databases</strong>
2605
+ <strong data-i18n="web.delete.d1Databases">D1 Databases</strong>
2556
2606
  <small id="delete-d1-count">(0 databases)</small>
2557
2607
  </span>
2558
2608
  </label>
@@ -2560,7 +2610,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2560
2610
  <label class="checkbox-item delete-option">
2561
2611
  <input type="checkbox" id="delete-kv" checked>
2562
2612
  <span>
2563
- <strong>KV Namespaces</strong>
2613
+ <strong data-i18n="web.delete.kvNamespaces">KV Namespaces</strong>
2564
2614
  <small id="delete-kv-count">(0 namespaces)</small>
2565
2615
  </span>
2566
2616
  </label>
@@ -2568,7 +2618,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2568
2618
  <label class="checkbox-item delete-option">
2569
2619
  <input type="checkbox" id="delete-queues" checked>
2570
2620
  <span>
2571
- <strong>Queues</strong>
2621
+ <strong data-i18n="web.delete.queues">Queues</strong>
2572
2622
  <small id="delete-queues-count">(0 queues)</small>
2573
2623
  </span>
2574
2624
  </label>
@@ -2576,7 +2626,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2576
2626
  <label class="checkbox-item delete-option">
2577
2627
  <input type="checkbox" id="delete-r2" checked>
2578
2628
  <span>
2579
- <strong>R2 Buckets</strong>
2629
+ <strong data-i18n="web.delete.r2Buckets">R2 Buckets</strong>
2580
2630
  <small id="delete-r2-count">(0 buckets)</small>
2581
2631
  </span>
2582
2632
  </label>
@@ -2584,7 +2634,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2584
2634
  <label class="checkbox-item delete-option">
2585
2635
  <input type="checkbox" id="delete-pages" checked>
2586
2636
  <span>
2587
- <strong>Pages Projects</strong>
2637
+ <strong data-i18n="web.delete.pagesProjects">Pages Projects</strong>
2588
2638
  <small id="delete-pages-count">(0 projects)</small>
2589
2639
  </span>
2590
2640
  </label>
@@ -2596,7 +2646,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2596
2646
  <div id="delete-progress-ui" class="progress-container hidden">
2597
2647
  <div class="progress-status">
2598
2648
  <div class="spinner" id="delete-spinner"></div>
2599
- <span id="delete-current-task">Initializing...</span>
2649
+ <span id="delete-current-task" data-i18n="web.provision.initializing">Initializing...</span>
2600
2650
  </div>
2601
2651
  <div class="progress-bar-wrapper">
2602
2652
  <div class="progress-bar" id="delete-progress-bar" style="width: 0%"></div>
@@ -2605,7 +2655,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2605
2655
 
2606
2656
  <div class="log-toggle" id="delete-log-toggle">
2607
2657
  <span class="arrow">▶</span>
2608
- <span>Show detailed log</span>
2658
+ <span data-i18n="web.provision.showLog">Show detailed log</span>
2609
2659
  </div>
2610
2660
  </div>
2611
2661
 
@@ -2616,8 +2666,8 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2616
2666
  <div id="delete-result" class="hidden"></div>
2617
2667
 
2618
2668
  <div class="button-group">
2619
- <button class="btn-secondary" id="btn-back-env-delete">Cancel</button>
2620
- <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>
2621
2671
  </div>
2622
2672
  </div>
2623
2673
  </div>
@@ -2626,14 +2676,16 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2626
2676
  <div id="save-config-modal" class="modal hidden">
2627
2677
  <div class="modal-backdrop"></div>
2628
2678
  <div class="modal-content">
2629
- <h3 style="margin: 0 0 1rem 0;">💾 Save Configuration?</h3>
2630
- <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">
2631
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">
2632
2684
  This allows you to resume setup later or use the same settings for another deployment.
2633
2685
  </p>
2634
2686
  <div class="button-group" style="justify-content: flex-end;">
2635
- <button class="btn-secondary" id="modal-skip-save">Skip</button>
2636
- <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>
2637
2689
  </div>
2638
2690
  </div>
2639
2691
  </div>
@@ -2794,11 +2846,11 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2794
2846
  if (isHidden) {
2795
2847
  log.classList.remove('hidden');
2796
2848
  toggle.classList.add('open');
2797
- toggle.querySelector('span:last-child').textContent = 'Hide detailed log';
2849
+ toggle.querySelector('span:last-child').textContent = t('web.provision.hideLog');
2798
2850
  } else {
2799
2851
  log.classList.add('hidden');
2800
2852
  toggle.classList.remove('open');
2801
- toggle.querySelector('span:last-child').textContent = 'Show detailed log';
2853
+ toggle.querySelector('span:last-child').textContent = t('web.provision.showLog');
2802
2854
  }
2803
2855
  });
2804
2856
  }
@@ -2938,18 +2990,18 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2938
2990
  prereqContent.textContent = '';
2939
2991
 
2940
2992
  if (!result.wranglerInstalled) {
2941
- prereqStatus.textContent = 'Error';
2993
+ prereqStatus.textContent = t('web.error');
2942
2994
  prereqStatus.className = 'status-badge status-error';
2943
2995
 
2944
2996
  const alertDiv = document.createElement('div');
2945
2997
  alertDiv.className = 'alert alert-error';
2946
2998
 
2947
2999
  const title = document.createElement('strong');
2948
- title.textContent = 'Wrangler not installed';
3000
+ title.textContent = t('web.error.wranglerNotInstalled');
2949
3001
  alertDiv.appendChild(title);
2950
3002
 
2951
3003
  const para = document.createElement('p');
2952
- para.textContent = 'Please install wrangler first:';
3004
+ para.textContent = t('web.error.pleaseInstall');
2953
3005
  alertDiv.appendChild(para);
2954
3006
 
2955
3007
  const code = document.createElement('code');
@@ -2966,18 +3018,18 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2966
3018
  }
2967
3019
 
2968
3020
  if (!result.auth.isLoggedIn) {
2969
- prereqStatus.textContent = 'Login Required';
3021
+ prereqStatus.textContent = t('web.error.notLoggedIn');
2970
3022
  prereqStatus.className = 'status-badge status-warning';
2971
3023
 
2972
3024
  const alertDiv = document.createElement('div');
2973
3025
  alertDiv.className = 'alert alert-warning';
2974
3026
 
2975
3027
  const title = document.createElement('strong');
2976
- title.textContent = 'Not logged in to Cloudflare';
3028
+ title.textContent = t('web.error.notLoggedIn');
2977
3029
  alertDiv.appendChild(title);
2978
3030
 
2979
3031
  const para1 = document.createElement('p');
2980
- para1.textContent = 'Please run this command in your terminal:';
3032
+ para1.textContent = t('web.error.runCommand');
2981
3033
  alertDiv.appendChild(para1);
2982
3034
 
2983
3035
  const code = document.createElement('code');
@@ -2991,14 +3043,14 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
2991
3043
 
2992
3044
  const para2 = document.createElement('p');
2993
3045
  para2.style.marginTop = '0.5rem';
2994
- para2.textContent = 'Then refresh this page.';
3046
+ para2.textContent = t('web.error.thenRefresh');
2995
3047
  alertDiv.appendChild(para2);
2996
3048
 
2997
3049
  prereqContent.appendChild(alertDiv);
2998
3050
  return false;
2999
3051
  }
3000
3052
 
3001
- prereqStatus.textContent = 'Ready';
3053
+ prereqStatus.textContent = t('web.prereq.ready');
3002
3054
  prereqStatus.className = 'status-badge status-success';
3003
3055
 
3004
3056
  // Store working directory and workers subdomain for later use
@@ -3009,11 +3061,12 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3009
3061
  alertDiv.className = 'alert alert-success';
3010
3062
 
3011
3063
  const p1 = document.createElement('p');
3012
- p1.textContent = '✓ Wrangler installed';
3064
+ p1.textContent = '✓ ' + t('web.prereq.wranglerInstalled');
3065
+ p1.setAttribute('data-i18n', 'web.prereq.wranglerInstalled');
3013
3066
  alertDiv.appendChild(p1);
3014
3067
 
3015
3068
  const p2 = document.createElement('p');
3016
- p2.textContent = '✓ Logged in as ' + (result.auth.email || 'Unknown');
3069
+ p2.textContent = '✓ ' + t('web.prereq.loggedInAs', { email: result.auth.email || 'Unknown' });
3017
3070
  alertDiv.appendChild(p2);
3018
3071
 
3019
3072
  prereqContent.appendChild(alertDiv);
@@ -3023,7 +3076,8 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3023
3076
 
3024
3077
  const btn = document.createElement('button');
3025
3078
  btn.className = 'btn-primary';
3026
- btn.textContent = 'Continue';
3079
+ btn.textContent = t('common.continue');
3080
+ btn.setAttribute('data-i18n', 'common.continue');
3027
3081
  btn.addEventListener('click', showTopMenu);
3028
3082
  buttonGroup.appendChild(btn);
3029
3083
 
@@ -3031,10 +3085,10 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3031
3085
 
3032
3086
  return true;
3033
3087
  } catch (error) {
3034
- prereqStatus.textContent = 'Error';
3088
+ prereqStatus.textContent = t('web.error');
3035
3089
  prereqStatus.className = 'status-badge status-error';
3036
3090
  prereqContent.textContent = '';
3037
- prereqContent.appendChild(createAlert('error', 'Error checking prerequisites: ' + error.message));
3091
+ prereqContent.appendChild(createAlert('error', t('web.error.checkingPrereq') + ' ' + error.message));
3038
3092
  return false;
3039
3093
  }
3040
3094
  }
@@ -3105,7 +3159,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3105
3159
  const errorList = document.getElementById('config-validation-errors');
3106
3160
  while (errorList.firstChild) errorList.removeChild(errorList.firstChild);
3107
3161
  const li = document.createElement('li');
3108
- li.textContent = 'Invalid JSON: ' + err.message;
3162
+ li.textContent = t('web.error.invalidJson') + ' ' + err.message;
3109
3163
  errorList.appendChild(li);
3110
3164
  loadedConfig = null;
3111
3165
  return;
@@ -3150,7 +3204,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3150
3204
  const errorList = document.getElementById('config-validation-errors');
3151
3205
  while (errorList.firstChild) errorList.removeChild(errorList.firstChild);
3152
3206
  const li = document.createElement('li');
3153
- li.textContent = 'Validation request failed: ' + err.message;
3207
+ li.textContent = t('web.error.validationFailed') + ' ' + err.message;
3154
3208
  errorList.appendChild(li);
3155
3209
  loadedConfig = null;
3156
3210
  }
@@ -3437,7 +3491,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3437
3491
  // Check if environment already exists
3438
3492
  const configureBtn = document.getElementById('btn-configure');
3439
3493
  const originalText = configureBtn.textContent;
3440
- configureBtn.textContent = 'Checking...';
3494
+ configureBtn.textContent = t('web.status.checking');
3441
3495
  configureBtn.disabled = true;
3442
3496
 
3443
3497
  try {
@@ -3601,7 +3655,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3601
3655
 
3602
3656
  // Save email configuration to server
3603
3657
  btn.disabled = true;
3604
- btn.textContent = 'Saving...';
3658
+ btn.textContent = t('web.status.saving');
3605
3659
 
3606
3660
  try {
3607
3661
  const result = await api('/email/configure', {
@@ -3629,12 +3683,12 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3629
3683
  } catch (error) {
3630
3684
  alert('Failed to save email configuration: ' + error.message);
3631
3685
  btn.disabled = false;
3632
- btn.textContent = 'Continue';
3686
+ btn.textContent = t('web.btn.continue');
3633
3687
  return;
3634
3688
  }
3635
3689
 
3636
3690
  btn.disabled = false;
3637
- btn.textContent = 'Continue';
3691
+ btn.textContent = t('web.btn.continue');
3638
3692
  } else {
3639
3693
  // Configure later - no email provider
3640
3694
  config.email = {
@@ -3657,18 +3711,18 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3657
3711
  const modal = document.getElementById('save-config-modal');
3658
3712
  const btn = document.getElementById('modal-save-config');
3659
3713
  btn.disabled = true;
3660
- btn.textContent = 'Saving...';
3714
+ btn.textContent = t('web.status.saving');
3661
3715
 
3662
3716
  try {
3663
3717
  await saveConfigToFile();
3664
3718
  modal.classList.add('hidden');
3665
3719
  btn.disabled = false;
3666
- btn.textContent = 'Save Configuration';
3720
+ btn.textContent = t('web.btn.saveConfiguration');
3667
3721
  proceedToProvision();
3668
3722
  } catch (error) {
3669
3723
  alert('Failed to save configuration: ' + error.message);
3670
3724
  btn.disabled = false;
3671
- btn.textContent = 'Save Configuration';
3725
+ btn.textContent = t('web.btn.saveConfiguration');
3672
3726
  }
3673
3727
  });
3674
3728
 
@@ -3714,7 +3768,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3714
3768
  btn.disabled = true;
3715
3769
  btn.classList.add('hidden');
3716
3770
  btnGotoDeploy.classList.add('hidden');
3717
- status.textContent = 'Running...';
3771
+ status.textContent = t('web.status.running');
3718
3772
  status.className = 'status-badge status-running';
3719
3773
  progressUI.classList.remove('hidden');
3720
3774
  log.classList.add('hidden'); // Log is hidden by default, toggled via button
@@ -3724,7 +3778,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3724
3778
 
3725
3779
  let provisionCompleted = 0;
3726
3780
  const totalResources = 8; // D1 Core, D1 PII, KV Settings, KV Cache, KV Tokens, R2 (optional), Queues (optional), Keys
3727
- updateProgressUI('provision', 0, totalResources, 'Initializing...');
3781
+ updateProgressUI('provision', 0, totalResources, t('web.status.initializing'));
3728
3782
 
3729
3783
  // Start polling for progress
3730
3784
  let lastProgressLength = 0;
@@ -3801,7 +3855,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3801
3855
  updateProgressUI('provision', totalResources, totalResources, '✅ Provisioning complete!');
3802
3856
  output.textContent += '\\n✅ Provisioning complete!\\n';
3803
3857
  scrollToBottom(log);
3804
- status.textContent = 'Complete';
3858
+ status.textContent = t('web.status.complete');
3805
3859
  status.className = 'status-badge status-success';
3806
3860
 
3807
3861
  // Mark provisioning as completed
@@ -3811,7 +3865,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3811
3865
  keysSavedInfo.classList.remove('hidden');
3812
3866
 
3813
3867
  // Update buttons - change to warning style for re-provision
3814
- btn.textContent = 'Re-provision (Delete & Create)';
3868
+ btn.textContent = t('web.btn.reprovision');
3815
3869
  btn.classList.remove('hidden', 'btn-primary');
3816
3870
  btn.classList.add('btn-warning');
3817
3871
  btn.disabled = false;
@@ -3828,7 +3882,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3828
3882
 
3829
3883
  output.textContent += '\\n❌ Error: ' + error.message + '\\n';
3830
3884
  scrollToBottom(log);
3831
- status.textContent = 'Error';
3885
+ status.textContent = t('web.status.error');
3832
3886
  status.className = 'status-badge status-error';
3833
3887
  btn.classList.remove('hidden');
3834
3888
  btn.disabled = false;
@@ -3864,18 +3918,18 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3864
3918
 
3865
3919
  btn.disabled = true;
3866
3920
  btn.classList.add('hidden');
3867
- status.textContent = 'Deploying...';
3921
+ status.textContent = t('web.status.deploying');
3868
3922
  status.className = 'status-badge status-running';
3869
3923
  readyText.classList.add('hidden');
3870
3924
  progressUI.classList.remove('hidden');
3871
3925
  log.classList.add('hidden'); // Log is hidden by default, toggled via button
3872
- output.textContent = 'Starting deployment...\\n\\n';
3926
+ output.textContent = t('web.status.startingDeploy') + '\\n\\n';
3873
3927
 
3874
3928
  let completedCount = 0;
3875
3929
  // Use indeterminate progress - actual step count varies based on components
3876
3930
  // We'll update the total dynamically based on actual progress
3877
3931
  let totalComponents = 0; // Will be calculated from actual progress
3878
- updateProgressUI('deploy', 0, 100, 'Initializing...');
3932
+ updateProgressUI('deploy', 0, 100, t('web.status.initializing'));
3879
3933
 
3880
3934
  try {
3881
3935
  // Generate wrangler configs first
@@ -3997,7 +4051,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
3997
4051
  }
3998
4052
  scrollToBottom(log);
3999
4053
 
4000
- status.textContent = 'Complete';
4054
+ status.textContent = t('web.status.complete');
4001
4055
  status.className = 'status-badge status-success';
4002
4056
 
4003
4057
  // Show completion with setup URL and debug info
@@ -4012,7 +4066,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4012
4066
  } catch (error) {
4013
4067
  output.textContent += '\\n✗ Error: ' + error.message + '\\n';
4014
4068
  scrollToBottom(log);
4015
- status.textContent = 'Error';
4069
+ status.textContent = t('web.status.error');
4016
4070
  status.className = 'status-badge status-error';
4017
4071
  btn.disabled = false;
4018
4072
  }
@@ -4205,14 +4259,14 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4205
4259
  const btnSaveConfig = document.getElementById('btn-save-config-provision');
4206
4260
 
4207
4261
  if (provisioningCompleted) {
4208
- btnProvision.textContent = 'Re-provision (Delete & Create)';
4262
+ btnProvision.textContent = t('web.btn.reprovision');
4209
4263
  btnProvision.classList.remove('btn-primary');
4210
4264
  btnProvision.classList.add('btn-warning');
4211
4265
  btnProvision.disabled = false;
4212
4266
  btnGotoDeploy.classList.remove('hidden');
4213
4267
  btnSaveConfig.classList.remove('hidden');
4214
4268
  } else {
4215
- btnProvision.textContent = 'Create Resources';
4269
+ btnProvision.textContent = t('web.btn.createResources');
4216
4270
  btnProvision.classList.remove('btn-warning');
4217
4271
  btnProvision.classList.add('btn-primary');
4218
4272
  btnProvision.disabled = false;
@@ -4351,7 +4405,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4351
4405
  const output = document.getElementById('env-scan-output');
4352
4406
  const noEnvsMessage = document.getElementById('no-envs-message');
4353
4407
 
4354
- status.textContent = 'Scanning...';
4408
+ status.textContent = t('web.status.scanning');
4355
4409
  status.className = 'status-badge status-running';
4356
4410
  loading.classList.remove('hidden');
4357
4411
  content.classList.add('hidden');
@@ -4379,7 +4433,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4379
4433
  if (result.success) {
4380
4434
  detectedEnvironments = result.environments || [];
4381
4435
 
4382
- status.textContent = detectedEnvironments.length + ' found';
4436
+ status.textContent = t('web.status.found', { count: detectedEnvironments.length });
4383
4437
  status.className = 'status-badge status-success';
4384
4438
  loading.classList.add('hidden');
4385
4439
  content.classList.remove('hidden');
@@ -4390,7 +4444,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4390
4444
  }
4391
4445
  } catch (error) {
4392
4446
  clearInterval(pollInterval);
4393
- status.textContent = 'Error';
4447
+ status.textContent = t('web.status.error');
4394
4448
  status.className = 'status-badge status-error';
4395
4449
  output.textContent += '\\n❌ Error: ' + error.message;
4396
4450
  }
@@ -4485,7 +4539,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4485
4539
  const badge = document.createElement('span');
4486
4540
  badge.className = 'status-badge status-warning';
4487
4541
  badge.style.cssText = 'font-size: 0.75rem; padding: 0.125rem 0.5rem;';
4488
- badge.textContent = 'Admin未設定';
4542
+ badge.textContent = t('web.status.adminNotConfigured');
4489
4543
  badgeContainer.appendChild(badge);
4490
4544
  }
4491
4545
  }
@@ -4564,7 +4618,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4564
4618
  if (resources.length === 0) {
4565
4619
  const empty = document.createElement('div');
4566
4620
  empty.className = 'resource-empty';
4567
- empty.textContent = 'None';
4621
+ empty.textContent = t('web.status.none');
4568
4622
  list.appendChild(empty);
4569
4623
  return;
4570
4624
  }
@@ -4583,7 +4637,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4583
4637
  if (resourceType === 'd1' || resourceType === 'worker') {
4584
4638
  const detailsDiv = document.createElement('div');
4585
4639
  detailsDiv.className = 'resource-item-details resource-item-loading';
4586
- detailsDiv.textContent = 'Loading...';
4640
+ detailsDiv.textContent = t('web.status.loading');
4587
4641
  item.appendChild(detailsDiv);
4588
4642
  }
4589
4643
 
@@ -4635,7 +4689,7 @@ export function getHtmlTemplate(sessionToken, manageOnly, locale = 'en', transla
4635
4689
  }
4636
4690
  } else {
4637
4691
  detailsDiv.className = 'resource-item-details resource-item-error';
4638
- detailsDiv.textContent = 'Failed to load';
4692
+ detailsDiv.textContent = t('web.status.failedToLoad');
4639
4693
  }
4640
4694
  } catch (e) {
4641
4695
  console.error('Failed to load D1 details:', e);