@unbrained/pm-web 2026.7.11 → 2026.7.17-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +26 -0
  3. package/dist/app.d.ts +16 -1
  4. package/dist/app.js +53 -3
  5. package/dist/app.js.map +1 -1
  6. package/dist/auth.d.ts +2 -1
  7. package/dist/auth.js +10 -0
  8. package/dist/auth.js.map +1 -1
  9. package/dist/db.js +17 -1
  10. package/dist/db.js.map +1 -1
  11. package/dist/ical.js.map +1 -1
  12. package/dist/index.d.ts +1 -22
  13. package/dist/index.js +2 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/oidc.d.ts +89 -0
  16. package/dist/oidc.js +305 -0
  17. package/dist/oidc.js.map +1 -0
  18. package/dist/routes/auth.js +3 -15
  19. package/dist/routes/auth.js.map +1 -1
  20. package/dist/routes/github.js +3 -3
  21. package/dist/routes/github.js.map +1 -1
  22. package/dist/routes/oidc.d.ts +4 -0
  23. package/dist/routes/oidc.js +147 -0
  24. package/dist/routes/oidc.js.map +1 -0
  25. package/dist/routes/pm.js +172 -108
  26. package/dist/routes/pm.js.map +1 -1
  27. package/dist/routes/projects.js +9 -2
  28. package/dist/routes/projects.js.map +1 -1
  29. package/dist/server.js +7 -2
  30. package/dist/server.js.map +1 -1
  31. package/dist/services/pm-runner.d.ts +27 -3
  32. package/dist/services/pm-runner.js +188 -54
  33. package/dist/services/pm-runner.js.map +1 -1
  34. package/dist/services/realtime-bus.d.ts +1 -0
  35. package/dist/services/realtime-bus.js +148 -0
  36. package/dist/services/realtime-bus.js.map +1 -0
  37. package/dist/services/sse.d.ts +3 -1
  38. package/dist/services/sse.js +83 -48
  39. package/dist/services/sse.js.map +1 -1
  40. package/manifest.json +1 -1
  41. package/package.json +11 -11
  42. package/public/cookie-consent.js +54 -39
  43. package/public/cookie-settings.html +7 -7
  44. package/public/index.html +32 -26
  45. package/public/legal-notice.html +10 -12
  46. package/public/privacy-policy.html +14 -24
  47. package/public/src/app.js +18 -4
  48. package/public/src/app.js.map +1 -1
  49. package/public/src/app.ts +19 -4
  50. package/public/src/components/modals.js +5 -3
  51. package/public/src/components/modals.js.map +1 -1
  52. package/public/src/components/modals.ts +6 -3
  53. package/public/src/components/toast.js.map +1 -1
  54. package/public/src/cookie-consent.ts +68 -0
  55. package/public/src/i18n/de.json +117 -0
  56. package/public/src/i18n/en.json +117 -0
  57. package/public/src/i18n/es.json +117 -0
  58. package/public/src/i18n/zh.json +117 -0
  59. package/public/src/i18n.js +287 -0
  60. package/public/src/i18n.js.map +1 -0
  61. package/public/src/i18n.ts +306 -0
  62. package/public/src/sw.ts +446 -0
  63. package/public/src/views/auth.js +29 -6
  64. package/public/src/views/auth.js.map +1 -1
  65. package/public/src/views/auth.ts +27 -6
  66. package/public/src/views/graph.js.map +1 -1
  67. package/public/src/views/settings.js +73 -48
  68. package/public/src/views/settings.js.map +1 -1
  69. package/public/src/views/settings.ts +77 -48
  70. package/public/styles.css +2 -0
  71. package/public/sw.js +323 -256
  72. package/public/terms.html +7 -9
  73. package/public/tsconfig.json +1 -1
  74. package/public/tsconfig.scripts.json +18 -0
  75. package/public/tsconfig.sw.json +18 -0
  76. package/sql/schema.sql +16 -0
@@ -6,6 +6,7 @@ import { api } from '../api.js';
6
6
  import { escHtml } from '../utils.js';
7
7
  import { toast } from '../components/toast.js';
8
8
  import { confirmDialog } from '../components/modals.js';
9
+ import { t, translateError, localeDate, getLocale, SUPPORTED_LOCALES } from '../i18n.js';
9
10
  function avatarInitial(name) {
10
11
  return (name.trim()[0] || '?').toUpperCase();
11
12
  }
@@ -17,21 +18,45 @@ function avatarBg(seed) {
17
18
  const hue = hash % 360;
18
19
  return `hsl(${hue},55%,45%)`;
19
20
  }
21
+ /** Compile-time-complete mapping from supported locale → settings label key. */
22
+ const LOCALE_LABEL_KEYS = {
23
+ en: 'settings.languageEn',
24
+ de: 'settings.languageDe',
25
+ es: 'settings.languageEs',
26
+ zh: 'settings.languageZh',
27
+ };
20
28
  export function renderSettingsView() {
21
29
  const el = document.getElementById('content-settings');
22
30
  if (!el)
23
31
  return;
24
32
  const u = state.user || {};
25
33
  const createdInfo = u.created_at
26
- ? `<span style="display:block;margin-top:4px;font-size:12px;color:var(--text-muted)">Account created ${new Date(u.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</span>`
34
+ ? `<span style="display:block;margin-top:4px;font-size:12px;color:var(--text-muted)">${escHtml(t('settings.accountCreated', { date: localeDate(u.created_at, { year: 'numeric', month: 'long', day: 'numeric' }) }))}</span>`
27
35
  : '';
36
+ const currentLocale = getLocale();
37
+ const langOptions = SUPPORTED_LOCALES.map((loc) => {
38
+ const label = t(LOCALE_LABEL_KEYS[loc]);
39
+ return `<option value="${loc}"${loc === currentLocale ? ' selected' : ''}>${escHtml(label)}</option>`;
40
+ }).join('');
28
41
  el.innerHTML = `
29
42
  <div class="page-header">
30
- <div><div class="page-title">Settings</div><div class="page-subtitle">Manage your profile and account</div></div>
43
+ <div><div class="page-title">${escHtml(t('settings.title'))}</div><div class="page-subtitle">${escHtml(t('settings.subtitle'))}</div></div>
31
44
  </div>
32
45
  <div style="max-width:560px;display:flex;flex-direction:column;gap:20px">
33
46
  <div class="card">
34
- <div class="card-header"><div class="card-title">Profile</div></div>
47
+ <div class="card-header"><div class="card-title">${escHtml(t('settings.language'))}</div></div>
48
+ <div class="card-body">
49
+ <div class="form-group">
50
+ <label class="form-label" for="settings-language">${escHtml(t('settings.language'))}</label>
51
+ <select class="form-input" id="settings-language" onchange="window.__app.changeLanguage(this.value)" aria-label="${escHtml(t('settings.language'))}">
52
+ ${langOptions}
53
+ </select>
54
+ <div style="font-size:12px;color:var(--text-muted);margin-top:6px">${escHtml(t('settings.languageHint'))}</div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ <div class="card">
59
+ <div class="card-header"><div class="card-title">${escHtml(t('settings.profile'))}</div></div>
35
60
  <div class="card-body">
36
61
  <div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
37
62
  <div style="width:64px;height:64px;border-radius:50%;background:${avatarBg(u.email || u.display_name || '?')};display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;color:#fff;flex-shrink:0;user-select:none" aria-hidden="true">${escHtml(avatarInitial(u.display_name || u.email || '?'))}</div>
@@ -41,60 +66,60 @@ export function renderSettingsView() {
41
66
  </div>
42
67
  </div>
43
68
  <div class="form-group">
44
- <label class="form-label" for="settings-display-name">Display Name</label>
45
- <input class="form-input" id="settings-display-name" type="text" value="${escHtml(u.display_name || u.email || '')}" placeholder="Your display name" aria-label="Display name">
69
+ <label class="form-label" for="settings-display-name">${escHtml(t('settings.displayName'))}</label>
70
+ <input class="form-input" id="settings-display-name" type="text" value="${escHtml(u.display_name || u.email || '')}" placeholder="${escHtml(t('settings.displayNamePlaceholder'))}" aria-label="${escHtml(t('settings.displayName'))}">
46
71
  </div>
47
72
  <div class="form-group">
48
- <label class="form-label">Email</label>
49
- <input class="form-input" type="text" value="${escHtml(u.email || '')}" disabled style="opacity:0.6;cursor:not-allowed" aria-label="Email address (read only)">
73
+ <label class="form-label">${escHtml(t('settings.email'))}</label>
74
+ <input class="form-input" type="text" value="${escHtml(u.email || '')}" disabled style="opacity:0.6;cursor:not-allowed" aria-label="${escHtml(t('settings.emailReadonly'))}">
50
75
  ${createdInfo}
51
76
  </div>
52
77
  <div class="form-error" id="settings-profile-error" style="display:none" role="alert"></div>
53
- <button class="btn btn-primary btn-sm" id="settings-profile-btn" onclick="window.__app.saveProfile()" aria-label="Save profile changes"><span>Save Profile</span></button>
78
+ <button class="btn btn-primary btn-sm" id="settings-profile-btn" onclick="window.__app.saveProfile()" aria-label="${escHtml(t('settings.saveProfile'))}"><span>${escHtml(t('settings.saveProfile'))}</span></button>
54
79
  </div>
55
80
  </div>
56
81
  <div class="card">
57
- <div class="card-header"><div class="card-title">Change Password</div></div>
82
+ <div class="card-header"><div class="card-title">${escHtml(t('settings.changePassword'))}</div></div>
58
83
  <div class="card-body">
59
84
  <div class="form-group">
60
- <label class="form-label" for="settings-current-pw">Current Password</label>
61
- <input class="form-input" id="settings-current-pw" type="password" placeholder="Current password" autocomplete="current-password" aria-label="Current password">
85
+ <label class="form-label" for="settings-current-pw">${escHtml(t('settings.currentPassword'))}</label>
86
+ <input class="form-input" id="settings-current-pw" type="password" placeholder="${escHtml(t('settings.currentPasswordPlaceholder'))}" autocomplete="current-password" aria-label="${escHtml(t('settings.currentPassword'))}">
62
87
  </div>
63
88
  <div class="form-group">
64
- <label class="form-label" for="settings-new-pw">New Password</label>
65
- <input class="form-input" id="settings-new-pw" type="password" placeholder="New password" autocomplete="new-password" aria-label="New password">
89
+ <label class="form-label" for="settings-new-pw">${escHtml(t('settings.newPassword'))}</label>
90
+ <input class="form-input" id="settings-new-pw" type="password" placeholder="${escHtml(t('settings.newPasswordPlaceholder'))}" autocomplete="new-password" aria-label="${escHtml(t('settings.newPassword'))}">
66
91
  </div>
67
92
  <div class="form-group">
68
- <label class="form-label" for="settings-confirm-pw">Confirm New Password</label>
69
- <input class="form-input" id="settings-confirm-pw" type="password" placeholder="Confirm new password" autocomplete="new-password" aria-label="Confirm new password">
93
+ <label class="form-label" for="settings-confirm-pw">${escHtml(t('settings.confirmPassword'))}</label>
94
+ <input class="form-input" id="settings-confirm-pw" type="password" placeholder="${escHtml(t('settings.confirmPasswordPlaceholder'))}" autocomplete="new-password" aria-label="${escHtml(t('settings.confirmPassword'))}">
70
95
  </div>
71
96
  <div class="form-error" id="settings-pw-error" style="display:none" role="alert"></div>
72
- <button class="btn btn-primary btn-sm" id="settings-pw-btn" onclick="window.__app.changePassword()" aria-label="Change password"><span>Change Password</span></button>
97
+ <button class="btn btn-primary btn-sm" id="settings-pw-btn" onclick="window.__app.changePassword()" aria-label="${escHtml(t('settings.changePassword'))}"><span>${escHtml(t('settings.changePassword'))}</span></button>
73
98
  </div>
74
99
  </div>
75
100
  <div class="card">
76
101
  <div class="card-header">
77
- <div class="card-title">GitHub Token</div>
78
- ${u.has_github_token ? `<span style="font-size:12px;color:var(--status-closed)">✓ Token configured</span>` : `<span style="font-size:12px;color:var(--text-muted)">No token set</span>`}
102
+ <div class="card-title">${escHtml(t('settings.githubToken'))}</div>
103
+ ${u.has_github_token ? `<span style="font-size:12px;color:var(--status-closed)">${escHtml(t('settings.tokenConfigured'))}</span>` : `<span style="font-size:12px;color:var(--text-muted)">${escHtml(t('settings.tokenNotSet'))}</span>`}
79
104
  </div>
80
105
  <div class="card-body">
81
106
  <div style="margin-bottom:12px;padding:10px 12px;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);font-size:12px;color:var(--text-secondary)">
82
- <strong style="color:var(--text-primary)">How to get a token:</strong>
107
+ <strong style="color:var(--text-primary)">${escHtml(t('settings.howToGet'))}</strong>
83
108
  <ol style="margin-top:6px;padding-left:18px;line-height:1.8">
84
- <li>Go to GitHub → Settings → Developer settings → Personal access tokens</li>
85
- <li>Generate a new token with <code style="font-family:monospace;background:var(--bg-base);padding:0 3px;border-radius:3px">repo</code> scope</li>
86
- <li>Paste the token below and click Save</li>
109
+ <li>${escHtml(t('settings.tokenStep1'))}</li>
110
+ <li>${escHtml(t('settings.tokenStep2'))}</li>
111
+ <li>${escHtml(t('settings.tokenStep3'))}</li>
87
112
  </ol>
88
113
  </div>
89
114
  <div class="form-group">
90
- <label class="form-label" for="settings-github-token">Personal Access Token (PAT)</label>
91
- <input class="form-input" id="settings-github-token" type="password" placeholder="${u.has_github_token ? 'Leave blank to keep current token' : 'ghp_…'}" autocomplete="off" aria-label="GitHub personal access token">
92
- <div style="font-size:11px;color:var(--text-muted);margin-top:4px">Used for GitHub integration. Needs <code style="font-family:monospace;background:var(--bg-input);padding:0 3px;border-radius:3px">repo</code> scope.</div>
115
+ <label class="form-label" for="settings-github-token">${escHtml(t('settings.tokenLabel'))}</label>
116
+ <input class="form-input" id="settings-github-token" type="password" placeholder="${escHtml(u.has_github_token ? t('settings.tokenPlaceholder.keep') : t('settings.tokenPlaceholder.set'))}" autocomplete="off" aria-label="${escHtml(t('settings.tokenLabel'))}">
117
+ <div style="font-size:11px;color:var(--text-muted);margin-top:4px">${t('settings.tokenHint')}</div>
93
118
  </div>
94
119
  <div class="form-error" id="settings-github-error" style="display:none" role="alert"></div>
95
120
  <div style="display:flex;gap:8px">
96
- <button class="btn btn-primary btn-sm" id="settings-github-btn" onclick="window.__app.saveGitHubToken()" aria-label="Save GitHub token"><span>Save Token</span></button>
97
- ${u.has_github_token ? `<button class="btn btn-danger btn-sm" onclick="window.__app.clearGitHubToken()" aria-label="Clear GitHub token">Clear Token</button>` : ''}
121
+ <button class="btn btn-primary btn-sm" id="settings-github-btn" onclick="window.__app.saveGitHubToken()" aria-label="${escHtml(t('settings.saveToken'))}"><span>${escHtml(t('settings.saveToken'))}</span></button>
122
+ ${u.has_github_token ? `<button class="btn btn-danger btn-sm" onclick="window.__app.clearGitHubToken()" aria-label="${escHtml(t('settings.clearToken'))}">${escHtml(t('settings.clearToken'))}</button>` : ''}
98
123
  </div>
99
124
  </div>
100
125
  </div>
@@ -106,7 +131,7 @@ export async function saveProfile() {
106
131
  const btn = document.getElementById('settings-profile-btn');
107
132
  if (!displayName) {
108
133
  if (errEl) {
109
- errEl.textContent = 'Display name cannot be empty';
134
+ errEl.textContent = t('settings.displayNameEmpty');
110
135
  errEl.style.display = 'block';
111
136
  }
112
137
  return;
@@ -117,7 +142,7 @@ export async function saveProfile() {
117
142
  btn.disabled = true;
118
143
  const sp = btn.querySelector('span');
119
144
  if (sp)
120
- sp.textContent = 'Saving…';
145
+ sp.textContent = t('settings.saving');
121
146
  }
122
147
  try {
123
148
  const data = await api('PATCH', '/auth/profile', { displayName });
@@ -135,12 +160,12 @@ export async function saveProfile() {
135
160
  const nameEl = document.getElementById('user-name-display');
136
161
  if (nameEl)
137
162
  nameEl.textContent = u.display_name || u.email;
138
- toast('Profile saved', 'success');
163
+ toast(t('settings.profileSaved'), 'success');
139
164
  renderSettingsView();
140
165
  }
141
166
  catch (err) {
142
167
  if (errEl) {
143
- errEl.textContent = err instanceof Error ? err.message : String(err);
168
+ errEl.textContent = translateError(err instanceof Error ? err.message : String(err));
144
169
  errEl.style.display = 'block';
145
170
  }
146
171
  }
@@ -149,7 +174,7 @@ export async function saveProfile() {
149
174
  btn.disabled = false;
150
175
  const sp = btn.querySelector('span');
151
176
  if (sp)
152
- sp.textContent = 'Save Profile';
177
+ sp.textContent = t('settings.saveProfile');
153
178
  }
154
179
  }
155
180
  }
@@ -163,21 +188,21 @@ export async function changePassword() {
163
188
  errEl.style.display = 'none';
164
189
  if (!currentPassword || !newPassword || !confirmPassword) {
165
190
  if (errEl) {
166
- errEl.textContent = 'All fields are required';
191
+ errEl.textContent = t('settings.allFieldsRequired');
167
192
  errEl.style.display = 'block';
168
193
  }
169
194
  return;
170
195
  }
171
196
  if (newPassword !== confirmPassword) {
172
197
  if (errEl) {
173
- errEl.textContent = 'New passwords do not match';
198
+ errEl.textContent = t('settings.passwordsDoNotMatch');
174
199
  errEl.style.display = 'block';
175
200
  }
176
201
  return;
177
202
  }
178
203
  if (newPassword.length < 6) {
179
204
  if (errEl) {
180
- errEl.textContent = 'New password must be at least 6 characters';
205
+ errEl.textContent = t('settings.passwordTooShort');
181
206
  errEl.style.display = 'block';
182
207
  }
183
208
  return;
@@ -186,11 +211,11 @@ export async function changePassword() {
186
211
  btn.disabled = true;
187
212
  const sp = btn.querySelector('span');
188
213
  if (sp)
189
- sp.textContent = 'Changing…';
214
+ sp.textContent = t('settings.changing');
190
215
  }
191
216
  try {
192
217
  await api('POST', '/auth/change-password', { currentPassword, newPassword });
193
- toast('Password changed successfully', 'success');
218
+ toast(t('settings.passwordChanged'), 'success');
194
219
  const curEl = document.getElementById('settings-current-pw');
195
220
  const newEl = document.getElementById('settings-new-pw');
196
221
  const confEl = document.getElementById('settings-confirm-pw');
@@ -203,7 +228,7 @@ export async function changePassword() {
203
228
  }
204
229
  catch (err) {
205
230
  if (errEl) {
206
- errEl.textContent = err instanceof Error ? err.message : String(err);
231
+ errEl.textContent = translateError(err instanceof Error ? err.message : String(err));
207
232
  errEl.style.display = 'block';
208
233
  }
209
234
  }
@@ -212,7 +237,7 @@ export async function changePassword() {
212
237
  btn.disabled = false;
213
238
  const sp = btn.querySelector('span');
214
239
  if (sp)
215
- sp.textContent = 'Change Password';
240
+ sp.textContent = t('settings.changePassword');
216
241
  }
217
242
  }
218
243
  }
@@ -222,7 +247,7 @@ export async function saveGitHubToken() {
222
247
  const btn = document.getElementById('settings-github-btn');
223
248
  if (!token) {
224
249
  if (errEl) {
225
- errEl.textContent = 'Please enter a token';
250
+ errEl.textContent = t('settings.tokenEmpty');
226
251
  errEl.style.display = 'block';
227
252
  }
228
253
  return;
@@ -233,17 +258,17 @@ export async function saveGitHubToken() {
233
258
  btn.disabled = true;
234
259
  const sp = btn.querySelector('span');
235
260
  if (sp)
236
- sp.textContent = 'Saving…';
261
+ sp.textContent = t('settings.saving');
237
262
  }
238
263
  try {
239
264
  const data = await api('PATCH', '/auth/github-token', { token });
240
265
  state.user.has_github_token = data.hasToken !== undefined ? data.hasToken : true;
241
- toast('GitHub token saved', 'success');
266
+ toast(t('settings.tokenSaved'), 'success');
242
267
  renderSettingsView();
243
268
  }
244
269
  catch (err) {
245
270
  if (errEl) {
246
- errEl.textContent = err instanceof Error ? err.message : String(err);
271
+ errEl.textContent = translateError(err instanceof Error ? err.message : String(err));
247
272
  errEl.style.display = 'block';
248
273
  }
249
274
  }
@@ -252,21 +277,21 @@ export async function saveGitHubToken() {
252
277
  btn.disabled = false;
253
278
  const sp = btn.querySelector('span');
254
279
  if (sp)
255
- sp.textContent = 'Save Token';
280
+ sp.textContent = t('settings.saveToken');
256
281
  }
257
282
  }
258
283
  }
259
284
  export function clearGitHubToken() {
260
- confirmDialog('Clear GitHub Token?', 'Your GitHub integration will stop working.', async () => {
285
+ confirmDialog(t('settings.clearTokenTitle'), t('settings.clearTokenBody'), async () => {
261
286
  try {
262
287
  const data = await api('PATCH', '/auth/github-token', { token: '' });
263
288
  state.user.has_github_token = false;
264
- toast('GitHub token cleared', 'success');
289
+ toast(t('settings.tokenCleared'), 'success');
265
290
  renderSettingsView();
266
291
  }
267
292
  catch (err) {
268
- toast(err instanceof Error ? err.message : String(err), 'error');
293
+ toast(translateError(err instanceof Error ? err.message : String(err)), 'error');
269
294
  }
270
- });
295
+ }, false, { cancel: t('dialog.cancel'), confirm: t('dialog.confirm') });
271
296
  }
272
297
  //# sourceMappingURL=settings.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"settings.js","sourceRoot":"","sources":["settings.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["settings.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,7 @@ import { api } from '../api.js';
6
6
  import { escHtml } from '../utils.js';
7
7
  import { toast } from '../components/toast.js';
8
8
  import { confirmDialog } from '../components/modals.js';
9
+ import { t, translateError, localeDate, getLocale, SUPPORTED_LOCALES, type SupportedLocale } from '../i18n.js';
9
10
 
10
11
  function avatarInitial(name: string): string {
11
12
  return (name.trim()[0] || '?').toUpperCase();
@@ -18,20 +19,45 @@ function avatarBg(seed: string): string {
18
19
  return `hsl(${hue},55%,45%)`;
19
20
  }
20
21
 
22
+ /** Compile-time-complete mapping from supported locale → settings label key. */
23
+ const LOCALE_LABEL_KEYS: Record<SupportedLocale, string> = {
24
+ en: 'settings.languageEn',
25
+ de: 'settings.languageDe',
26
+ es: 'settings.languageEs',
27
+ zh: 'settings.languageZh',
28
+ };
29
+
21
30
  export function renderSettingsView(): void {
22
31
  const el = document.getElementById('content-settings');
23
32
  if (!el) return;
24
33
  const u = state.user || ({} as any);
25
34
  const createdInfo = u.created_at
26
- ? `<span style="display:block;margin-top:4px;font-size:12px;color:var(--text-muted)">Account created ${new Date(u.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</span>`
35
+ ? `<span style="display:block;margin-top:4px;font-size:12px;color:var(--text-muted)">${escHtml(t('settings.accountCreated', { date: localeDate(u.created_at, { year: 'numeric', month: 'long', day: 'numeric' }) }))}</span>`
27
36
  : '';
37
+ const currentLocale = getLocale();
38
+ const langOptions = SUPPORTED_LOCALES.map((loc) => {
39
+ const label = t(LOCALE_LABEL_KEYS[loc]);
40
+ return `<option value="${loc}"${loc === currentLocale ? ' selected' : ''}>${escHtml(label)}</option>`;
41
+ }).join('');
28
42
  el.innerHTML = `
29
43
  <div class="page-header">
30
- <div><div class="page-title">Settings</div><div class="page-subtitle">Manage your profile and account</div></div>
44
+ <div><div class="page-title">${escHtml(t('settings.title'))}</div><div class="page-subtitle">${escHtml(t('settings.subtitle'))}</div></div>
31
45
  </div>
32
46
  <div style="max-width:560px;display:flex;flex-direction:column;gap:20px">
33
47
  <div class="card">
34
- <div class="card-header"><div class="card-title">Profile</div></div>
48
+ <div class="card-header"><div class="card-title">${escHtml(t('settings.language'))}</div></div>
49
+ <div class="card-body">
50
+ <div class="form-group">
51
+ <label class="form-label" for="settings-language">${escHtml(t('settings.language'))}</label>
52
+ <select class="form-input" id="settings-language" onchange="window.__app.changeLanguage(this.value)" aria-label="${escHtml(t('settings.language'))}">
53
+ ${langOptions}
54
+ </select>
55
+ <div style="font-size:12px;color:var(--text-muted);margin-top:6px">${escHtml(t('settings.languageHint'))}</div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <div class="card">
60
+ <div class="card-header"><div class="card-title">${escHtml(t('settings.profile'))}</div></div>
35
61
  <div class="card-body">
36
62
  <div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
37
63
  <div style="width:64px;height:64px;border-radius:50%;background:${avatarBg(u.email||u.display_name||'?')};display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;color:#fff;flex-shrink:0;user-select:none" aria-hidden="true">${escHtml(avatarInitial(u.display_name||u.email||'?'))}</div>
@@ -41,60 +67,60 @@ export function renderSettingsView(): void {
41
67
  </div>
42
68
  </div>
43
69
  <div class="form-group">
44
- <label class="form-label" for="settings-display-name">Display Name</label>
45
- <input class="form-input" id="settings-display-name" type="text" value="${escHtml(u.display_name||u.email||'')}" placeholder="Your display name" aria-label="Display name">
70
+ <label class="form-label" for="settings-display-name">${escHtml(t('settings.displayName'))}</label>
71
+ <input class="form-input" id="settings-display-name" type="text" value="${escHtml(u.display_name||u.email||'')}" placeholder="${escHtml(t('settings.displayNamePlaceholder'))}" aria-label="${escHtml(t('settings.displayName'))}">
46
72
  </div>
47
73
  <div class="form-group">
48
- <label class="form-label">Email</label>
49
- <input class="form-input" type="text" value="${escHtml(u.email||'')}" disabled style="opacity:0.6;cursor:not-allowed" aria-label="Email address (read only)">
74
+ <label class="form-label">${escHtml(t('settings.email'))}</label>
75
+ <input class="form-input" type="text" value="${escHtml(u.email||'')}" disabled style="opacity:0.6;cursor:not-allowed" aria-label="${escHtml(t('settings.emailReadonly'))}">
50
76
  ${createdInfo}
51
77
  </div>
52
78
  <div class="form-error" id="settings-profile-error" style="display:none" role="alert"></div>
53
- <button class="btn btn-primary btn-sm" id="settings-profile-btn" onclick="window.__app.saveProfile()" aria-label="Save profile changes"><span>Save Profile</span></button>
79
+ <button class="btn btn-primary btn-sm" id="settings-profile-btn" onclick="window.__app.saveProfile()" aria-label="${escHtml(t('settings.saveProfile'))}"><span>${escHtml(t('settings.saveProfile'))}</span></button>
54
80
  </div>
55
81
  </div>
56
82
  <div class="card">
57
- <div class="card-header"><div class="card-title">Change Password</div></div>
83
+ <div class="card-header"><div class="card-title">${escHtml(t('settings.changePassword'))}</div></div>
58
84
  <div class="card-body">
59
85
  <div class="form-group">
60
- <label class="form-label" for="settings-current-pw">Current Password</label>
61
- <input class="form-input" id="settings-current-pw" type="password" placeholder="Current password" autocomplete="current-password" aria-label="Current password">
86
+ <label class="form-label" for="settings-current-pw">${escHtml(t('settings.currentPassword'))}</label>
87
+ <input class="form-input" id="settings-current-pw" type="password" placeholder="${escHtml(t('settings.currentPasswordPlaceholder'))}" autocomplete="current-password" aria-label="${escHtml(t('settings.currentPassword'))}">
62
88
  </div>
63
89
  <div class="form-group">
64
- <label class="form-label" for="settings-new-pw">New Password</label>
65
- <input class="form-input" id="settings-new-pw" type="password" placeholder="New password" autocomplete="new-password" aria-label="New password">
90
+ <label class="form-label" for="settings-new-pw">${escHtml(t('settings.newPassword'))}</label>
91
+ <input class="form-input" id="settings-new-pw" type="password" placeholder="${escHtml(t('settings.newPasswordPlaceholder'))}" autocomplete="new-password" aria-label="${escHtml(t('settings.newPassword'))}">
66
92
  </div>
67
93
  <div class="form-group">
68
- <label class="form-label" for="settings-confirm-pw">Confirm New Password</label>
69
- <input class="form-input" id="settings-confirm-pw" type="password" placeholder="Confirm new password" autocomplete="new-password" aria-label="Confirm new password">
94
+ <label class="form-label" for="settings-confirm-pw">${escHtml(t('settings.confirmPassword'))}</label>
95
+ <input class="form-input" id="settings-confirm-pw" type="password" placeholder="${escHtml(t('settings.confirmPasswordPlaceholder'))}" autocomplete="new-password" aria-label="${escHtml(t('settings.confirmPassword'))}">
70
96
  </div>
71
97
  <div class="form-error" id="settings-pw-error" style="display:none" role="alert"></div>
72
- <button class="btn btn-primary btn-sm" id="settings-pw-btn" onclick="window.__app.changePassword()" aria-label="Change password"><span>Change Password</span></button>
98
+ <button class="btn btn-primary btn-sm" id="settings-pw-btn" onclick="window.__app.changePassword()" aria-label="${escHtml(t('settings.changePassword'))}"><span>${escHtml(t('settings.changePassword'))}</span></button>
73
99
  </div>
74
100
  </div>
75
101
  <div class="card">
76
102
  <div class="card-header">
77
- <div class="card-title">GitHub Token</div>
78
- ${u.has_github_token ? `<span style="font-size:12px;color:var(--status-closed)">✓ Token configured</span>` : `<span style="font-size:12px;color:var(--text-muted)">No token set</span>`}
103
+ <div class="card-title">${escHtml(t('settings.githubToken'))}</div>
104
+ ${u.has_github_token ? `<span style="font-size:12px;color:var(--status-closed)">${escHtml(t('settings.tokenConfigured'))}</span>` : `<span style="font-size:12px;color:var(--text-muted)">${escHtml(t('settings.tokenNotSet'))}</span>`}
79
105
  </div>
80
106
  <div class="card-body">
81
107
  <div style="margin-bottom:12px;padding:10px 12px;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);font-size:12px;color:var(--text-secondary)">
82
- <strong style="color:var(--text-primary)">How to get a token:</strong>
108
+ <strong style="color:var(--text-primary)">${escHtml(t('settings.howToGet'))}</strong>
83
109
  <ol style="margin-top:6px;padding-left:18px;line-height:1.8">
84
- <li>Go to GitHub → Settings → Developer settings → Personal access tokens</li>
85
- <li>Generate a new token with <code style="font-family:monospace;background:var(--bg-base);padding:0 3px;border-radius:3px">repo</code> scope</li>
86
- <li>Paste the token below and click Save</li>
110
+ <li>${escHtml(t('settings.tokenStep1'))}</li>
111
+ <li>${escHtml(t('settings.tokenStep2'))}</li>
112
+ <li>${escHtml(t('settings.tokenStep3'))}</li>
87
113
  </ol>
88
114
  </div>
89
115
  <div class="form-group">
90
- <label class="form-label" for="settings-github-token">Personal Access Token (PAT)</label>
91
- <input class="form-input" id="settings-github-token" type="password" placeholder="${u.has_github_token ? 'Leave blank to keep current token' : 'ghp_…'}" autocomplete="off" aria-label="GitHub personal access token">
92
- <div style="font-size:11px;color:var(--text-muted);margin-top:4px">Used for GitHub integration. Needs <code style="font-family:monospace;background:var(--bg-input);padding:0 3px;border-radius:3px">repo</code> scope.</div>
116
+ <label class="form-label" for="settings-github-token">${escHtml(t('settings.tokenLabel'))}</label>
117
+ <input class="form-input" id="settings-github-token" type="password" placeholder="${escHtml(u.has_github_token ? t('settings.tokenPlaceholder.keep') : t('settings.tokenPlaceholder.set'))}" autocomplete="off" aria-label="${escHtml(t('settings.tokenLabel'))}">
118
+ <div style="font-size:11px;color:var(--text-muted);margin-top:4px">${t('settings.tokenHint')}</div>
93
119
  </div>
94
120
  <div class="form-error" id="settings-github-error" style="display:none" role="alert"></div>
95
121
  <div style="display:flex;gap:8px">
96
- <button class="btn btn-primary btn-sm" id="settings-github-btn" onclick="window.__app.saveGitHubToken()" aria-label="Save GitHub token"><span>Save Token</span></button>
97
- ${u.has_github_token ? `<button class="btn btn-danger btn-sm" onclick="window.__app.clearGitHubToken()" aria-label="Clear GitHub token">Clear Token</button>` : ''}
122
+ <button class="btn btn-primary btn-sm" id="settings-github-btn" onclick="window.__app.saveGitHubToken()" aria-label="${escHtml(t('settings.saveToken'))}"><span>${escHtml(t('settings.saveToken'))}</span></button>
123
+ ${u.has_github_token ? `<button class="btn btn-danger btn-sm" onclick="window.__app.clearGitHubToken()" aria-label="${escHtml(t('settings.clearToken'))}">${escHtml(t('settings.clearToken'))}</button>` : ''}
98
124
  </div>
99
125
  </div>
100
126
  </div>
@@ -105,9 +131,9 @@ export async function saveProfile(): Promise<void> {
105
131
  const displayName = (document.getElementById('settings-display-name') as HTMLInputElement | null)?.value?.trim() || '';
106
132
  const errEl = document.getElementById('settings-profile-error') as HTMLElement | null;
107
133
  const btn = document.getElementById('settings-profile-btn') as HTMLButtonElement | null;
108
- if (!displayName) { if (errEl) { errEl.textContent = 'Display name cannot be empty'; errEl.style.display = 'block'; } return; }
134
+ if (!displayName) { if (errEl) { errEl.textContent = t('settings.displayNameEmpty'); errEl.style.display = 'block'; } return; }
109
135
  if (errEl) errEl.style.display = 'none';
110
- if (btn) { btn.disabled = true; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Saving…'; }
136
+ if (btn) { btn.disabled = true; const sp = btn.querySelector('span'); if (sp) sp.textContent = t('settings.saving'); }
111
137
  try {
112
138
  const data = await api('PATCH','/auth/profile',{displayName});
113
139
  if ((data as any).user) {
@@ -121,12 +147,12 @@ export async function saveProfile(): Promise<void> {
121
147
  if (avatarEl) avatarEl.textContent = initials;
122
148
  const nameEl = document.getElementById('user-name-display');
123
149
  if (nameEl) nameEl.textContent = u.display_name||u.email;
124
- toast('Profile saved','success');
150
+ toast(t('settings.profileSaved'),'success');
125
151
  renderSettingsView();
126
152
  } catch(err: unknown) {
127
- if (errEl) { errEl.textContent = err instanceof Error ? err.message : String(err); errEl.style.display = 'block'; }
153
+ if (errEl) { errEl.textContent = translateError(err instanceof Error ? err.message : String(err)); errEl.style.display = 'block'; }
128
154
  } finally {
129
- if (btn) { btn.disabled = false; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Save Profile'; }
155
+ if (btn) { btn.disabled = false; const sp = btn.querySelector('span'); if (sp) sp.textContent = t('settings.saveProfile'); }
130
156
  }
131
157
  }
132
158
 
@@ -137,13 +163,13 @@ export async function changePassword(): Promise<void> {
137
163
  const errEl = document.getElementById('settings-pw-error') as HTMLElement | null;
138
164
  const btn = document.getElementById('settings-pw-btn') as HTMLButtonElement | null;
139
165
  if (errEl) errEl.style.display = 'none';
140
- if (!currentPassword || !newPassword || !confirmPassword) { if (errEl) { errEl.textContent = 'All fields are required'; errEl.style.display = 'block'; } return; }
141
- if (newPassword !== confirmPassword) { if (errEl) { errEl.textContent = 'New passwords do not match'; errEl.style.display = 'block'; } return; }
142
- if (newPassword.length < 6) { if (errEl) { errEl.textContent = 'New password must be at least 6 characters'; errEl.style.display = 'block'; } return; }
143
- if (btn) { btn.disabled = true; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Changing…'; }
166
+ if (!currentPassword || !newPassword || !confirmPassword) { if (errEl) { errEl.textContent = t('settings.allFieldsRequired'); errEl.style.display = 'block'; } return; }
167
+ if (newPassword !== confirmPassword) { if (errEl) { errEl.textContent = t('settings.passwordsDoNotMatch'); errEl.style.display = 'block'; } return; }
168
+ if (newPassword.length < 6) { if (errEl) { errEl.textContent = t('settings.passwordTooShort'); errEl.style.display = 'block'; } return; }
169
+ if (btn) { btn.disabled = true; const sp = btn.querySelector('span'); if (sp) sp.textContent = t('settings.changing'); }
144
170
  try {
145
171
  await api('POST','/auth/change-password',{currentPassword,newPassword});
146
- toast('Password changed successfully','success');
172
+ toast(t('settings.passwordChanged'),'success');
147
173
  const curEl = document.getElementById('settings-current-pw') as HTMLInputElement | null;
148
174
  const newEl = document.getElementById('settings-new-pw') as HTMLInputElement | null;
149
175
  const confEl = document.getElementById('settings-confirm-pw') as HTMLInputElement | null;
@@ -151,9 +177,9 @@ export async function changePassword(): Promise<void> {
151
177
  if (newEl) newEl.value = '';
152
178
  if (confEl) confEl.value = '';
153
179
  } catch(err: unknown) {
154
- if (errEl) { errEl.textContent = err instanceof Error ? err.message : String(err); errEl.style.display = 'block'; }
180
+ if (errEl) { errEl.textContent = translateError(err instanceof Error ? err.message : String(err)); errEl.style.display = 'block'; }
155
181
  } finally {
156
- if (btn) { btn.disabled = false; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Change Password'; }
182
+ if (btn) { btn.disabled = false; const sp = btn.querySelector('span'); if (sp) sp.textContent = t('settings.changePassword'); }
157
183
  }
158
184
  }
159
185
 
@@ -161,30 +187,33 @@ export async function saveGitHubToken(): Promise<void> {
161
187
  const token = (document.getElementById('settings-github-token') as HTMLInputElement | null)?.value?.trim() || '';
162
188
  const errEl = document.getElementById('settings-github-error') as HTMLElement | null;
163
189
  const btn = document.getElementById('settings-github-btn') as HTMLButtonElement | null;
164
- if (!token) { if (errEl) { errEl.textContent = 'Please enter a token'; errEl.style.display = 'block'; } return; }
190
+ if (!token) { if (errEl) { errEl.textContent = t('settings.tokenEmpty'); errEl.style.display = 'block'; } return; }
165
191
  if (errEl) errEl.style.display = 'none';
166
- if (btn) { btn.disabled = true; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Saving…'; }
192
+ if (btn) { btn.disabled = true; const sp = btn.querySelector('span'); if (sp) sp.textContent = t('settings.saving'); }
167
193
  try {
168
194
  const data = await api('PATCH','/auth/github-token',{token});
169
195
  state.user!.has_github_token = (data as any).hasToken !== undefined ? (data as any).hasToken : true;
170
- toast('GitHub token saved','success');
196
+ toast(t('settings.tokenSaved'),'success');
171
197
  renderSettingsView();
172
198
  } catch(err: unknown) {
173
- if (errEl) { errEl.textContent = err instanceof Error ? err.message : String(err); errEl.style.display = 'block'; }
199
+ if (errEl) { errEl.textContent = translateError(err instanceof Error ? err.message : String(err)); errEl.style.display = 'block'; }
174
200
  } finally {
175
- if (btn) { btn.disabled = false; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Save Token'; }
201
+ if (btn) { btn.disabled = false; const sp = btn.querySelector('span'); if (sp) sp.textContent = t('settings.saveToken'); }
176
202
  }
177
203
  }
178
204
 
179
205
  export function clearGitHubToken(): void {
180
- confirmDialog('Clear GitHub Token?', 'Your GitHub integration will stop working.', async () => {
206
+ confirmDialog(t('settings.clearTokenTitle'), t('settings.clearTokenBody'), async () => {
181
207
  try {
182
208
  const data = await api('PATCH','/auth/github-token',{token:''});
183
209
  state.user!.has_github_token = false;
184
- toast('GitHub token cleared','success');
210
+ toast(t('settings.tokenCleared'),'success');
185
211
  renderSettingsView();
186
212
  } catch(err: unknown) {
187
- toast(err instanceof Error ? err.message : String(err),'error');
213
+ toast(translateError(err instanceof Error ? err.message : String(err)),'error');
188
214
  }
189
- });
215
+ }, false, { cancel: t('dialog.cancel'), confirm: t('dialog.confirm') });
190
216
  }
217
+
218
+ // Re-export so type-only re-exports stay usable if extended later.
219
+ export type { SupportedLocale };
package/public/styles.css CHANGED
@@ -240,6 +240,8 @@ html, body { height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Sego
240
240
  .form-error { color: #f87171; font-size: 12px; margin-top: 8px; }
241
241
 
242
242
  /* ── Buttons ── */
243
+ [hidden] { display: none !important; }
244
+
243
245
  .btn {
244
246
  display: inline-flex; align-items: center; gap: 6px; padding: 9px 16px;
245
247
  border: none; border-radius: var(--radius); cursor: pointer; font-family: inherit;