@ait-co/devtools 0.1.17 → 0.1.19

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.
@@ -1,3 +1,372 @@
1
+ //#region src/i18n/en.ts
2
+ const en = {
3
+ "panel.title": "AIT DevTools",
4
+ "panel.toggle.title": "AIT DevTools",
5
+ "panel.close": "Close",
6
+ "panel.editMode.on": "EDIT",
7
+ "panel.editMode.off": "READ-ONLY",
8
+ "panel.editMode.toggleTitle": "Toggle panel edit mode",
9
+ "panel.tabError": "Error rendering \"{tab}\" tab.",
10
+ "panel.tab.env": "Environment",
11
+ "panel.tab.presets": "Presets",
12
+ "panel.tab.viewport": "Viewport",
13
+ "panel.tab.permissions": "Permissions",
14
+ "panel.tab.notifications": "Notifications",
15
+ "panel.tab.location": "Location",
16
+ "panel.tab.device": "Device",
17
+ "panel.tab.iap": "IAP",
18
+ "panel.tab.ads": "Ads",
19
+ "panel.tab.events": "Events",
20
+ "panel.tab.analytics": "Analytics",
21
+ "panel.tab.storage": "Storage",
22
+ "common.readOnly": "Read-only — mock responses are controlled at build time.",
23
+ "toast.consent.title": "Send anonymous usage stats?",
24
+ "toast.consent.body": "We collect anonymous events only, to improve the tool. You can turn this off anytime in the Environment tab.",
25
+ "toast.consent.learnMore": "Learn more",
26
+ "toast.consent.accept": "Yes, send",
27
+ "toast.consent.deny": "No, thanks",
28
+ "env.section.platform": "Platform",
29
+ "env.row.os": "OS",
30
+ "env.row.appVersion": "App Version",
31
+ "env.row.environment": "Environment",
32
+ "env.row.locale": "Locale",
33
+ "env.section.network": "Network",
34
+ "env.row.networkStatus": "Status",
35
+ "env.section.safeArea": "Safe Area Insets",
36
+ "env.row.safeArea.top": "Top",
37
+ "env.row.safeArea.bottom": "Bottom",
38
+ "env.telemetry.section": "Telemetry",
39
+ "env.telemetry.row": "Telemetry",
40
+ "env.telemetry.on": "On",
41
+ "env.telemetry.off": "Off",
42
+ "env.telemetry.turnOn": "Turn on",
43
+ "env.telemetry.turnOff": "Turn off",
44
+ "env.telemetry.anonIdLabel": "anon_id: {value}",
45
+ "env.telemetry.anonIdNotSet": "(not yet set)",
46
+ "env.telemetry.anonIdCopyTitle": "Click to copy full anon_id",
47
+ "env.telemetry.deleteBtn": "Delete my data",
48
+ "env.telemetry.deleting": "Deleting…",
49
+ "env.telemetry.deleted": "Deleted",
50
+ "env.telemetry.deleteFailedRetry": "Delete failed (please retry)",
51
+ "env.telemetry.deleteFailed": "Delete failed",
52
+ "env.telemetry.privacyLink": "Privacy policy",
53
+ "env.section.language": "Language",
54
+ "env.language.row": "Language",
55
+ "env.language.ko": "한국어",
56
+ "env.language.en": "English",
57
+ "permissions.section.device": "Device Permissions",
58
+ "location.section.current": "Current Location",
59
+ "location.row.latitude": "Latitude",
60
+ "location.row.longitude": "Longitude",
61
+ "location.row.accuracy": "Accuracy",
62
+ "device.section.modes": "Device API Modes",
63
+ "device.row.camera": "Camera",
64
+ "device.row.photos": "Photos",
65
+ "device.row.location": "Location",
66
+ "device.row.network": "Network",
67
+ "device.row.clipboard": "Clipboard",
68
+ "device.section.mockImages": "Mock Images ({count})",
69
+ "device.btn.add": "+ Add",
70
+ "device.btn.useDefaults": "Use defaults",
71
+ "device.btn.clear": "Clear",
72
+ "device.prompt.camera.title": "Camera Prompt — Select an image",
73
+ "device.prompt.photos.title": "Photos Prompt — Select images",
74
+ "device.prompt.location.title": "Location Prompt — Enter coordinates",
75
+ "device.prompt.locationUpdate.title": "Location Update — Send coordinates",
76
+ "device.prompt.fallbackTitle": "Prompt: {type}",
77
+ "device.prompt.label.lat": "Lat",
78
+ "device.prompt.label.lng": "Lng",
79
+ "device.prompt.send": "Send",
80
+ "device.prompt.cancel": "Cancel",
81
+ "viewport.section.device": "Device",
82
+ "viewport.row.preset": "Preset",
83
+ "viewport.row.orientation": "Orientation",
84
+ "viewport.row.notchSide": "Notch side",
85
+ "viewport.section.custom": "Custom size",
86
+ "viewport.row.width": "Width (px)",
87
+ "viewport.row.height": "Height (px)",
88
+ "viewport.section.appearance": "Appearance",
89
+ "viewport.row.showFrame": "Show frame",
90
+ "viewport.row.showAitNavBar": "Show Apps in Toss nav bar",
91
+ "viewport.row.navBarType": "Nav bar type",
92
+ "viewport.status.noConstraint": "No viewport constraint — body fills the window.",
93
+ "viewport.status.cssPhysical": "CSS / physical",
94
+ "viewport.status.safeArea": "Safe area",
95
+ "viewport.status.aitNavBar": "AIT nav bar",
96
+ "viewport.status.aitNavBarValue": "{height}px (excl. SafeArea) · {type}",
97
+ "viewport.orientation.autoSuffix": "{orient} (auto)",
98
+ "iap.section.simulator": "IAP Simulator",
99
+ "iap.row.nextResult": "Next Purchase Result",
100
+ "iap.section.tossPay": "TossPay",
101
+ "iap.row.tossPayResult": "Next Payment Result",
102
+ "iap.section.pending": "Pending Orders ({count})",
103
+ "iap.empty.pending": "(no pending orders)",
104
+ "iap.section.completed": "Completed Orders ({count})",
105
+ "iap.empty.completed": "(no completed orders)",
106
+ "iap.btn.complete": "Complete",
107
+ "iap.label.pending": "PENDING",
108
+ "events.section.navigation": "Navigation Events",
109
+ "events.btn.triggerBack": "Trigger Back Event",
110
+ "events.btn.triggerHome": "Trigger Home Event",
111
+ "events.section.login": "Login",
112
+ "events.row.loggedIn": "Logged In",
113
+ "events.row.tossLoginIntegrated": "Toss Login Integrated",
114
+ "analytics.section.log": "Analytics Log ({count})",
115
+ "analytics.btn.clear": "Clear",
116
+ "storage.section.title": "Storage ({count} items)",
117
+ "storage.btn.clearAll": "Clear All",
118
+ "storage.empty": "No items in storage",
119
+ "presets.section.builtIn": "Built-in scenarios",
120
+ "presets.section.saved": "Saved presets ({count})",
121
+ "presets.section.save": "Save",
122
+ "presets.save.description": "Capture network / permissions / auth / IAP / ads / payment slices.",
123
+ "presets.btn.saveCurrent": "Save current as preset",
124
+ "presets.btn.apply": "Apply",
125
+ "presets.btn.reApply": "Re-apply",
126
+ "presets.btn.delete": "Delete",
127
+ "presets.empty.saved": "No saved presets yet.",
128
+ "presets.empty.builtIn": "No built-in presets.",
129
+ "presets.prompt.label": "Preset label?",
130
+ "presets.confirm.delete": "Delete preset \"{label}\"?",
131
+ "ads.section.state": "Ads State",
132
+ "ads.row.isLoaded": "isLoaded",
133
+ "ads.row.forceNoFill": "Force \"no fill\"",
134
+ "ads.empty.events": "No events yet",
135
+ "ads.section.googleAdMob": "GoogleAdMob",
136
+ "ads.section.tossAds": "TossAds",
137
+ "ads.section.fullScreenAd": "FullScreenAd",
138
+ "ads.btn.load": "Load",
139
+ "ads.btn.show": "Show",
140
+ "notifications.section.title": "requestNotificationAgreement",
141
+ "notifications.option.newAgreement": "newAgreement (first-time agree)",
142
+ "notifications.option.alreadyAgreed": "alreadyAgreed (already opted-in)",
143
+ "notifications.option.agreementRejected": "agreementRejected (user declined)"
144
+ };
145
+ //#endregion
146
+ //#region src/i18n/ko.ts
147
+ const ko = {
148
+ "panel.title": "AIT DevTools",
149
+ "panel.toggle.title": "AIT DevTools",
150
+ "panel.close": "Close",
151
+ "panel.editMode.on": "EDIT",
152
+ "panel.editMode.off": "READ-ONLY",
153
+ "panel.editMode.toggleTitle": "패널 편집 모드 전환",
154
+ "panel.tabError": "\"{tab}\" 탭 렌더링 중 오류가 발생했습니다.",
155
+ "panel.tab.env": "Environment",
156
+ "panel.tab.presets": "Presets",
157
+ "panel.tab.viewport": "Viewport",
158
+ "panel.tab.permissions": "Permissions",
159
+ "panel.tab.notifications": "Notifications",
160
+ "panel.tab.location": "Location",
161
+ "panel.tab.device": "Device",
162
+ "panel.tab.iap": "IAP",
163
+ "panel.tab.ads": "Ads",
164
+ "panel.tab.events": "Events",
165
+ "panel.tab.analytics": "Analytics",
166
+ "panel.tab.storage": "Storage",
167
+ "common.readOnly": "읽기 전용 — mock 응답은 빌드 타임에 고정됩니다.",
168
+ "toast.consent.title": "익명 사용 통계를 보낼까요?",
169
+ "toast.consent.body": "도구 개선을 위해 익명 이벤트만 수집해요. 언제든 환경 탭에서 끌 수 있어요.",
170
+ "toast.consent.learnMore": "더 알아보기",
171
+ "toast.consent.accept": "네, 보낼게요",
172
+ "toast.consent.deny": "아니요",
173
+ "env.section.platform": "Platform",
174
+ "env.row.os": "OS",
175
+ "env.row.appVersion": "App Version",
176
+ "env.row.environment": "Environment",
177
+ "env.row.locale": "Locale",
178
+ "env.section.network": "Network",
179
+ "env.row.networkStatus": "Status",
180
+ "env.section.safeArea": "Safe Area Insets",
181
+ "env.row.safeArea.top": "Top",
182
+ "env.row.safeArea.bottom": "Bottom",
183
+ "env.telemetry.section": "Telemetry",
184
+ "env.telemetry.row": "Telemetry",
185
+ "env.telemetry.on": "On",
186
+ "env.telemetry.off": "Off",
187
+ "env.telemetry.turnOn": "Turn on",
188
+ "env.telemetry.turnOff": "Turn off",
189
+ "env.telemetry.anonIdLabel": "anon_id: {value}",
190
+ "env.telemetry.anonIdNotSet": "(not yet set)",
191
+ "env.telemetry.anonIdCopyTitle": "전체 anon_id 복사",
192
+ "env.telemetry.deleteBtn": "내 데이터 삭제",
193
+ "env.telemetry.deleting": "삭제 중…",
194
+ "env.telemetry.deleted": "삭제 완료",
195
+ "env.telemetry.deleteFailedRetry": "삭제 실패 (다시 시도해주세요)",
196
+ "env.telemetry.deleteFailed": "삭제 실패",
197
+ "env.telemetry.privacyLink": "개인정보 처리방침",
198
+ "env.section.language": "Language",
199
+ "env.language.row": "Language",
200
+ "env.language.ko": "한국어",
201
+ "env.language.en": "English",
202
+ "permissions.section.device": "Device Permissions",
203
+ "location.section.current": "Current Location",
204
+ "location.row.latitude": "Latitude",
205
+ "location.row.longitude": "Longitude",
206
+ "location.row.accuracy": "Accuracy",
207
+ "device.section.modes": "Device API Modes",
208
+ "device.row.camera": "Camera",
209
+ "device.row.photos": "Photos",
210
+ "device.row.location": "Location",
211
+ "device.row.network": "Network",
212
+ "device.row.clipboard": "Clipboard",
213
+ "device.section.mockImages": "Mock Images ({count})",
214
+ "device.btn.add": "+ Add",
215
+ "device.btn.useDefaults": "Use defaults",
216
+ "device.btn.clear": "Clear",
217
+ "device.prompt.camera.title": "Camera Prompt — 이미지를 선택하세요",
218
+ "device.prompt.photos.title": "Photos Prompt — 이미지를 선택하세요",
219
+ "device.prompt.location.title": "Location Prompt — 좌표 입력",
220
+ "device.prompt.locationUpdate.title": "Location Update — 좌표 전송",
221
+ "device.prompt.fallbackTitle": "Prompt: {type}",
222
+ "device.prompt.label.lat": "Lat",
223
+ "device.prompt.label.lng": "Lng",
224
+ "device.prompt.send": "Send",
225
+ "device.prompt.cancel": "Cancel",
226
+ "viewport.section.device": "Device",
227
+ "viewport.row.preset": "Preset",
228
+ "viewport.row.orientation": "Orientation",
229
+ "viewport.row.notchSide": "Notch side",
230
+ "viewport.section.custom": "Custom size",
231
+ "viewport.row.width": "Width (px)",
232
+ "viewport.row.height": "Height (px)",
233
+ "viewport.section.appearance": "Appearance",
234
+ "viewport.row.showFrame": "Show frame",
235
+ "viewport.row.showAitNavBar": "Apps in Toss 내비게이션 바 표시",
236
+ "viewport.row.navBarType": "Nav bar type",
237
+ "viewport.status.noConstraint": "뷰포트 제약 없음 — body가 창을 가득 채웁니다.",
238
+ "viewport.status.cssPhysical": "CSS / physical",
239
+ "viewport.status.safeArea": "Safe area",
240
+ "viewport.status.aitNavBar": "AIT nav bar",
241
+ "viewport.status.aitNavBarValue": "{height}px (excl. SafeArea) · {type}",
242
+ "viewport.orientation.autoSuffix": "{orient} (auto)",
243
+ "iap.section.simulator": "IAP Simulator",
244
+ "iap.row.nextResult": "Next Purchase Result",
245
+ "iap.section.tossPay": "TossPay",
246
+ "iap.row.tossPayResult": "Next Payment Result",
247
+ "iap.section.pending": "Pending Orders ({count})",
248
+ "iap.empty.pending": "(대기 중인 주문 없음)",
249
+ "iap.section.completed": "Completed Orders ({count})",
250
+ "iap.empty.completed": "(완료된 주문 없음)",
251
+ "iap.btn.complete": "Complete",
252
+ "iap.label.pending": "PENDING",
253
+ "events.section.navigation": "Navigation Events",
254
+ "events.btn.triggerBack": "Back 이벤트 발생",
255
+ "events.btn.triggerHome": "Home 이벤트 발생",
256
+ "events.section.login": "Login",
257
+ "events.row.loggedIn": "Logged In",
258
+ "events.row.tossLoginIntegrated": "Toss Login Integrated",
259
+ "analytics.section.log": "Analytics Log ({count})",
260
+ "analytics.btn.clear": "Clear",
261
+ "storage.section.title": "Storage ({count} items)",
262
+ "storage.btn.clearAll": "Clear All",
263
+ "storage.empty": "저장된 항목이 없습니다",
264
+ "presets.section.builtIn": "Built-in scenarios",
265
+ "presets.section.saved": "Saved presets ({count})",
266
+ "presets.section.save": "Save",
267
+ "presets.save.description": "network / permissions / auth / IAP / ads / payment 슬라이스를 캡처합니다.",
268
+ "presets.btn.saveCurrent": "현재 상태를 프리셋으로 저장",
269
+ "presets.btn.apply": "Apply",
270
+ "presets.btn.reApply": "Re-apply",
271
+ "presets.btn.delete": "Delete",
272
+ "presets.empty.saved": "저장된 프리셋이 아직 없습니다.",
273
+ "presets.empty.builtIn": "내장 프리셋이 없습니다.",
274
+ "presets.prompt.label": "프리셋 라벨을 입력하세요",
275
+ "presets.confirm.delete": "\"{label}\" 프리셋을 삭제할까요?",
276
+ "ads.section.state": "Ads State",
277
+ "ads.row.isLoaded": "isLoaded",
278
+ "ads.row.forceNoFill": "강제 \"no fill\"",
279
+ "ads.empty.events": "아직 이벤트가 없습니다",
280
+ "ads.section.googleAdMob": "GoogleAdMob",
281
+ "ads.section.tossAds": "TossAds",
282
+ "ads.section.fullScreenAd": "FullScreenAd",
283
+ "ads.btn.load": "Load",
284
+ "ads.btn.show": "Show",
285
+ "notifications.section.title": "requestNotificationAgreement",
286
+ "notifications.option.newAgreement": "newAgreement (최초 동의)",
287
+ "notifications.option.alreadyAgreed": "alreadyAgreed (이미 동의됨)",
288
+ "notifications.option.agreementRejected": "agreementRejected (사용자 거절)"
289
+ };
290
+ //#endregion
291
+ //#region src/i18n/index.ts
292
+ /**
293
+ * Vanilla TS i18n for the floating DevTools panel.
294
+ *
295
+ * Public surface:
296
+ * - `t(key, vars?)` — look up a UI string, with `{name}` placeholder
297
+ * interpolation. Falls back to the key itself if a translation is missing.
298
+ * - `getLocale()` / `setLocale(locale)` — read/persist the active locale.
299
+ * `setLocale` dispatches `__ait:localechange` so the panel can remount.
300
+ * - `detectLocale()` — first-run heuristic from `navigator.language`.
301
+ *
302
+ * `ko` is the source of truth (keys are typed from it). `en` is also a full
303
+ * `Record<StringKey, string>` (devtools is developer-facing, en is a real
304
+ * audience). The `Partial` lookup table preserves the runtime `?? key` safety
305
+ * net even though we ship complete catalogs today.
306
+ */
307
+ const LOCALE_STORAGE_KEY = "__ait_locale";
308
+ const LOCALE_CHANGE_EVENT = "__ait:localechange";
309
+ const tables = {
310
+ ko,
311
+ en
312
+ };
313
+ let currentLocale = null;
314
+ function safeReadStorage() {
315
+ if (typeof localStorage === "undefined") return null;
316
+ try {
317
+ const raw = localStorage.getItem(LOCALE_STORAGE_KEY);
318
+ if (raw === "ko" || raw === "en") return raw;
319
+ } catch {}
320
+ return null;
321
+ }
322
+ function safeWriteStorage(locale) {
323
+ if (typeof localStorage === "undefined") return;
324
+ try {
325
+ localStorage.setItem(LOCALE_STORAGE_KEY, locale);
326
+ } catch {}
327
+ }
328
+ /**
329
+ * Read `navigator.language` and decide a locale. `ko` (and `ko-*`) → `'ko'`,
330
+ * everything else → `'en'`. Pure function; does not touch storage.
331
+ */
332
+ function detectLocale() {
333
+ if (typeof navigator === "undefined") return "en";
334
+ const lang = navigator.language ?? "";
335
+ return /^ko\b/i.test(lang) ? "ko" : "en";
336
+ }
337
+ /**
338
+ * Resolve the active locale, in order:
339
+ * 1. previously set in-memory value (set by `setLocale`)
340
+ * 2. localStorage `__ait_locale`
341
+ * 3. `detectLocale()` from navigator
342
+ */
343
+ function getLocale() {
344
+ if (currentLocale) return currentLocale;
345
+ currentLocale = safeReadStorage() ?? detectLocale();
346
+ return currentLocale;
347
+ }
348
+ /**
349
+ * Persist a locale choice and notify listeners. The panel listens for
350
+ * `__ait:localechange` and re-mounts so every string re-evaluates.
351
+ */
352
+ function setLocale(locale) {
353
+ currentLocale = locale;
354
+ safeWriteStorage(locale);
355
+ if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent(LOCALE_CHANGE_EVENT));
356
+ }
357
+ /**
358
+ * Look up a UI string for the current locale. Falls back to the key if missing,
359
+ * so a forgotten key surfaces visibly rather than rendering empty.
360
+ */
361
+ function t(key, vars) {
362
+ const raw = tables[getLocale()][key] ?? key;
363
+ if (!vars) return raw;
364
+ return raw.replace(/\{(\w+)\}/g, (match, name) => {
365
+ const value = vars[name];
366
+ return value === void 0 ? match : String(value);
367
+ });
368
+ }
369
+ //#endregion
1
370
  //#region src/mock/state.ts
2
371
  const DEFAULT_STATE = {
3
372
  platform: "ios",
@@ -224,9 +593,6 @@ if (typeof window !== "undefined") window.__ait = aitState;
224
593
  /**
225
594
  * Consent toast UI — vanilla DOM, fixed bottom-right.
226
595
  *
227
- * Ko-only for now — devtools has no i18n layer.
228
- * TODO: revisit when/if an i18n layer is added to the panel.
229
- *
230
596
  * Shows once per "undecided + reprompt window cleared" session.
231
597
  * Calls onAccept / onDeny callbacks; caller is responsible for persisting state.
232
598
  */
@@ -318,26 +684,26 @@ function showConsentToast({ onAccept, onDeny }) {
318
684
  toast.id = TOAST_ID;
319
685
  const header = document.createElement("div");
320
686
  header.className = "ait-toast-header";
321
- header.textContent = "익명 사용 통계를 보낼까요?";
687
+ header.textContent = t("toast.consent.title");
322
688
  const body = document.createElement("div");
323
689
  body.className = "ait-toast-body";
324
- body.textContent = "도구 개선을 위해 익명 이벤트만 수집해요. 언제든 환경 탭에서 끌 수 있어요.";
690
+ body.textContent = t("toast.consent.body");
325
691
  const learnMore = document.createElement("a");
326
692
  learnMore.className = "ait-toast-link";
327
693
  learnMore.href = "https://docs.aitc.dev/privacy";
328
694
  learnMore.target = "_blank";
329
695
  learnMore.rel = "noopener noreferrer";
330
- learnMore.textContent = "더 알아보기";
696
+ learnMore.textContent = t("toast.consent.learnMore");
331
697
  const yesBtn = document.createElement("button");
332
698
  yesBtn.className = "ait-toast-btn-primary";
333
- yesBtn.textContent = "Yes, send";
699
+ yesBtn.textContent = t("toast.consent.accept");
334
700
  yesBtn.addEventListener("click", () => {
335
701
  removeToast();
336
702
  onAccept();
337
703
  });
338
704
  const noBtn = document.createElement("button");
339
705
  noBtn.className = "ait-toast-btn-secondary";
340
- noBtn.textContent = "No, thanks";
706
+ noBtn.textContent = t("toast.consent.deny");
341
707
  noBtn.addEventListener("click", () => {
342
708
  removeToast();
343
709
  onDeny();
@@ -582,7 +948,7 @@ function readGlobalString(key) {
582
948
  }
583
949
  const TELEMETRY_ENDPOINT = readGlobalString("__TELEMETRY_ENDPOINT__") ?? "https://t.aitc.dev";
584
950
  function getVersion() {
585
- return "0.1.17";
951
+ return "0.1.19";
586
952
  }
587
953
  let panelVisibleSince = null;
588
954
  let accumulatedMs = 0;
@@ -691,7 +1057,7 @@ function inputRow(label, value, onChange, disabled = false) {
691
1057
  return h("div", { className: "ait-row" }, h("label", {}, label), input);
692
1058
  }
693
1059
  function monitoringNotice() {
694
- return h("div", { className: "ait-monitoring-notice" }, "Read-only — mock responses are controlled at build time.");
1060
+ return h("div", { className: "ait-monitoring-notice" }, t("common.readOnly"));
695
1061
  }
696
1062
  const PANEL_STYLES = `
697
1063
  .ait-panel-toggle {
@@ -1671,7 +2037,7 @@ function renderPromptBanner() {
1671
2037
  if (!pendingPrompt) return null;
1672
2038
  const banner = h("div", { className: "ait-prompt-banner" });
1673
2039
  if (pendingPrompt.type === "camera") {
1674
- banner.append(h("div", { className: "ait-prompt-title" }, "Camera Prompt — Select an image"));
2040
+ banner.append(h("div", { className: "ait-prompt-title" }, t("device.prompt.camera.title")));
1675
2041
  const input = h("input", {
1676
2042
  type: "file",
1677
2043
  accept: "image/*",
@@ -1686,7 +2052,7 @@ function renderPromptBanner() {
1686
2052
  });
1687
2053
  banner.appendChild(input);
1688
2054
  } else if (pendingPrompt.type === "photos") {
1689
- banner.append(h("div", { className: "ait-prompt-title" }, "Photos Prompt — Select images"));
2055
+ banner.append(h("div", { className: "ait-prompt-title" }, t("device.prompt.photos.title")));
1690
2056
  const input = h("input", {
1691
2057
  type: "file",
1692
2058
  accept: "image/*",
@@ -1704,7 +2070,7 @@ function renderPromptBanner() {
1704
2070
  });
1705
2071
  banner.appendChild(input);
1706
2072
  } else if (pendingPrompt.type === "location" || pendingPrompt.type === "location-update") {
1707
- banner.append(h("div", { className: "ait-prompt-title" }, pendingPrompt.type === "location" ? "Location Prompt — Enter coordinates" : "Location Update — Send coordinates"));
2073
+ banner.append(h("div", { className: "ait-prompt-title" }, pendingPrompt.type === "location" ? t("device.prompt.location.title") : t("device.prompt.locationUpdate.title")));
1708
2074
  const latInput = h("input", {
1709
2075
  className: "ait-input",
1710
2076
  value: String(aitState.state.location.coords.latitude),
@@ -1715,7 +2081,7 @@ function renderPromptBanner() {
1715
2081
  value: String(aitState.state.location.coords.longitude),
1716
2082
  style: "width:80px"
1717
2083
  });
1718
- const sendBtn = h("button", { className: "ait-btn ait-btn-sm" }, "Send");
2084
+ const sendBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("device.prompt.send"));
1719
2085
  sendBtn.addEventListener("click", () => {
1720
2086
  const loc = {
1721
2087
  coords: {
@@ -1731,12 +2097,12 @@ function renderPromptBanner() {
1731
2097
  };
1732
2098
  resolvePrompt(pendingPrompt.type, loc);
1733
2099
  });
1734
- banner.append(h("div", { className: "ait-prompt-input-row" }, h("label", {}, "Lat"), latInput, h("label", {}, "Lng"), lngInput, sendBtn));
1735
- } else banner.append(h("div", { className: "ait-prompt-title" }, `Prompt: ${pendingPrompt.type}`));
2100
+ banner.append(h("div", { className: "ait-prompt-input-row" }, h("label", {}, t("device.prompt.label.lat")), latInput, h("label", {}, t("device.prompt.label.lng")), lngInput, sendBtn));
2101
+ } else banner.append(h("div", { className: "ait-prompt-title" }, t("device.prompt.fallbackTitle", { type: pendingPrompt.type })));
1736
2102
  const cancelBtn = h("button", {
1737
2103
  className: "ait-btn ait-btn-sm ait-btn-danger",
1738
2104
  style: "margin-top:8px"
1739
- }, "Cancel");
2105
+ }, t("device.prompt.cancel"));
1740
2106
  cancelBtn.addEventListener("click", () => {
1741
2107
  pendingPrompt = null;
1742
2108
  window.dispatchEvent(new CustomEvent("__ait:prompt-cancel"));
@@ -1754,9 +2120,9 @@ function renderDeviceTab() {
1754
2120
  const promptBanner = renderPromptBanner();
1755
2121
  if (promptBanner) container.appendChild(promptBanner);
1756
2122
  }
1757
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Device API Modes"), ...[
2123
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("device.section.modes")), ...[
1758
2124
  {
1759
- label: "Camera",
2125
+ labelKey: "device.row.camera",
1760
2126
  key: "camera",
1761
2127
  options: [
1762
2128
  "mock",
@@ -1765,7 +2131,7 @@ function renderDeviceTab() {
1765
2131
  ]
1766
2132
  },
1767
2133
  {
1768
- label: "Photos",
2134
+ labelKey: "device.row.photos",
1769
2135
  key: "photos",
1770
2136
  options: [
1771
2137
  "mock",
@@ -1774,7 +2140,7 @@ function renderDeviceTab() {
1774
2140
  ]
1775
2141
  },
1776
2142
  {
1777
- label: "Location",
2143
+ labelKey: "device.row.location",
1778
2144
  key: "location",
1779
2145
  options: [
1780
2146
  "mock",
@@ -1783,16 +2149,16 @@ function renderDeviceTab() {
1783
2149
  ]
1784
2150
  },
1785
2151
  {
1786
- label: "Network",
2152
+ labelKey: "device.row.network",
1787
2153
  key: "network",
1788
2154
  options: ["mock", "web"]
1789
2155
  },
1790
2156
  {
1791
- label: "Clipboard",
2157
+ labelKey: "device.row.clipboard",
1792
2158
  key: "clipboard",
1793
2159
  options: ["mock", "web"]
1794
2160
  }
1795
- ].map((entry) => selectRow(entry.label, entry.options, s.deviceModes[entry.key], (v) => {
2161
+ ].map((entry) => selectRow(t(entry.labelKey), entry.options, s.deviceModes[entry.key], (v) => {
1796
2162
  aitState.patch("deviceModes", { [entry.key]: v });
1797
2163
  }, disabled))));
1798
2164
  const images = s.mockData.images;
@@ -1810,7 +2176,7 @@ function renderDeviceTab() {
1810
2176
  thumb.append(img, removeBtn);
1811
2177
  imageGrid.appendChild(thumb);
1812
2178
  });
1813
- const addBtn = h("button", { className: "ait-btn-secondary" }, "+ Add");
2179
+ const addBtn = h("button", { className: "ait-btn-secondary" }, t("device.btn.add"));
1814
2180
  addBtn.addEventListener("click", () => {
1815
2181
  const input = document.createElement("input");
1816
2182
  input.type = "file";
@@ -1829,17 +2195,17 @@ function renderDeviceTab() {
1829
2195
  input.click();
1830
2196
  });
1831
2197
  if (disabled) addBtn.disabled = true;
1832
- const defaultsBtn = h("button", { className: "ait-btn-secondary" }, "Use defaults");
2198
+ const defaultsBtn = h("button", { className: "ait-btn-secondary" }, t("device.btn.useDefaults"));
1833
2199
  defaultsBtn.addEventListener("click", () => {
1834
2200
  aitState.patch("mockData", { images: [...getDefaultPlaceholderImages()] });
1835
2201
  });
1836
2202
  if (disabled) defaultsBtn.disabled = true;
1837
- const clearImagesBtn = h("button", { className: "ait-btn-secondary" }, "Clear");
2203
+ const clearImagesBtn = h("button", { className: "ait-btn-secondary" }, t("device.btn.clear"));
1838
2204
  clearImagesBtn.addEventListener("click", () => {
1839
2205
  aitState.patch("mockData", { images: [] });
1840
2206
  });
1841
2207
  if (disabled) clearImagesBtn.disabled = true;
1842
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, `Mock Images (${images.length})`), imageGrid, h("div", { className: "ait-btn-row" }, addBtn, defaultsBtn, clearImagesBtn)));
2208
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("device.section.mockImages", { count: images.length })), imageGrid, h("div", { className: "ait-btn-row" }, addBtn, defaultsBtn, clearImagesBtn)));
1843
2209
  return container;
1844
2210
  }
1845
2211
  //#endregion
@@ -1956,7 +2322,7 @@ function statusRow(label, value) {
1956
2322
  }
1957
2323
  function lastEventLine() {
1958
2324
  const last = aitState.state.ads.lastEvent;
1959
- if (!last) return h("div", { className: "ait-log-entry" }, h("span", { style: "color:#555" }, "No events yet"));
2325
+ if (!last) return h("div", { className: "ait-log-entry" }, h("span", { style: "color:#555" }, t("ads.empty.events")));
1960
2326
  const time = new Date(last.timestamp).toLocaleTimeString();
1961
2327
  return h("div", { className: "ait-log-entry" }, h("span", {
1962
2328
  className: "ait-log-type",
@@ -1964,8 +2330,8 @@ function lastEventLine() {
1964
2330
  }, last.type), h("span", { className: "ait-log-time" }, time));
1965
2331
  }
1966
2332
  function adSection(title, onLoad, onShow, disabled) {
1967
- const loadBtn = h("button", { className: "ait-btn ait-btn-sm" }, "Load");
1968
- const showBtn = h("button", { className: "ait-btn ait-btn-sm" }, "Show");
2333
+ const loadBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("ads.btn.load"));
2334
+ const showBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("ads.btn.show"));
1969
2335
  if (disabled) {
1970
2336
  loadBtn.disabled = true;
1971
2337
  showBtn.disabled = true;
@@ -1988,7 +2354,7 @@ function renderAdsTab() {
1988
2354
  forceNoFillCb.addEventListener("change", () => {
1989
2355
  aitState.patch("ads", { forceNoFill: forceNoFillCb.checked });
1990
2356
  });
1991
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Ads State"), statusRow("isLoaded", String(s.ads.isLoaded)), h("div", { className: "ait-row" }, h("label", {}, "Force \"no fill\""), forceNoFillCb), lastEventLine()), adSection("GoogleAdMob", () => {
2357
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("ads.section.state")), statusRow(t("ads.row.isLoaded"), String(s.ads.isLoaded)), h("div", { className: "ait-row" }, h("label", {}, t("ads.row.forceNoFill")), forceNoFillCb), lastEventLine()), adSection(t("ads.section.googleAdMob"), () => {
1992
2358
  GoogleAdMob.loadAppsInTossAdMob({
1993
2359
  onEvent: (e) => recordEvent(e.type),
1994
2360
  onError: (err) => recordError(err.message)
@@ -1998,7 +2364,7 @@ function renderAdsTab() {
1998
2364
  onEvent: (e) => recordEvent(e.type),
1999
2365
  onError: (err) => recordError(err.message)
2000
2366
  });
2001
- }, disabled), adSection("TossAds", () => {
2367
+ }, disabled), adSection(t("ads.section.tossAds"), () => {
2002
2368
  if (aitState.state.ads.forceNoFill) {
2003
2369
  recordError("No fill");
2004
2370
  return;
@@ -2015,7 +2381,7 @@ function renderAdsTab() {
2015
2381
  recordEvent("dismissed");
2016
2382
  aitState.patch("ads", { isLoaded: false });
2017
2383
  }, 1500);
2018
- }, disabled), adSection("FullScreenAd", () => {
2384
+ }, disabled), adSection(t("ads.section.fullScreenAd"), () => {
2019
2385
  loadFullScreenAd({
2020
2386
  onEvent: (e) => recordEvent(e.type),
2021
2387
  onError: (err) => recordError(err.message)
@@ -2035,13 +2401,13 @@ function renderAnalyticsTab() {
2035
2401
  const container = h("div");
2036
2402
  if (disabled) container.appendChild(monitoringNotice());
2037
2403
  const logs = aitState.state.analyticsLog;
2038
- const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "Clear");
2404
+ const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("analytics.btn.clear"));
2039
2405
  if (disabled) clearBtn.disabled = true;
2040
2406
  clearBtn.addEventListener("click", () => {
2041
2407
  aitState.update({ analyticsLog: [] });
2042
2408
  });
2043
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-row" }, h("div", { className: "ait-section-title" }, `Analytics Log (${logs.length})`), clearBtn), ...logs.slice(-30).reverse().map((entry) => {
2044
- return h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-time" }, new Date(entry.timestamp).toLocaleTimeString("ko-KR", { hour12: false })), h("span", { className: "ait-log-type" }, entry.type), JSON.stringify(entry.params));
2409
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-row" }, h("div", { className: "ait-section-title" }, t("analytics.section.log", { count: logs.length })), clearBtn), ...logs.slice(-30).reverse().map((entry) => {
2410
+ return h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-time" }, new Date(entry.timestamp).toLocaleTimeString()), h("span", { className: "ait-log-type" }, entry.type), JSON.stringify(entry.params));
2045
2411
  })));
2046
2412
  return container;
2047
2413
  }
@@ -2052,7 +2418,7 @@ function renderEnvironmentTab() {
2052
2418
  const disabled = !s.panelEditable;
2053
2419
  const container = h("div");
2054
2420
  if (disabled) container.appendChild(monitoringNotice());
2055
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Platform"), selectRow("OS", ["ios", "android"], s.platform, (v) => aitState.update({ platform: v }), disabled), inputRow("App Version", s.appVersion, (v) => aitState.update({ appVersion: v }), disabled), selectRow("Environment", ["toss", "sandbox"], s.environment, (v) => aitState.update({ environment: v }), disabled), inputRow("Locale", s.locale, (v) => aitState.update({ locale: v }), disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Network"), selectRow("Status", [
2421
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.section.platform")), selectRow(t("env.row.os"), ["ios", "android"], s.platform, (v) => aitState.update({ platform: v }), disabled), inputRow(t("env.row.appVersion"), s.appVersion, (v) => aitState.update({ appVersion: v }), disabled), selectRow(t("env.row.environment"), ["toss", "sandbox"], s.environment, (v) => aitState.update({ environment: v }), disabled), inputRow(t("env.row.locale"), s.locale, (v) => aitState.update({ locale: v }), disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.section.network")), selectRow(t("env.row.networkStatus"), [
2056
2422
  "WIFI",
2057
2423
  "4G",
2058
2424
  "5G",
@@ -2061,39 +2427,61 @@ function renderEnvironmentTab() {
2061
2427
  "OFFLINE",
2062
2428
  "WWAN",
2063
2429
  "UNKNOWN"
2064
- ], s.networkStatus, (v) => aitState.update({ networkStatus: v }), disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Safe Area Insets"), inputRow("Top", String(s.safeAreaInsets.top), (v) => aitState.patch("safeAreaInsets", { top: Number(v) }), disabled), inputRow("Bottom", String(s.safeAreaInsets.bottom), (v) => aitState.patch("safeAreaInsets", { bottom: Number(v) }), disabled)), buildTelemetrySection());
2430
+ ], s.networkStatus, (v) => aitState.update({ networkStatus: v }), disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.section.safeArea")), inputRow(t("env.row.safeArea.top"), String(s.safeAreaInsets.top), (v) => aitState.patch("safeAreaInsets", { top: Number(v) }), disabled), inputRow(t("env.row.safeArea.bottom"), String(s.safeAreaInsets.bottom), (v) => aitState.patch("safeAreaInsets", { bottom: Number(v) }), disabled)), buildLanguageSection(), buildTelemetrySection());
2065
2431
  return container;
2066
2432
  }
2433
+ function buildLanguageSection() {
2434
+ const current = getLocale();
2435
+ const select = h("select", { className: "ait-select" });
2436
+ for (const opt of [{
2437
+ value: "ko",
2438
+ labelKey: "env.language.ko"
2439
+ }, {
2440
+ value: "en",
2441
+ labelKey: "env.language.en"
2442
+ }]) {
2443
+ const option = h("option", { value: opt.value }, t(opt.labelKey));
2444
+ if (opt.value === current) option.selected = true;
2445
+ select.appendChild(option);
2446
+ }
2447
+ select.addEventListener("change", () => {
2448
+ setLocale(select.value);
2449
+ });
2450
+ return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.section.language")), h("div", { className: "ait-row" }, h("label", {}, t("env.language.row")), select));
2451
+ }
2067
2452
  function buildTelemetrySection() {
2068
2453
  const isGranted = readConsentState() === "granted";
2069
- const statusLabel = h("span", { style: `font-size:12px;font-weight:600;color:${isGranted ? "#4ade80" : "#888"}` }, isGranted ? "On" : "Off");
2454
+ const statusLabel = h("span", { style: `font-size:12px;font-weight:600;color:${isGranted ? "#4ade80" : "#888"}` }, isGranted ? t("env.telemetry.on") : t("env.telemetry.off"));
2070
2455
  const toggleBtn = h("button", {
2071
2456
  className: "ait-btn ait-btn-sm",
2072
2457
  style: "font-size:11px"
2073
- }, isGranted ? "Turn off" : "Turn on");
2458
+ }, isGranted ? t("env.telemetry.turnOff") : t("env.telemetry.turnOn"));
2074
2459
  toggleBtn.addEventListener("click", () => {
2075
2460
  setConsentViaToggle(!isGranted);
2076
2461
  window.dispatchEvent(new CustomEvent("__ait:panel-switch-tab", { detail: { tab: "env" } }));
2077
2462
  });
2078
- const statusRow = h("div", { className: "ait-row" }, h("label", {}, "Telemetry"), h("span", { style: "display:flex;align-items:center;gap:8px" }, statusLabel, toggleBtn));
2079
- const anonId = localStorage.getItem("__ait_telemetry:anon_id") ?? "(not yet set)";
2463
+ const statusRow = h("div", { className: "ait-row" }, h("label", {}, t("env.telemetry.row")), h("span", { style: "display:flex;align-items:center;gap:8px" }, statusLabel, toggleBtn));
2464
+ const rawAnonId = localStorage.getItem("__ait_telemetry:anon_id");
2465
+ const displayAnonId = rawAnonId ?? t("env.telemetry.anonIdNotSet");
2466
+ const truncatedId = displayAnonId.length > 8 ? `${displayAnonId.slice(0, 8)}…` : displayAnonId;
2080
2467
  const anonIdEl = h("span", {
2081
2468
  style: "font-family:'SF Mono','Menlo',monospace;font-size:11px;color:#95e6cb;cursor:pointer",
2082
- title: "Click to copy full anon_id"
2083
- }, `anon_id: ${anonId.length > 8 ? `${anonId.slice(0, 8)}…` : anonId}`);
2469
+ title: t("env.telemetry.anonIdCopyTitle")
2470
+ }, t("env.telemetry.anonIdLabel", { value: truncatedId }));
2084
2471
  anonIdEl.addEventListener("click", () => {
2085
- navigator.clipboard.writeText(anonId).catch(() => {});
2472
+ if (!rawAnonId) return;
2473
+ navigator.clipboard.writeText(rawAnonId).catch(() => {});
2086
2474
  });
2087
- const deleteBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "내 데이터 삭제");
2475
+ const deleteBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("env.telemetry.deleteBtn"));
2088
2476
  const deleteStatus = h("span", { style: "font-size:11px;color:#aaa" });
2089
2477
  deleteBtn.addEventListener("click", () => {
2090
2478
  deleteBtn.disabled = true;
2091
- deleteStatus.textContent = "삭제 중…";
2479
+ deleteStatus.textContent = t("env.telemetry.deleting");
2092
2480
  deleteMyData(TELEMETRY_ENDPOINT).then((ok) => {
2093
- deleteStatus.textContent = ok ? "삭제 완료" : "삭제 실패 (다시 시도해주세요)";
2481
+ deleteStatus.textContent = ok ? t("env.telemetry.deleted") : t("env.telemetry.deleteFailedRetry");
2094
2482
  deleteBtn.disabled = false;
2095
2483
  }).catch(() => {
2096
- deleteStatus.textContent = "삭제 실패";
2484
+ deleteStatus.textContent = t("env.telemetry.deleteFailed");
2097
2485
  deleteBtn.disabled = false;
2098
2486
  });
2099
2487
  });
@@ -2103,8 +2491,8 @@ function buildTelemetrySection() {
2103
2491
  rel: "noopener noreferrer",
2104
2492
  style: "font-size:11px;color:#666;text-decoration:none"
2105
2493
  });
2106
- privacyLink.textContent = "개인정보 처리방침";
2107
- return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Telemetry"), statusRow, h("div", { style: "margin-bottom:6px" }, anonIdEl), h("div", {
2494
+ privacyLink.textContent = t("env.telemetry.privacyLink");
2495
+ return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.telemetry.section")), statusRow, h("div", { style: "margin-bottom:6px" }, anonIdEl), h("div", {
2108
2496
  className: "ait-btn-row",
2109
2497
  style: "align-items:center;gap:8px;margin-top:6px"
2110
2498
  }, deleteBtn, deleteStatus), h("div", { style: "margin-top:8px" }, privacyLink));
@@ -2115,15 +2503,15 @@ function renderEventsTab() {
2115
2503
  const disabled = !aitState.state.panelEditable;
2116
2504
  const container = h("div");
2117
2505
  if (disabled) container.appendChild(monitoringNotice());
2118
- const backBtn = h("button", { className: "ait-btn" }, "Trigger Back Event");
2506
+ const backBtn = h("button", { className: "ait-btn" }, t("events.btn.triggerBack"));
2119
2507
  backBtn.addEventListener("click", () => aitState.trigger("backEvent"));
2120
2508
  if (disabled) backBtn.disabled = true;
2121
- const homeBtn = h("button", { className: "ait-btn" }, "Trigger Home Event");
2509
+ const homeBtn = h("button", { className: "ait-btn" }, t("events.btn.triggerHome"));
2122
2510
  homeBtn.addEventListener("click", () => aitState.trigger("homeEvent"));
2123
2511
  if (disabled) homeBtn.disabled = true;
2124
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Navigation Events"), h("div", { className: "ait-row" }, backBtn, homeBtn)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Login"), selectRow("Logged In", ["true", "false"], String(aitState.state.auth.isLoggedIn), (v) => {
2512
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("events.section.navigation")), h("div", { className: "ait-row" }, backBtn, homeBtn)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("events.section.login")), selectRow(t("events.row.loggedIn"), ["true", "false"], String(aitState.state.auth.isLoggedIn), (v) => {
2125
2513
  aitState.patch("auth", { isLoggedIn: v === "true" });
2126
- }, disabled), selectRow("Toss Login Integrated", ["true", "false"], String(aitState.state.auth.isTossLoginIntegrated), (v) => {
2514
+ }, disabled), selectRow(t("events.row.tossLoginIntegrated"), ["true", "false"], String(aitState.state.auth.isTossLoginIntegrated), (v) => {
2127
2515
  aitState.patch("auth", { isTossLoginIntegrated: v === "true" });
2128
2516
  }, disabled)));
2129
2517
  return container;
@@ -2267,23 +2655,23 @@ function renderIapTab() {
2267
2655
  ];
2268
2656
  if (disabled) container.appendChild(monitoringNotice());
2269
2657
  const pendingOrders = s.iap.pendingOrders;
2270
- const pendingSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, `Pending Orders (${pendingOrders.length})`));
2271
- if (pendingOrders.length === 0) pendingSection.appendChild(h("div", { className: "ait-log-entry" }, "(no pending orders)"));
2658
+ const pendingSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.pending", { count: pendingOrders.length })));
2659
+ if (pendingOrders.length === 0) pendingSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.pending")));
2272
2660
  else for (const o of pendingOrders) {
2273
- const completeBtn = h("button", { className: "ait-btn ait-btn-sm" }, "Complete");
2661
+ const completeBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("iap.btn.complete"));
2274
2662
  if (disabled) completeBtn.disabled = true;
2275
2663
  completeBtn.addEventListener("click", () => {
2276
2664
  IAP.completeProductGrant({ params: { orderId: o.orderId } }).catch((err) => console.error("[@ait-co/devtools] completeProductGrant error:", err));
2277
2665
  });
2278
- pendingSection.appendChild(h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-type" }, "PENDING"), `${o.sku} (${shortOrderId(o.orderId)}) · ${formatTimestamp(o.paymentCompletedDate)} `, completeBtn));
2666
+ pendingSection.appendChild(h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-type" }, t("iap.label.pending")), `${o.sku} (${shortOrderId(o.orderId)}) · ${formatTimestamp(o.paymentCompletedDate)} `, completeBtn));
2279
2667
  }
2280
2668
  const completedOrders = s.iap.completedOrders;
2281
- const completedSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, `Completed Orders (${completedOrders.length})`));
2282
- if (completedOrders.length === 0) completedSection.appendChild(h("div", { className: "ait-log-entry" }, "(no completed orders)"));
2669
+ const completedSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.completed", { count: completedOrders.length })));
2670
+ if (completedOrders.length === 0) completedSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.completed")));
2283
2671
  else for (const o of completedOrders) completedSection.appendChild(h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-type" }, o.status), `${o.sku} (${shortOrderId(o.orderId)}) · ${formatTimestamp(o.date)}`));
2284
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "IAP Simulator"), selectRow("Next Purchase Result", results, s.iap.nextResult, (v) => {
2672
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.simulator")), selectRow(t("iap.row.nextResult"), results, s.iap.nextResult, (v) => {
2285
2673
  aitState.patch("iap", { nextResult: v });
2286
- }, disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "TossPay"), selectRow("Next Payment Result", ["success", "fail"], s.payment.nextResult, (v) => {
2674
+ }, disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.tossPay")), selectRow(t("iap.row.tossPayResult"), ["success", "fail"], s.payment.nextResult, (v) => {
2287
2675
  aitState.patch("payment", { nextResult: v });
2288
2676
  }, disabled)), pendingSection, completedSection);
2289
2677
  return container;
@@ -2295,19 +2683,19 @@ function renderLocationTab() {
2295
2683
  const disabled = !s.panelEditable;
2296
2684
  const container = h("div");
2297
2685
  if (disabled) container.appendChild(monitoringNotice());
2298
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Current Location"), inputRow("Latitude", String(s.location.coords.latitude), (v) => {
2686
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("location.section.current")), inputRow(t("location.row.latitude"), String(s.location.coords.latitude), (v) => {
2299
2687
  const coords = {
2300
2688
  ...s.location.coords,
2301
2689
  latitude: Number(v)
2302
2690
  };
2303
2691
  aitState.patch("location", { coords });
2304
- }, disabled), inputRow("Longitude", String(s.location.coords.longitude), (v) => {
2692
+ }, disabled), inputRow(t("location.row.longitude"), String(s.location.coords.longitude), (v) => {
2305
2693
  const coords = {
2306
2694
  ...s.location.coords,
2307
2695
  longitude: Number(v)
2308
2696
  };
2309
2697
  aitState.patch("location", { coords });
2310
- }, disabled), inputRow("Accuracy", String(s.location.coords.accuracy), (v) => {
2698
+ }, disabled), inputRow(t("location.row.accuracy"), String(s.location.coords.accuracy), (v) => {
2311
2699
  const coords = {
2312
2700
  ...s.location.coords,
2313
2701
  accuracy: Number(v)
@@ -2321,15 +2709,15 @@ function renderLocationTab() {
2321
2709
  const RESULTS = [
2322
2710
  {
2323
2711
  value: "newAgreement",
2324
- label: "newAgreement (first-time agree)"
2712
+ labelKey: "notifications.option.newAgreement"
2325
2713
  },
2326
2714
  {
2327
2715
  value: "alreadyAgreed",
2328
- label: "alreadyAgreed (already opted-in)"
2716
+ labelKey: "notifications.option.alreadyAgreed"
2329
2717
  },
2330
2718
  {
2331
2719
  value: "agreementRejected",
2332
- label: "agreementRejected (user declined)"
2720
+ labelKey: "notifications.option.agreementRejected"
2333
2721
  }
2334
2722
  ];
2335
2723
  function radioRow(name, current, option, disabled) {
@@ -2343,14 +2731,14 @@ function radioRow(name, current, option, disabled) {
2343
2731
  input.addEventListener("change", () => {
2344
2732
  if (input.checked) aitState.patch("notification", { nextResult: option.value });
2345
2733
  });
2346
- return h("label", { className: "ait-row" }, input, h("span", {}, option.label));
2734
+ return h("label", { className: "ait-row" }, input, h("span", {}, t(option.labelKey)));
2347
2735
  }
2348
2736
  function renderNotificationsTab() {
2349
2737
  const s = aitState.state;
2350
2738
  const disabled = !s.panelEditable;
2351
2739
  const container = h("div");
2352
2740
  if (disabled) container.appendChild(monitoringNotice());
2353
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "requestNotificationAgreement"), ...RESULTS.map((opt) => radioRow("ait-notification-result", s.notification.nextResult, opt, disabled))));
2741
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("notifications.section.title")), ...RESULTS.map((opt) => radioRow("ait-notification-result", s.notification.nextResult, opt, disabled))));
2354
2742
  return container;
2355
2743
  }
2356
2744
  //#endregion
@@ -2373,7 +2761,7 @@ function renderPermissionsTab() {
2373
2761
  "notDetermined"
2374
2762
  ];
2375
2763
  if (disabled) container.appendChild(monitoringNotice());
2376
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Device Permissions"), ...names.map((name) => selectRow(name, statuses, s.permissions[name], (v) => {
2764
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("permissions.section.device")), ...names.map((name) => selectRow(name, statuses, s.permissions[name], (v) => {
2377
2765
  aitState.patch("permissions", { [name]: v });
2378
2766
  }, disabled))));
2379
2767
  return container;
@@ -2661,12 +3049,12 @@ function renderPresetsTab(refreshPanel) {
2661
3049
  if (disabled) container.appendChild(monitoringNotice());
2662
3050
  const userPresets = listUserPresets();
2663
3051
  const snapshot = aitState.state;
2664
- container.append(renderSection("Built-in scenarios", builtInPresets, disabled, snapshot, refreshPanel, false));
2665
- container.append(renderSection(`Saved presets (${userPresets.length})`, userPresets, disabled, snapshot, refreshPanel, true));
2666
- const saveBtn = h("button", { className: "ait-btn ait-btn-sm" }, "Save current as preset");
3052
+ container.append(renderSection(t("presets.section.builtIn"), builtInPresets, disabled, snapshot, refreshPanel, false));
3053
+ container.append(renderSection(t("presets.section.saved", { count: userPresets.length }), userPresets, disabled, snapshot, refreshPanel, true));
3054
+ const saveBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("presets.btn.saveCurrent"));
2667
3055
  if (disabled) saveBtn.disabled = true;
2668
3056
  saveBtn.addEventListener("click", () => {
2669
- const label = window.prompt("Preset label?");
3057
+ const label = window.prompt(t("presets.prompt.label"));
2670
3058
  if (label === null) return;
2671
3059
  try {
2672
3060
  saveUserPreset(label, captureCurrentState(aitState.state));
@@ -2676,19 +3064,19 @@ function renderPresetsTab(refreshPanel) {
2676
3064
  }
2677
3065
  refreshPanel();
2678
3066
  });
2679
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Save"), h("div", { style: "color:#888;font-size:11px;margin-bottom:6px" }, "Capture network / permissions / auth / IAP / ads / payment slices."), saveBtn));
3067
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("presets.section.save")), h("div", { style: "color:#888;font-size:11px;margin-bottom:6px" }, t("presets.save.description")), saveBtn));
2680
3068
  return container;
2681
3069
  }
2682
3070
  function renderSection(title, presets, disabled, snapshot, refreshPanel, deletable) {
2683
3071
  const section = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, title));
2684
3072
  if (presets.length === 0) {
2685
- section.append(h("div", { style: "color:#555;font-size:12px" }, deletable ? "No saved presets yet." : "No built-in presets."));
3073
+ section.append(h("div", { style: "color:#555;font-size:12px" }, deletable ? t("presets.empty.saved") : t("presets.empty.builtIn")));
2686
3074
  return section;
2687
3075
  }
2688
3076
  for (const preset of presets) {
2689
3077
  const isActive = matchesPreset(snapshot, preset.state);
2690
3078
  const labelEl = h("span", { className: "ait-preset-label" }, isActive ? `✓ ${preset.label}` : preset.label);
2691
- const applyBtn = h("button", { className: "ait-btn ait-btn-sm" }, isActive ? "Re-apply" : "Apply");
3079
+ const applyBtn = h("button", { className: "ait-btn ait-btn-sm" }, isActive ? t("presets.btn.reApply") : t("presets.btn.apply"));
2692
3080
  if (disabled) applyBtn.disabled = true;
2693
3081
  applyBtn.addEventListener("click", () => {
2694
3082
  applyPreset(preset.state);
@@ -2696,10 +3084,10 @@ function renderSection(title, presets, disabled, snapshot, refreshPanel, deletab
2696
3084
  });
2697
3085
  const buttons = [applyBtn];
2698
3086
  if (deletable) {
2699
- const delBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "Delete");
3087
+ const delBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("presets.btn.delete"));
2700
3088
  if (disabled) delBtn.disabled = true;
2701
3089
  delBtn.addEventListener("click", () => {
2702
- if (!window.confirm(`Delete preset "${preset.label}"?`)) return;
3090
+ if (!window.confirm(t("presets.confirm.delete", { label: preset.label }))) return;
2703
3091
  deleteUserPreset(preset.id);
2704
3092
  refreshPanel();
2705
3093
  });
@@ -2724,13 +3112,13 @@ function renderStorageTab(refreshPanel) {
2724
3112
  const key = localStorage.key(i);
2725
3113
  if (key?.startsWith(prefix)) entries.push([key.slice(14), localStorage.getItem(key) ?? ""]);
2726
3114
  }
2727
- const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "Clear All");
3115
+ const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("storage.btn.clearAll"));
2728
3116
  if (disabled) clearBtn.disabled = true;
2729
3117
  clearBtn.addEventListener("click", () => {
2730
3118
  for (const [key] of entries) localStorage.removeItem(prefix + key);
2731
3119
  refreshPanel();
2732
3120
  });
2733
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-row" }, h("div", { className: "ait-section-title" }, `Storage (${entries.length} items)`), clearBtn), entries.length === 0 ? h("div", { style: "color:#555;font-size:12px" }, "No items in storage") : h("div", {}, ...entries.map(([key, value]) => h("div", { className: "ait-storage-row" }, h("span", { className: "ait-storage-key" }, key), h("span", { className: "ait-storage-value" }, value.length > 100 ? `${value.slice(0, 100)}...` : value))))));
3121
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-row" }, h("div", { className: "ait-section-title" }, t("storage.section.title", { count: entries.length })), clearBtn), entries.length === 0 ? h("div", { style: "color:#555;font-size:12px" }, t("storage.empty")) : h("div", {}, ...entries.map(([key, value]) => h("div", { className: "ait-storage-row" }, h("span", { className: "ait-storage-key" }, key), h("span", { className: "ait-storage-value" }, value.length > 100 ? `${value.slice(0, 100)}...` : value))))));
2734
3122
  return container;
2735
3123
  }
2736
3124
  //#endregion
@@ -2771,8 +3159,9 @@ const NONE_PRESET = {
2771
3159
  };
2772
3160
  /**
2773
3161
  * Device presets (2026). CSS viewport 크기는 실제 기기의 `window.innerWidth/innerHeight`.
2774
- * iPhone 17 시리즈는 2025-09 출시. iPhone Air Galaxy S26 시리즈는 2026-04 기준 미출시라
2775
- * 추정 값(`(est)` 라벨 표기). 실제 출시 값을 갱신한다.
3162
+ * iPhone 17 시리즈는 2025-09 출시. iPhone Air 2026-04 기준 미출시 추정(`(est)` 라벨).
3163
+ * Galaxy S26 시리즈는 2026-03-11 출시 viewport 값은 phone-simulator.com에서 보고된
3164
+ * 측정치를 사용. safe area는 토스 호스트 환경 실측 필요 — S25 값으로 잠정.
2776
3165
  *
2777
3166
  * iPhone 17과 17 Pro는 CSS viewport / DPR / safe area가 동일 — 이는 의도이며 카피-페이스트
2778
3167
  * 실수가 아니다. Apple의 17 lineup은 base와 Pro의 web-relevant 스펙이 같다.
@@ -2841,9 +3230,9 @@ const VIEWPORT_PRESETS = [
2841
3230
  },
2842
3231
  {
2843
3232
  id: "galaxy-s26",
2844
- label: "Galaxy S26 (S25 fallback)",
2845
- width: 384,
2846
- height: 832,
3233
+ label: "Galaxy S26",
3234
+ width: 360,
3235
+ height: 773,
2847
3236
  dpr: 3,
2848
3237
  notch: "punch-hole-center",
2849
3238
  safeAreaTop: 32,
@@ -2851,9 +3240,9 @@ const VIEWPORT_PRESETS = [
2851
3240
  },
2852
3241
  {
2853
3242
  id: "galaxy-s26-plus",
2854
- label: "Galaxy S26+ (S25 fallback)",
2855
- width: 412,
2856
- height: 915,
3243
+ label: "Galaxy S26+",
3244
+ width: 480,
3245
+ height: 1040,
2857
3246
  dpr: 3,
2858
3247
  notch: "punch-hole-center",
2859
3248
  safeAreaTop: 32,
@@ -2861,10 +3250,10 @@ const VIEWPORT_PRESETS = [
2861
3250
  },
2862
3251
  {
2863
3252
  id: "galaxy-s26-ultra",
2864
- label: "Galaxy S26 Ultra (S25 fallback)",
2865
- width: 412,
2866
- height: 915,
2867
- dpr: 3.5,
3253
+ label: "Galaxy S26 Ultra",
3254
+ width: 480,
3255
+ height: 1040,
3256
+ dpr: 3,
2868
3257
  notch: "punch-hole-center",
2869
3258
  safeAreaTop: 40,
2870
3259
  safeAreaBottom: 0
@@ -3339,7 +3728,7 @@ function renderViewportTab() {
3339
3728
  heightInput.value = String(clamped);
3340
3729
  }
3341
3730
  });
3342
- customRow.append(h("div", { className: "ait-section-title" }, "Custom size"), h("div", { className: "ait-row" }, h("label", {}, "Width (px)"), widthInput), h("div", { className: "ait-row" }, h("label", {}, "Height (px)"), heightInput));
3731
+ customRow.append(h("div", { className: "ait-section-title" }, t("viewport.section.custom")), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.width")), widthInput), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.height")), heightInput));
3343
3732
  }
3344
3733
  const frameCheckbox = h("input", { type: "checkbox" });
3345
3734
  frameCheckbox.checked = vp.frame;
@@ -3365,7 +3754,7 @@ function renderViewportTab() {
3365
3754
  });
3366
3755
  const size = resolveViewportSize(vp);
3367
3756
  const statusEl = h("div", { className: "ait-section" });
3368
- if (vp.preset === "none" || size.width === 0) statusEl.appendChild(h("div", { style: "color:#888;font-size:11px" }, "No viewport constraint — body fills the window."));
3757
+ if (vp.preset === "none" || size.width === 0) statusEl.appendChild(h("div", { style: "color:#888;font-size:11px" }, t("viewport.status.noConstraint")));
3369
3758
  else {
3370
3759
  const preset = vp.preset === "custom" ? null : getPreset(vp.preset);
3371
3760
  const effOrient = effectiveOrientation(vp);
@@ -3374,75 +3763,84 @@ function renderViewportTab() {
3374
3763
  const dpr = preset?.dpr ?? 1;
3375
3764
  const physW = Math.round(size.width * dpr);
3376
3765
  const physH = Math.round(size.height * dpr);
3377
- const orientDisplay = vp.orientation === "auto" ? `${effOrient} (auto)` : effOrient;
3378
- rows.push(h("div", { className: "ait-status-row" }, h("span", {}, "CSS / physical"), h("span", { className: "ait-status-value" }, `${size.width}×${size.height}@${dpr}x | ${physW}×${physH} ${orientDisplay}`)));
3766
+ const orientDisplay = vp.orientation === "auto" ? t("viewport.orientation.autoSuffix", { orient: effOrient }) : effOrient;
3767
+ rows.push(h("div", { className: "ait-status-row" }, h("span", {}, t("viewport.status.cssPhysical")), h("span", { className: "ait-status-value" }, `${size.width}×${size.height}@${dpr}x | ${physW}×${physH} ${orientDisplay}`)));
3379
3768
  if (preset) {
3380
3769
  const insets = computeSafeAreaInsets(preset, landscape, vp.landscapeSide);
3381
- rows.push(h("div", { className: "ait-status-row" }, h("span", {}, "Safe area"), h("span", { className: "ait-status-value" }, `T${insets.top} R${insets.right} B${insets.bottom} L${insets.left}`)));
3770
+ rows.push(h("div", { className: "ait-status-row" }, h("span", {}, t("viewport.status.safeArea")), h("span", { className: "ait-status-value" }, `T${insets.top} R${insets.right} B${insets.bottom} L${insets.left}`)));
3382
3771
  }
3383
- if (vp.aitNavBar && !landscape) rows.push(h("div", { className: "ait-status-row" }, h("span", {}, "AIT nav bar"), h("span", { className: "ait-status-value" }, `48px (excl. SafeArea) · ${vp.aitNavBarType}`)));
3772
+ if (vp.aitNavBar && !landscape) rows.push(h("div", { className: "ait-status-row" }, h("span", {}, t("viewport.status.aitNavBar")), h("span", { className: "ait-status-value" }, t("viewport.status.aitNavBarValue", {
3773
+ height: 48,
3774
+ type: vp.aitNavBarType
3775
+ }))));
3384
3776
  for (const row of rows) statusEl.appendChild(row);
3385
3777
  }
3386
- const deviceSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Device"), h("div", { className: "ait-row" }, h("label", {}, "Preset"), presetSelect), h("div", { className: "ait-row" }, h("label", {}, "Orientation"), orientationSelect));
3778
+ const deviceSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("viewport.section.device")), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.preset")), presetSelect), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.orientation")), orientationSelect));
3387
3779
  if (effectiveOrientation(vp) === "landscape" && vp.preset !== "none" && vp.preset !== "custom") {
3388
3780
  const notch = getPreset(vp.preset).notch;
3389
- if (notch === "notch" || notch === "dynamic-island") deviceSection.appendChild(h("div", { className: "ait-row" }, h("label", {}, "Notch side"), landscapeSideSelect));
3781
+ if (notch === "notch" || notch === "dynamic-island") deviceSection.appendChild(h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.notchSide")), landscapeSideSelect));
3390
3782
  }
3391
- container.append(deviceSection, customRow, h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Appearance"), h("div", { className: "ait-row" }, h("label", {}, "Show frame"), frameCheckbox), h("div", { className: "ait-row" }, h("label", {}, "Show Apps in Toss nav bar"), navBarCheckbox), h("div", { className: "ait-row" }, h("label", {}, "Nav bar type"), navBarTypeSelect)), statusEl);
3783
+ container.append(deviceSection, customRow, h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("viewport.section.appearance")), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.showFrame")), frameCheckbox), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.showAitNavBar")), navBarCheckbox), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.navBarType")), navBarTypeSelect)), statusEl);
3392
3784
  return container;
3393
3785
  }
3394
3786
  //#endregion
3395
3787
  //#region src/panel/tabs/index.ts
3396
- const TABS = [
3788
+ const TAB_DEFS = [
3397
3789
  {
3398
3790
  id: "env",
3399
- label: "Environment"
3791
+ labelKey: "panel.tab.env"
3400
3792
  },
3401
3793
  {
3402
3794
  id: "presets",
3403
- label: "Presets"
3795
+ labelKey: "panel.tab.presets"
3404
3796
  },
3405
3797
  {
3406
3798
  id: "viewport",
3407
- label: "Viewport"
3799
+ labelKey: "panel.tab.viewport"
3408
3800
  },
3409
3801
  {
3410
3802
  id: "permissions",
3411
- label: "Permissions"
3803
+ labelKey: "panel.tab.permissions"
3412
3804
  },
3413
3805
  {
3414
3806
  id: "notifications",
3415
- label: "Notifications"
3807
+ labelKey: "panel.tab.notifications"
3416
3808
  },
3417
3809
  {
3418
3810
  id: "location",
3419
- label: "Location"
3811
+ labelKey: "panel.tab.location"
3420
3812
  },
3421
3813
  {
3422
3814
  id: "device",
3423
- label: "Device"
3815
+ labelKey: "panel.tab.device"
3424
3816
  },
3425
3817
  {
3426
3818
  id: "iap",
3427
- label: "IAP"
3819
+ labelKey: "panel.tab.iap"
3428
3820
  },
3429
3821
  {
3430
3822
  id: "ads",
3431
- label: "Ads"
3823
+ labelKey: "panel.tab.ads"
3432
3824
  },
3433
3825
  {
3434
3826
  id: "events",
3435
- label: "Events"
3827
+ labelKey: "panel.tab.events"
3436
3828
  },
3437
3829
  {
3438
3830
  id: "analytics",
3439
- label: "Analytics"
3831
+ labelKey: "panel.tab.analytics"
3440
3832
  },
3441
3833
  {
3442
3834
  id: "storage",
3443
- label: "Storage"
3835
+ labelKey: "panel.tab.storage"
3444
3836
  }
3445
3837
  ];
3838
+ function getTabs() {
3839
+ return TAB_DEFS.map((def) => ({
3840
+ id: def.id,
3841
+ label: t(def.labelKey)
3842
+ }));
3843
+ }
3446
3844
  function createTabRenderers(refreshPanel) {
3447
3845
  return {
3448
3846
  env: renderEnvironmentTab,
@@ -3604,6 +4002,7 @@ let injectedStyle = null;
3604
4002
  let panelSwitchTabHandler = null;
3605
4003
  let resizeHandler = null;
3606
4004
  let aitStateUnsubscribe = null;
4005
+ let localeChangeHandler = null;
3607
4006
  let tabRenderers = null;
3608
4007
  function refreshPanel() {
3609
4008
  if (!bodyEl || !tabsEl) return;
@@ -3613,7 +4012,7 @@ function refreshPanel() {
3613
4012
  bodyEl.appendChild(tabRenderers[currentTab]());
3614
4013
  } catch (err) {
3615
4014
  console.error(`[@ait-co/devtools] Error rendering tab "${currentTab}":`, err);
3616
- bodyEl.appendChild(h("div", { className: "ait-panel-tab-error" }, `Error rendering "${currentTab}" tab.`));
4015
+ bodyEl.appendChild(h("div", { className: "ait-panel-tab-error" }, t("panel.tabError", { tab: currentTab })));
3617
4016
  }
3618
4017
  tabsEl.querySelectorAll(".ait-panel-tab").forEach((el) => {
3619
4018
  el.classList.toggle("active", el.getAttribute("data-tab") === currentTab);
@@ -3629,14 +4028,14 @@ function mount() {
3629
4028
  document.head.appendChild(injectedStyle);
3630
4029
  const toggle = h("button", {
3631
4030
  className: "ait-panel-toggle",
3632
- title: "AIT DevTools"
4031
+ title: t("panel.toggle.title")
3633
4032
  }, "AIT");
3634
4033
  toggleEl = toggle;
3635
4034
  restoreButtonPosition(toggle);
3636
4035
  panelEl = h("div", { className: "ait-panel" });
3637
4036
  const closeBtn = h("button", {
3638
4037
  className: "ait-panel-close",
3639
- title: "Close"
4038
+ title: t("panel.close")
3640
4039
  }, "×");
3641
4040
  closeBtn.addEventListener("click", () => {
3642
4041
  isOpen = false;
@@ -3645,18 +4044,18 @@ function mount() {
3645
4044
  });
3646
4045
  const mockBadge = h("span", {
3647
4046
  className: `ait-mock-badge ${aitState.state.panelEditable ? "ait-mock-badge-on" : "ait-mock-badge-off"}`,
3648
- title: "Toggle panel edit mode"
3649
- }, aitState.state.panelEditable ? "EDIT" : "READ-ONLY");
4047
+ title: t("panel.editMode.toggleTitle")
4048
+ }, aitState.state.panelEditable ? t("panel.editMode.on") : t("panel.editMode.off"));
3650
4049
  mockBadge.addEventListener("click", () => {
3651
4050
  aitState.update({ panelEditable: !aitState.state.panelEditable });
3652
4051
  mockBadge.className = `ait-mock-badge ${aitState.state.panelEditable ? "ait-mock-badge-on" : "ait-mock-badge-off"}`;
3653
- mockBadge.textContent = aitState.state.panelEditable ? "EDIT" : "READ-ONLY";
4052
+ mockBadge.textContent = aitState.state.panelEditable ? t("panel.editMode.on") : t("panel.editMode.off");
3654
4053
  refreshPanel();
3655
4054
  });
3656
- const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.17`), closeBtn);
3657
- const header = h("div", { className: "ait-panel-header" }, h("span", {}, "AIT DevTools"), headerRight);
4055
+ const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.19`), closeBtn);
4056
+ const header = h("div", { className: "ait-panel-header" }, h("span", {}, t("panel.title")), headerRight);
3658
4057
  tabsEl = h("div", { className: "ait-panel-tabs" });
3659
- for (const tab of TABS) {
4058
+ for (const tab of getTabs()) {
3660
4059
  const tabEl = h("button", {
3661
4060
  className: "ait-panel-tab",
3662
4061
  "data-tab": tab.id
@@ -3709,6 +4108,23 @@ function mount() {
3709
4108
  refreshPanel();
3710
4109
  };
3711
4110
  window.addEventListener("__ait:panel-switch-tab", panelSwitchTabHandler);
4111
+ localeChangeHandler = () => {
4112
+ const savedTab = currentTab;
4113
+ const savedOpen = isOpen;
4114
+ disposePanel();
4115
+ try {
4116
+ mount();
4117
+ currentTab = savedTab;
4118
+ if (savedOpen && panelEl) {
4119
+ isOpen = true;
4120
+ panelEl.classList.add("open");
4121
+ }
4122
+ refreshPanel();
4123
+ } catch (err) {
4124
+ console.error("[@ait-co/devtools] Failed to re-mount after locale change:", err);
4125
+ }
4126
+ };
4127
+ window.addEventListener(LOCALE_CHANGE_EVENT, localeChangeHandler);
3712
4128
  refreshPanel();
3713
4129
  telemetry.init();
3714
4130
  }
@@ -3724,6 +4140,7 @@ function disposePanel() {
3724
4140
  if (typeof document === "undefined") return;
3725
4141
  if (panelSwitchTabHandler && typeof window !== "undefined") window.removeEventListener("__ait:panel-switch-tab", panelSwitchTabHandler);
3726
4142
  if (resizeHandler && typeof window !== "undefined") window.removeEventListener("resize", resizeHandler);
4143
+ if (localeChangeHandler && typeof window !== "undefined") window.removeEventListener(LOCALE_CHANGE_EVENT, localeChangeHandler);
3727
4144
  if (aitStateUnsubscribe) aitStateUnsubscribe();
3728
4145
  toggleEl?.remove();
3729
4146
  panelEl?.remove();
@@ -3732,6 +4149,7 @@ function disposePanel() {
3732
4149
  setDeviceRefreshPanel(() => {});
3733
4150
  panelSwitchTabHandler = null;
3734
4151
  resizeHandler = null;
4152
+ localeChangeHandler = null;
3735
4153
  aitStateUnsubscribe = null;
3736
4154
  toggleEl = null;
3737
4155
  panelEl = null;