@ait-co/devtools 0.1.18 → 0.1.20

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,384 @@
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.t0Row": "Anonymous usage signal (Tier 0)",
40
+ "env.telemetry.t0On": "On",
41
+ "env.telemetry.t0Off": "Off",
42
+ "env.telemetry.t0TurnOn": "Turn on",
43
+ "env.telemetry.t0TurnOff": "Turn off",
44
+ "env.telemetry.t0Desc": "Version + date only, no PII. Once per day. Helps improve the package.",
45
+ "env.telemetry.row": "Extended telemetry (Tier 1)",
46
+ "env.telemetry.on": "On",
47
+ "env.telemetry.off": "Off",
48
+ "env.telemetry.turnOn": "Turn on",
49
+ "env.telemetry.turnOff": "Turn off",
50
+ "env.telemetry.anonIdLabel": "anon_id: {value}",
51
+ "env.telemetry.anonIdNotSet": "(not yet set)",
52
+ "env.telemetry.anonIdCopyTitle": "Click to copy full anon_id",
53
+ "env.telemetry.deleteBtn": "Delete my data",
54
+ "env.telemetry.deleting": "Deleting…",
55
+ "env.telemetry.deleted": "Deleted",
56
+ "env.telemetry.deleteFailedRetry": "Delete failed (please retry)",
57
+ "env.telemetry.deleteFailed": "Delete failed",
58
+ "env.telemetry.privacyLink": "Privacy policy →",
59
+ "env.section.language": "Language",
60
+ "env.language.row": "Language",
61
+ "env.language.ko": "한국어",
62
+ "env.language.en": "English",
63
+ "permissions.section.device": "Device Permissions",
64
+ "location.section.current": "Current Location",
65
+ "location.row.latitude": "Latitude",
66
+ "location.row.longitude": "Longitude",
67
+ "location.row.accuracy": "Accuracy",
68
+ "device.section.modes": "Device API Modes",
69
+ "device.row.camera": "Camera",
70
+ "device.row.photos": "Photos",
71
+ "device.row.location": "Location",
72
+ "device.row.network": "Network",
73
+ "device.row.clipboard": "Clipboard",
74
+ "device.section.mockImages": "Mock Images ({count})",
75
+ "device.btn.add": "+ Add",
76
+ "device.btn.useDefaults": "Use defaults",
77
+ "device.btn.clear": "Clear",
78
+ "device.prompt.camera.title": "Camera Prompt — Select an image",
79
+ "device.prompt.photos.title": "Photos Prompt — Select images",
80
+ "device.prompt.location.title": "Location Prompt — Enter coordinates",
81
+ "device.prompt.locationUpdate.title": "Location Update — Send coordinates",
82
+ "device.prompt.fallbackTitle": "Prompt: {type}",
83
+ "device.prompt.label.lat": "Lat",
84
+ "device.prompt.label.lng": "Lng",
85
+ "device.prompt.send": "Send",
86
+ "device.prompt.cancel": "Cancel",
87
+ "viewport.section.device": "Device",
88
+ "viewport.row.preset": "Preset",
89
+ "viewport.row.orientation": "Orientation",
90
+ "viewport.row.notchSide": "Notch side",
91
+ "viewport.section.custom": "Custom size",
92
+ "viewport.row.width": "Width (px)",
93
+ "viewport.row.height": "Height (px)",
94
+ "viewport.section.appearance": "Appearance",
95
+ "viewport.row.showFrame": "Show frame",
96
+ "viewport.row.showAitNavBar": "Show Apps in Toss nav bar",
97
+ "viewport.row.navBarType": "Nav bar type",
98
+ "viewport.status.noConstraint": "No viewport constraint — body fills the window.",
99
+ "viewport.status.cssPhysical": "CSS / physical",
100
+ "viewport.status.safeArea": "Safe area",
101
+ "viewport.status.aitNavBar": "AIT nav bar",
102
+ "viewport.status.aitNavBarValue": "{height}px (excl. SafeArea) · {type}",
103
+ "viewport.orientation.autoSuffix": "{orient} (auto)",
104
+ "iap.section.simulator": "IAP Simulator",
105
+ "iap.row.nextResult": "Next Purchase Result",
106
+ "iap.section.tossPay": "TossPay",
107
+ "iap.row.tossPayResult": "Next Payment Result",
108
+ "iap.section.pending": "Pending Orders ({count})",
109
+ "iap.empty.pending": "(no pending orders)",
110
+ "iap.section.completed": "Completed Orders ({count})",
111
+ "iap.empty.completed": "(no completed orders)",
112
+ "iap.btn.complete": "Complete",
113
+ "iap.label.pending": "PENDING",
114
+ "events.section.navigation": "Navigation Events",
115
+ "events.btn.triggerBack": "Trigger Back Event",
116
+ "events.btn.triggerHome": "Trigger Home Event",
117
+ "events.section.login": "Login",
118
+ "events.row.loggedIn": "Logged In",
119
+ "events.row.tossLoginIntegrated": "Toss Login Integrated",
120
+ "analytics.section.log": "Analytics Log ({count})",
121
+ "analytics.btn.clear": "Clear",
122
+ "storage.section.title": "Storage ({count} items)",
123
+ "storage.btn.clearAll": "Clear All",
124
+ "storage.empty": "No items in storage",
125
+ "presets.section.builtIn": "Built-in scenarios",
126
+ "presets.section.saved": "Saved presets ({count})",
127
+ "presets.section.save": "Save",
128
+ "presets.save.description": "Capture network / permissions / auth / IAP / ads / payment slices.",
129
+ "presets.btn.saveCurrent": "Save current as preset",
130
+ "presets.btn.apply": "Apply",
131
+ "presets.btn.reApply": "Re-apply",
132
+ "presets.btn.delete": "Delete",
133
+ "presets.empty.saved": "No saved presets yet.",
134
+ "presets.empty.builtIn": "No built-in presets.",
135
+ "presets.prompt.label": "Preset label?",
136
+ "presets.confirm.delete": "Delete preset \"{label}\"?",
137
+ "ads.section.state": "Ads State",
138
+ "ads.row.isLoaded": "isLoaded",
139
+ "ads.row.forceNoFill": "Force \"no fill\"",
140
+ "ads.empty.events": "No events yet",
141
+ "ads.section.googleAdMob": "GoogleAdMob",
142
+ "ads.section.tossAds": "TossAds",
143
+ "ads.section.fullScreenAd": "FullScreenAd",
144
+ "ads.btn.load": "Load",
145
+ "ads.btn.show": "Show",
146
+ "notifications.section.title": "requestNotificationAgreement",
147
+ "notifications.option.newAgreement": "newAgreement (first-time agree)",
148
+ "notifications.option.alreadyAgreed": "alreadyAgreed (already opted-in)",
149
+ "notifications.option.agreementRejected": "agreementRejected (user declined)"
150
+ };
151
+ //#endregion
152
+ //#region src/i18n/ko.ts
153
+ const ko = {
154
+ "panel.title": "AIT DevTools",
155
+ "panel.toggle.title": "AIT DevTools",
156
+ "panel.close": "Close",
157
+ "panel.editMode.on": "EDIT",
158
+ "panel.editMode.off": "READ-ONLY",
159
+ "panel.editMode.toggleTitle": "패널 편집 모드 전환",
160
+ "panel.tabError": "\"{tab}\" 탭 렌더링 중 오류가 발생했습니다.",
161
+ "panel.tab.env": "Environment",
162
+ "panel.tab.presets": "Presets",
163
+ "panel.tab.viewport": "Viewport",
164
+ "panel.tab.permissions": "Permissions",
165
+ "panel.tab.notifications": "Notifications",
166
+ "panel.tab.location": "Location",
167
+ "panel.tab.device": "Device",
168
+ "panel.tab.iap": "IAP",
169
+ "panel.tab.ads": "Ads",
170
+ "panel.tab.events": "Events",
171
+ "panel.tab.analytics": "Analytics",
172
+ "panel.tab.storage": "Storage",
173
+ "common.readOnly": "읽기 전용 — mock 응답은 빌드 타임에 고정됩니다.",
174
+ "toast.consent.title": "익명 사용 통계를 보낼까요?",
175
+ "toast.consent.body": "도구 개선을 위해 익명 이벤트만 수집해요. 언제든 환경 탭에서 끌 수 있어요.",
176
+ "toast.consent.learnMore": "더 알아보기",
177
+ "toast.consent.accept": "네, 보낼게요",
178
+ "toast.consent.deny": "아니요",
179
+ "env.section.platform": "Platform",
180
+ "env.row.os": "OS",
181
+ "env.row.appVersion": "App Version",
182
+ "env.row.environment": "Environment",
183
+ "env.row.locale": "Locale",
184
+ "env.section.network": "Network",
185
+ "env.row.networkStatus": "Status",
186
+ "env.section.safeArea": "Safe Area Insets",
187
+ "env.row.safeArea.top": "Top",
188
+ "env.row.safeArea.bottom": "Bottom",
189
+ "env.telemetry.section": "Telemetry",
190
+ "env.telemetry.t0Row": "익명 사용 신호 (Tier 0)",
191
+ "env.telemetry.t0On": "On",
192
+ "env.telemetry.t0Off": "Off",
193
+ "env.telemetry.t0TurnOn": "Turn on",
194
+ "env.telemetry.t0TurnOff": "Turn off",
195
+ "env.telemetry.t0Desc": "버전·날짜만 수집, PII 없음. 하루 1회. 패키지 개선에 사용됩니다.",
196
+ "env.telemetry.row": "확장 텔레메트리 (Tier 1)",
197
+ "env.telemetry.on": "On",
198
+ "env.telemetry.off": "Off",
199
+ "env.telemetry.turnOn": "Turn on",
200
+ "env.telemetry.turnOff": "Turn off",
201
+ "env.telemetry.anonIdLabel": "anon_id: {value}",
202
+ "env.telemetry.anonIdNotSet": "(not yet set)",
203
+ "env.telemetry.anonIdCopyTitle": "전체 anon_id 복사",
204
+ "env.telemetry.deleteBtn": "내 데이터 삭제",
205
+ "env.telemetry.deleting": "삭제 중…",
206
+ "env.telemetry.deleted": "삭제 완료",
207
+ "env.telemetry.deleteFailedRetry": "삭제 실패 (다시 시도해주세요)",
208
+ "env.telemetry.deleteFailed": "삭제 실패",
209
+ "env.telemetry.privacyLink": "개인정보 처리방침 →",
210
+ "env.section.language": "Language",
211
+ "env.language.row": "Language",
212
+ "env.language.ko": "한국어",
213
+ "env.language.en": "English",
214
+ "permissions.section.device": "Device Permissions",
215
+ "location.section.current": "Current Location",
216
+ "location.row.latitude": "Latitude",
217
+ "location.row.longitude": "Longitude",
218
+ "location.row.accuracy": "Accuracy",
219
+ "device.section.modes": "Device API Modes",
220
+ "device.row.camera": "Camera",
221
+ "device.row.photos": "Photos",
222
+ "device.row.location": "Location",
223
+ "device.row.network": "Network",
224
+ "device.row.clipboard": "Clipboard",
225
+ "device.section.mockImages": "Mock Images ({count})",
226
+ "device.btn.add": "+ Add",
227
+ "device.btn.useDefaults": "Use defaults",
228
+ "device.btn.clear": "Clear",
229
+ "device.prompt.camera.title": "Camera Prompt — 이미지를 선택하세요",
230
+ "device.prompt.photos.title": "Photos Prompt — 이미지를 선택하세요",
231
+ "device.prompt.location.title": "Location Prompt — 좌표 입력",
232
+ "device.prompt.locationUpdate.title": "Location Update — 좌표 전송",
233
+ "device.prompt.fallbackTitle": "Prompt: {type}",
234
+ "device.prompt.label.lat": "Lat",
235
+ "device.prompt.label.lng": "Lng",
236
+ "device.prompt.send": "Send",
237
+ "device.prompt.cancel": "Cancel",
238
+ "viewport.section.device": "Device",
239
+ "viewport.row.preset": "Preset",
240
+ "viewport.row.orientation": "Orientation",
241
+ "viewport.row.notchSide": "Notch side",
242
+ "viewport.section.custom": "Custom size",
243
+ "viewport.row.width": "Width (px)",
244
+ "viewport.row.height": "Height (px)",
245
+ "viewport.section.appearance": "Appearance",
246
+ "viewport.row.showFrame": "Show frame",
247
+ "viewport.row.showAitNavBar": "Apps in Toss 내비게이션 바 표시",
248
+ "viewport.row.navBarType": "Nav bar type",
249
+ "viewport.status.noConstraint": "뷰포트 제약 없음 — body가 창을 가득 채웁니다.",
250
+ "viewport.status.cssPhysical": "CSS / physical",
251
+ "viewport.status.safeArea": "Safe area",
252
+ "viewport.status.aitNavBar": "AIT nav bar",
253
+ "viewport.status.aitNavBarValue": "{height}px (excl. SafeArea) · {type}",
254
+ "viewport.orientation.autoSuffix": "{orient} (auto)",
255
+ "iap.section.simulator": "IAP Simulator",
256
+ "iap.row.nextResult": "Next Purchase Result",
257
+ "iap.section.tossPay": "TossPay",
258
+ "iap.row.tossPayResult": "Next Payment Result",
259
+ "iap.section.pending": "Pending Orders ({count})",
260
+ "iap.empty.pending": "(대기 중인 주문 없음)",
261
+ "iap.section.completed": "Completed Orders ({count})",
262
+ "iap.empty.completed": "(완료된 주문 없음)",
263
+ "iap.btn.complete": "Complete",
264
+ "iap.label.pending": "PENDING",
265
+ "events.section.navigation": "Navigation Events",
266
+ "events.btn.triggerBack": "Back 이벤트 발생",
267
+ "events.btn.triggerHome": "Home 이벤트 발생",
268
+ "events.section.login": "Login",
269
+ "events.row.loggedIn": "Logged In",
270
+ "events.row.tossLoginIntegrated": "Toss Login Integrated",
271
+ "analytics.section.log": "Analytics Log ({count})",
272
+ "analytics.btn.clear": "Clear",
273
+ "storage.section.title": "Storage ({count} items)",
274
+ "storage.btn.clearAll": "Clear All",
275
+ "storage.empty": "저장된 항목이 없습니다",
276
+ "presets.section.builtIn": "Built-in scenarios",
277
+ "presets.section.saved": "Saved presets ({count})",
278
+ "presets.section.save": "Save",
279
+ "presets.save.description": "network / permissions / auth / IAP / ads / payment 슬라이스를 캡처합니다.",
280
+ "presets.btn.saveCurrent": "현재 상태를 프리셋으로 저장",
281
+ "presets.btn.apply": "Apply",
282
+ "presets.btn.reApply": "Re-apply",
283
+ "presets.btn.delete": "Delete",
284
+ "presets.empty.saved": "저장된 프리셋이 아직 없습니다.",
285
+ "presets.empty.builtIn": "내장 프리셋이 없습니다.",
286
+ "presets.prompt.label": "프리셋 라벨을 입력하세요",
287
+ "presets.confirm.delete": "\"{label}\" 프리셋을 삭제할까요?",
288
+ "ads.section.state": "Ads State",
289
+ "ads.row.isLoaded": "isLoaded",
290
+ "ads.row.forceNoFill": "강제 \"no fill\"",
291
+ "ads.empty.events": "아직 이벤트가 없습니다",
292
+ "ads.section.googleAdMob": "GoogleAdMob",
293
+ "ads.section.tossAds": "TossAds",
294
+ "ads.section.fullScreenAd": "FullScreenAd",
295
+ "ads.btn.load": "Load",
296
+ "ads.btn.show": "Show",
297
+ "notifications.section.title": "requestNotificationAgreement",
298
+ "notifications.option.newAgreement": "newAgreement (최초 동의)",
299
+ "notifications.option.alreadyAgreed": "alreadyAgreed (이미 동의됨)",
300
+ "notifications.option.agreementRejected": "agreementRejected (사용자 거절)"
301
+ };
302
+ //#endregion
303
+ //#region src/i18n/index.ts
304
+ /**
305
+ * Vanilla TS i18n for the floating DevTools panel.
306
+ *
307
+ * Public surface:
308
+ * - `t(key, vars?)` — look up a UI string, with `{name}` placeholder
309
+ * interpolation. Falls back to the key itself if a translation is missing.
310
+ * - `getLocale()` / `setLocale(locale)` — read/persist the active locale.
311
+ * `setLocale` dispatches `__ait:localechange` so the panel can remount.
312
+ * - `detectLocale()` — first-run heuristic from `navigator.language`.
313
+ *
314
+ * `ko` is the source of truth (keys are typed from it). `en` is also a full
315
+ * `Record<StringKey, string>` (devtools is developer-facing, en is a real
316
+ * audience). The `Partial` lookup table preserves the runtime `?? key` safety
317
+ * net even though we ship complete catalogs today.
318
+ */
319
+ const LOCALE_STORAGE_KEY = "__ait_locale";
320
+ const LOCALE_CHANGE_EVENT = "__ait:localechange";
321
+ const tables = {
322
+ ko,
323
+ en
324
+ };
325
+ let currentLocale = null;
326
+ function safeReadStorage() {
327
+ if (typeof localStorage === "undefined") return null;
328
+ try {
329
+ const raw = localStorage.getItem(LOCALE_STORAGE_KEY);
330
+ if (raw === "ko" || raw === "en") return raw;
331
+ } catch {}
332
+ return null;
333
+ }
334
+ function safeWriteStorage(locale) {
335
+ if (typeof localStorage === "undefined") return;
336
+ try {
337
+ localStorage.setItem(LOCALE_STORAGE_KEY, locale);
338
+ } catch {}
339
+ }
340
+ /**
341
+ * Read `navigator.language` and decide a locale. `ko` (and `ko-*`) → `'ko'`,
342
+ * everything else → `'en'`. Pure function; does not touch storage.
343
+ */
344
+ function detectLocale() {
345
+ if (typeof navigator === "undefined") return "en";
346
+ const lang = navigator.language ?? "";
347
+ return /^ko\b/i.test(lang) ? "ko" : "en";
348
+ }
349
+ /**
350
+ * Resolve the active locale, in order:
351
+ * 1. previously set in-memory value (set by `setLocale`)
352
+ * 2. localStorage `__ait_locale`
353
+ * 3. `detectLocale()` from navigator
354
+ */
355
+ function getLocale() {
356
+ if (currentLocale) return currentLocale;
357
+ currentLocale = safeReadStorage() ?? detectLocale();
358
+ return currentLocale;
359
+ }
360
+ /**
361
+ * Persist a locale choice and notify listeners. The panel listens for
362
+ * `__ait:localechange` and re-mounts so every string re-evaluates.
363
+ */
364
+ function setLocale(locale) {
365
+ currentLocale = locale;
366
+ safeWriteStorage(locale);
367
+ if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent(LOCALE_CHANGE_EVENT));
368
+ }
369
+ /**
370
+ * Look up a UI string for the current locale. Falls back to the key if missing,
371
+ * so a forgotten key surfaces visibly rather than rendering empty.
372
+ */
373
+ function t(key, vars) {
374
+ const raw = tables[getLocale()][key] ?? key;
375
+ if (!vars) return raw;
376
+ return raw.replace(/\{(\w+)\}/g, (match, name) => {
377
+ const value = vars[name];
378
+ return value === void 0 ? match : String(value);
379
+ });
380
+ }
381
+ //#endregion
1
382
  //#region src/mock/state.ts
2
383
  const DEFAULT_STATE = {
3
384
  platform: "ios",
@@ -224,9 +605,6 @@ if (typeof window !== "undefined") window.__ait = aitState;
224
605
  /**
225
606
  * Consent toast UI — vanilla DOM, fixed bottom-right.
226
607
  *
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
608
  * Shows once per "undecided + reprompt window cleared" session.
231
609
  * Calls onAccept / onDeny callbacks; caller is responsible for persisting state.
232
610
  */
@@ -318,26 +696,26 @@ function showConsentToast({ onAccept, onDeny }) {
318
696
  toast.id = TOAST_ID;
319
697
  const header = document.createElement("div");
320
698
  header.className = "ait-toast-header";
321
- header.textContent = "익명 사용 통계를 보낼까요?";
699
+ header.textContent = t("toast.consent.title");
322
700
  const body = document.createElement("div");
323
701
  body.className = "ait-toast-body";
324
- body.textContent = "도구 개선을 위해 익명 이벤트만 수집해요. 언제든 환경 탭에서 끌 수 있어요.";
702
+ body.textContent = t("toast.consent.body");
325
703
  const learnMore = document.createElement("a");
326
704
  learnMore.className = "ait-toast-link";
327
705
  learnMore.href = "https://docs.aitc.dev/privacy";
328
706
  learnMore.target = "_blank";
329
707
  learnMore.rel = "noopener noreferrer";
330
- learnMore.textContent = "더 알아보기";
708
+ learnMore.textContent = t("toast.consent.learnMore");
331
709
  const yesBtn = document.createElement("button");
332
710
  yesBtn.className = "ait-toast-btn-primary";
333
- yesBtn.textContent = "Yes, send";
711
+ yesBtn.textContent = t("toast.consent.accept");
334
712
  yesBtn.addEventListener("click", () => {
335
713
  removeToast();
336
714
  onAccept();
337
715
  });
338
716
  const noBtn = document.createElement("button");
339
717
  noBtn.className = "ait-toast-btn-secondary";
340
- noBtn.textContent = "No, thanks";
718
+ noBtn.textContent = t("toast.consent.deny");
341
719
  noBtn.addEventListener("click", () => {
342
720
  removeToast();
343
721
  onDeny();
@@ -354,11 +732,56 @@ const KEY_CONSENT = "__ait_telemetry:consent";
354
732
  const KEY_REPROMPT_AFTER = "__ait_telemetry:reprompt_after";
355
733
  const KEY_POLICY_VERSION = "__ait_telemetry:policy_version";
356
734
  const KEY_ANON_ID = "__ait_telemetry:anon_id";
735
+ const KEY_T0_LAST_SENT = "__ait_telemetry:t0_last_sent";
736
+ const KEY_T0_OFF = "__ait_telemetry:t0_off";
737
+ /**
738
+ * Returns true if Tier 0 ping is enabled.
739
+ * Disabled when `localStorage.__ait_telemetry:t0_off = '1'`
740
+ * or `process.env.AITC_TELEMETRY === 'off'`.
741
+ */
742
+ function isTier0Enabled() {
743
+ if (typeof process !== "undefined" && process.env.AITC_TELEMETRY === "off") return false;
744
+ try {
745
+ return localStorage.getItem(KEY_T0_OFF) !== "1";
746
+ } catch {
747
+ return true;
748
+ }
749
+ }
750
+ /**
751
+ * Sets or clears the Tier 0 opt-out marker.
752
+ */
753
+ function setTier0Enabled(enabled) {
754
+ try {
755
+ if (enabled) localStorage.removeItem(KEY_T0_OFF);
756
+ else localStorage.setItem(KEY_T0_OFF, "1");
757
+ } catch {}
758
+ }
759
+ /**
760
+ * Returns true if Tier 0 has already been sent today (YYYY-MM-DD).
761
+ */
762
+ function hasSentTier0Today() {
763
+ try {
764
+ const stored = localStorage.getItem(KEY_T0_LAST_SENT);
765
+ if (!stored) return false;
766
+ return stored === (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
767
+ } catch {
768
+ return false;
769
+ }
770
+ }
771
+ /**
772
+ * Records that Tier 0 was sent today.
773
+ */
774
+ function markTier0Sent() {
775
+ try {
776
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
777
+ localStorage.setItem(KEY_T0_LAST_SENT, today);
778
+ } catch {}
779
+ }
357
780
  /**
358
781
  * Current policy version. Bump this string whenever the privacy policy changes.
359
782
  * Users who previously granted on an older version will be re-prompted once.
360
783
  */
361
- const CURRENT_POLICY_VERSION = "2026-05-12";
784
+ const CURRENT_POLICY_VERSION = "2026-05-18";
362
785
  /** 30 days in milliseconds */
363
786
  const THIRTY_DAYS_MS = 720 * 60 * 60 * 1e3;
364
787
  function readConsentState() {
@@ -397,7 +820,7 @@ function getOrCreateAnonId() {
397
820
  function resolveEffectiveConsent() {
398
821
  const raw = localStorage.getItem(KEY_CONSENT);
399
822
  if (raw === "granted") {
400
- if (readPolicyVersion() !== "2026-05-12") {
823
+ if (readPolicyVersion() !== "2026-05-18") {
401
824
  localStorage.removeItem(KEY_CONSENT);
402
825
  localStorage.removeItem(KEY_POLICY_VERSION);
403
826
  return "undecided";
@@ -523,6 +946,7 @@ function delay(ms) {
523
946
  async function send(event, version, meta) {
524
947
  if (readConsentState() !== "granted") return;
525
948
  const payload = {
949
+ tier: 1,
526
950
  source: "devtools",
527
951
  event,
528
952
  anon_id: getOrCreateAnonId(),
@@ -543,6 +967,7 @@ async function send(event, version, meta) {
543
967
  function sendBeaconEvent(event, version, meta) {
544
968
  if (readConsentState() !== "granted") return;
545
969
  const payload = {
970
+ tier: 1,
546
971
  source: "devtools",
547
972
  event,
548
973
  anon_id: getOrCreateAnonId(),
@@ -563,6 +988,49 @@ function sendBeaconEvent(event, version, meta) {
563
988
  }).catch(() => {});
564
989
  }
565
990
  //#endregion
991
+ //#region src/telemetry/tier0.ts
992
+ /**
993
+ * Tier 0 telemetry — opt-out, fire-and-forget daily ping.
994
+ *
995
+ * Payload: { tier: 0, source: 'devtools', ts: number, version: string }
996
+ * No anon_id. No event name. No meta.
997
+ *
998
+ * Rules:
999
+ * - Sent once per calendar day (localStorage daily marker).
1000
+ * - Skipped when __ait_telemetry:t0_off = '1' or AITC_TELEMETRY=off.
1001
+ * - 5 s timeout, no retry. Failure is silently dropped.
1002
+ */
1003
+ /**
1004
+ * Sends the Tier 0 daily ping if eligible.
1005
+ * Returns true if a ping was sent, false if skipped or failed.
1006
+ */
1007
+ async function sendTier0Ping(version) {
1008
+ if (!isTier0Enabled()) return false;
1009
+ if (hasSentTier0Today()) return false;
1010
+ const payload = {
1011
+ tier: 0,
1012
+ source: "devtools",
1013
+ ts: Date.now(),
1014
+ version
1015
+ };
1016
+ const controller = new AbortController();
1017
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
1018
+ try {
1019
+ await fetch(`${TELEMETRY_ENDPOINT}/e`, {
1020
+ method: "POST",
1021
+ headers: { "Content-Type": "application/json" },
1022
+ body: JSON.stringify(payload),
1023
+ signal: controller.signal
1024
+ });
1025
+ markTier0Sent();
1026
+ return true;
1027
+ } catch {
1028
+ return false;
1029
+ } finally {
1030
+ clearTimeout(timeoutId);
1031
+ }
1032
+ }
1033
+ //#endregion
566
1034
  //#region src/telemetry/index.ts
567
1035
  /**
568
1036
  * Telemetry client — internal to @ait-co/devtools.
@@ -582,7 +1050,7 @@ function readGlobalString(key) {
582
1050
  }
583
1051
  const TELEMETRY_ENDPOINT = readGlobalString("__TELEMETRY_ENDPOINT__") ?? "https://t.aitc.dev";
584
1052
  function getVersion() {
585
- return "0.1.18";
1053
+ return "0.1.20";
586
1054
  }
587
1055
  let panelVisibleSince = null;
588
1056
  let accumulatedMs = 0;
@@ -606,11 +1074,12 @@ function wirePagehide() {
606
1074
  }
607
1075
  /**
608
1076
  * Call once after panel mounts.
609
- * Handles: consent check, optional toast, panel_mount event, pagehide wiring.
1077
+ * Handles: Tier 0 ping, consent check, optional toast, panel_mount event, pagehide wiring.
610
1078
  */
611
1079
  function init() {
612
1080
  if (typeof window === "undefined" || typeof document === "undefined") return;
613
1081
  wirePagehide();
1082
+ sendTier0Ping(getVersion());
614
1083
  if (resolveEffectiveConsent() === "granted") {
615
1084
  getOrCreateAnonId();
616
1085
  send("panel_mount", getVersion());
@@ -691,7 +1160,7 @@ function inputRow(label, value, onChange, disabled = false) {
691
1160
  return h("div", { className: "ait-row" }, h("label", {}, label), input);
692
1161
  }
693
1162
  function monitoringNotice() {
694
- return h("div", { className: "ait-monitoring-notice" }, "Read-only — mock responses are controlled at build time.");
1163
+ return h("div", { className: "ait-monitoring-notice" }, t("common.readOnly"));
695
1164
  }
696
1165
  const PANEL_STYLES = `
697
1166
  .ait-panel-toggle {
@@ -1671,7 +2140,7 @@ function renderPromptBanner() {
1671
2140
  if (!pendingPrompt) return null;
1672
2141
  const banner = h("div", { className: "ait-prompt-banner" });
1673
2142
  if (pendingPrompt.type === "camera") {
1674
- banner.append(h("div", { className: "ait-prompt-title" }, "Camera Prompt — Select an image"));
2143
+ banner.append(h("div", { className: "ait-prompt-title" }, t("device.prompt.camera.title")));
1675
2144
  const input = h("input", {
1676
2145
  type: "file",
1677
2146
  accept: "image/*",
@@ -1686,7 +2155,7 @@ function renderPromptBanner() {
1686
2155
  });
1687
2156
  banner.appendChild(input);
1688
2157
  } else if (pendingPrompt.type === "photos") {
1689
- banner.append(h("div", { className: "ait-prompt-title" }, "Photos Prompt — Select images"));
2158
+ banner.append(h("div", { className: "ait-prompt-title" }, t("device.prompt.photos.title")));
1690
2159
  const input = h("input", {
1691
2160
  type: "file",
1692
2161
  accept: "image/*",
@@ -1704,7 +2173,7 @@ function renderPromptBanner() {
1704
2173
  });
1705
2174
  banner.appendChild(input);
1706
2175
  } 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"));
2176
+ banner.append(h("div", { className: "ait-prompt-title" }, pendingPrompt.type === "location" ? t("device.prompt.location.title") : t("device.prompt.locationUpdate.title")));
1708
2177
  const latInput = h("input", {
1709
2178
  className: "ait-input",
1710
2179
  value: String(aitState.state.location.coords.latitude),
@@ -1715,7 +2184,7 @@ function renderPromptBanner() {
1715
2184
  value: String(aitState.state.location.coords.longitude),
1716
2185
  style: "width:80px"
1717
2186
  });
1718
- const sendBtn = h("button", { className: "ait-btn ait-btn-sm" }, "Send");
2187
+ const sendBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("device.prompt.send"));
1719
2188
  sendBtn.addEventListener("click", () => {
1720
2189
  const loc = {
1721
2190
  coords: {
@@ -1731,12 +2200,12 @@ function renderPromptBanner() {
1731
2200
  };
1732
2201
  resolvePrompt(pendingPrompt.type, loc);
1733
2202
  });
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}`));
2203
+ 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));
2204
+ } else banner.append(h("div", { className: "ait-prompt-title" }, t("device.prompt.fallbackTitle", { type: pendingPrompt.type })));
1736
2205
  const cancelBtn = h("button", {
1737
2206
  className: "ait-btn ait-btn-sm ait-btn-danger",
1738
2207
  style: "margin-top:8px"
1739
- }, "Cancel");
2208
+ }, t("device.prompt.cancel"));
1740
2209
  cancelBtn.addEventListener("click", () => {
1741
2210
  pendingPrompt = null;
1742
2211
  window.dispatchEvent(new CustomEvent("__ait:prompt-cancel"));
@@ -1754,9 +2223,9 @@ function renderDeviceTab() {
1754
2223
  const promptBanner = renderPromptBanner();
1755
2224
  if (promptBanner) container.appendChild(promptBanner);
1756
2225
  }
1757
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Device API Modes"), ...[
2226
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("device.section.modes")), ...[
1758
2227
  {
1759
- label: "Camera",
2228
+ labelKey: "device.row.camera",
1760
2229
  key: "camera",
1761
2230
  options: [
1762
2231
  "mock",
@@ -1765,7 +2234,7 @@ function renderDeviceTab() {
1765
2234
  ]
1766
2235
  },
1767
2236
  {
1768
- label: "Photos",
2237
+ labelKey: "device.row.photos",
1769
2238
  key: "photos",
1770
2239
  options: [
1771
2240
  "mock",
@@ -1774,7 +2243,7 @@ function renderDeviceTab() {
1774
2243
  ]
1775
2244
  },
1776
2245
  {
1777
- label: "Location",
2246
+ labelKey: "device.row.location",
1778
2247
  key: "location",
1779
2248
  options: [
1780
2249
  "mock",
@@ -1783,16 +2252,16 @@ function renderDeviceTab() {
1783
2252
  ]
1784
2253
  },
1785
2254
  {
1786
- label: "Network",
2255
+ labelKey: "device.row.network",
1787
2256
  key: "network",
1788
2257
  options: ["mock", "web"]
1789
2258
  },
1790
2259
  {
1791
- label: "Clipboard",
2260
+ labelKey: "device.row.clipboard",
1792
2261
  key: "clipboard",
1793
2262
  options: ["mock", "web"]
1794
2263
  }
1795
- ].map((entry) => selectRow(entry.label, entry.options, s.deviceModes[entry.key], (v) => {
2264
+ ].map((entry) => selectRow(t(entry.labelKey), entry.options, s.deviceModes[entry.key], (v) => {
1796
2265
  aitState.patch("deviceModes", { [entry.key]: v });
1797
2266
  }, disabled))));
1798
2267
  const images = s.mockData.images;
@@ -1810,7 +2279,7 @@ function renderDeviceTab() {
1810
2279
  thumb.append(img, removeBtn);
1811
2280
  imageGrid.appendChild(thumb);
1812
2281
  });
1813
- const addBtn = h("button", { className: "ait-btn-secondary" }, "+ Add");
2282
+ const addBtn = h("button", { className: "ait-btn-secondary" }, t("device.btn.add"));
1814
2283
  addBtn.addEventListener("click", () => {
1815
2284
  const input = document.createElement("input");
1816
2285
  input.type = "file";
@@ -1829,17 +2298,17 @@ function renderDeviceTab() {
1829
2298
  input.click();
1830
2299
  });
1831
2300
  if (disabled) addBtn.disabled = true;
1832
- const defaultsBtn = h("button", { className: "ait-btn-secondary" }, "Use defaults");
2301
+ const defaultsBtn = h("button", { className: "ait-btn-secondary" }, t("device.btn.useDefaults"));
1833
2302
  defaultsBtn.addEventListener("click", () => {
1834
2303
  aitState.patch("mockData", { images: [...getDefaultPlaceholderImages()] });
1835
2304
  });
1836
2305
  if (disabled) defaultsBtn.disabled = true;
1837
- const clearImagesBtn = h("button", { className: "ait-btn-secondary" }, "Clear");
2306
+ const clearImagesBtn = h("button", { className: "ait-btn-secondary" }, t("device.btn.clear"));
1838
2307
  clearImagesBtn.addEventListener("click", () => {
1839
2308
  aitState.patch("mockData", { images: [] });
1840
2309
  });
1841
2310
  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)));
2311
+ 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
2312
  return container;
1844
2313
  }
1845
2314
  //#endregion
@@ -1956,7 +2425,7 @@ function statusRow(label, value) {
1956
2425
  }
1957
2426
  function lastEventLine() {
1958
2427
  const last = aitState.state.ads.lastEvent;
1959
- if (!last) return h("div", { className: "ait-log-entry" }, h("span", { style: "color:#555" }, "No events yet"));
2428
+ if (!last) return h("div", { className: "ait-log-entry" }, h("span", { style: "color:#555" }, t("ads.empty.events")));
1960
2429
  const time = new Date(last.timestamp).toLocaleTimeString();
1961
2430
  return h("div", { className: "ait-log-entry" }, h("span", {
1962
2431
  className: "ait-log-type",
@@ -1964,8 +2433,8 @@ function lastEventLine() {
1964
2433
  }, last.type), h("span", { className: "ait-log-time" }, time));
1965
2434
  }
1966
2435
  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");
2436
+ const loadBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("ads.btn.load"));
2437
+ const showBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("ads.btn.show"));
1969
2438
  if (disabled) {
1970
2439
  loadBtn.disabled = true;
1971
2440
  showBtn.disabled = true;
@@ -1988,7 +2457,7 @@ function renderAdsTab() {
1988
2457
  forceNoFillCb.addEventListener("change", () => {
1989
2458
  aitState.patch("ads", { forceNoFill: forceNoFillCb.checked });
1990
2459
  });
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", () => {
2460
+ 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
2461
  GoogleAdMob.loadAppsInTossAdMob({
1993
2462
  onEvent: (e) => recordEvent(e.type),
1994
2463
  onError: (err) => recordError(err.message)
@@ -1998,7 +2467,7 @@ function renderAdsTab() {
1998
2467
  onEvent: (e) => recordEvent(e.type),
1999
2468
  onError: (err) => recordError(err.message)
2000
2469
  });
2001
- }, disabled), adSection("TossAds", () => {
2470
+ }, disabled), adSection(t("ads.section.tossAds"), () => {
2002
2471
  if (aitState.state.ads.forceNoFill) {
2003
2472
  recordError("No fill");
2004
2473
  return;
@@ -2015,7 +2484,7 @@ function renderAdsTab() {
2015
2484
  recordEvent("dismissed");
2016
2485
  aitState.patch("ads", { isLoaded: false });
2017
2486
  }, 1500);
2018
- }, disabled), adSection("FullScreenAd", () => {
2487
+ }, disabled), adSection(t("ads.section.fullScreenAd"), () => {
2019
2488
  loadFullScreenAd({
2020
2489
  onEvent: (e) => recordEvent(e.type),
2021
2490
  onError: (err) => recordError(err.message)
@@ -2035,13 +2504,13 @@ function renderAnalyticsTab() {
2035
2504
  const container = h("div");
2036
2505
  if (disabled) container.appendChild(monitoringNotice());
2037
2506
  const logs = aitState.state.analyticsLog;
2038
- const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "Clear");
2507
+ const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("analytics.btn.clear"));
2039
2508
  if (disabled) clearBtn.disabled = true;
2040
2509
  clearBtn.addEventListener("click", () => {
2041
2510
  aitState.update({ analyticsLog: [] });
2042
2511
  });
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));
2512
+ 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) => {
2513
+ 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
2514
  })));
2046
2515
  return container;
2047
2516
  }
@@ -2052,7 +2521,7 @@ function renderEnvironmentTab() {
2052
2521
  const disabled = !s.panelEditable;
2053
2522
  const container = h("div");
2054
2523
  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", [
2524
+ 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
2525
  "WIFI",
2057
2526
  "4G",
2058
2527
  "5G",
@@ -2061,39 +2530,73 @@ function renderEnvironmentTab() {
2061
2530
  "OFFLINE",
2062
2531
  "WWAN",
2063
2532
  "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());
2533
+ ], 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
2534
  return container;
2066
2535
  }
2536
+ function buildLanguageSection() {
2537
+ const current = getLocale();
2538
+ const select = h("select", { className: "ait-select" });
2539
+ for (const opt of [{
2540
+ value: "ko",
2541
+ labelKey: "env.language.ko"
2542
+ }, {
2543
+ value: "en",
2544
+ labelKey: "env.language.en"
2545
+ }]) {
2546
+ const option = h("option", { value: opt.value }, t(opt.labelKey));
2547
+ if (opt.value === current) option.selected = true;
2548
+ select.appendChild(option);
2549
+ }
2550
+ select.addEventListener("change", () => {
2551
+ setLocale(select.value);
2552
+ });
2553
+ 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));
2554
+ }
2067
2555
  function buildTelemetrySection() {
2556
+ const t0Enabled = isTier0Enabled();
2557
+ const t0StatusLabel = h("span", { style: `font-size:12px;font-weight:600;color:${t0Enabled ? "#4ade80" : "#888"}` }, t0Enabled ? t("env.telemetry.t0On") : t("env.telemetry.t0Off"));
2558
+ const t0ToggleBtn = h("button", {
2559
+ className: "ait-btn ait-btn-sm",
2560
+ style: "font-size:11px"
2561
+ }, t0Enabled ? t("env.telemetry.t0TurnOff") : t("env.telemetry.t0TurnOn"));
2562
+ t0ToggleBtn.addEventListener("click", () => {
2563
+ setTier0Enabled(!t0Enabled);
2564
+ window.dispatchEvent(new CustomEvent("__ait:panel-switch-tab", { detail: { tab: "env" } }));
2565
+ });
2566
+ const t0Row = h("div", { className: "ait-row" }, h("label", {}, t("env.telemetry.t0Row")), h("span", { style: "display:flex;align-items:center;gap:8px" }, t0StatusLabel, t0ToggleBtn));
2567
+ const t0Desc = h("div", { style: "font-size:11px;color:#666;margin-bottom:6px" }, t("env.telemetry.t0Desc"));
2068
2568
  const isGranted = readConsentState() === "granted";
2069
- const statusLabel = h("span", { style: `font-size:12px;font-weight:600;color:${isGranted ? "#4ade80" : "#888"}` }, isGranted ? "On" : "Off");
2569
+ 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
2570
  const toggleBtn = h("button", {
2071
2571
  className: "ait-btn ait-btn-sm",
2072
2572
  style: "font-size:11px"
2073
- }, isGranted ? "Turn off" : "Turn on");
2573
+ }, isGranted ? t("env.telemetry.turnOff") : t("env.telemetry.turnOn"));
2074
2574
  toggleBtn.addEventListener("click", () => {
2075
2575
  setConsentViaToggle(!isGranted);
2076
2576
  window.dispatchEvent(new CustomEvent("__ait:panel-switch-tab", { detail: { tab: "env" } }));
2077
2577
  });
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)";
2578
+ 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));
2579
+ const rawAnonId = localStorage.getItem("__ait_telemetry:anon_id");
2580
+ const displayAnonId = rawAnonId ?? t("env.telemetry.anonIdNotSet");
2581
+ const truncatedId = displayAnonId.length > 8 ? `${displayAnonId.slice(0, 8)}…` : displayAnonId;
2080
2582
  const anonIdEl = h("span", {
2081
2583
  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}`);
2584
+ title: t("env.telemetry.anonIdCopyTitle")
2585
+ }, t("env.telemetry.anonIdLabel", { value: truncatedId }));
2084
2586
  anonIdEl.addEventListener("click", () => {
2085
- navigator.clipboard.writeText(anonId).catch(() => {});
2587
+ if (!rawAnonId) return;
2588
+ navigator.clipboard.writeText(rawAnonId).catch(() => {});
2086
2589
  });
2087
- const deleteBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "내 데이터 삭제");
2590
+ const deleteBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("env.telemetry.deleteBtn"));
2088
2591
  const deleteStatus = h("span", { style: "font-size:11px;color:#aaa" });
2089
2592
  deleteBtn.addEventListener("click", () => {
2090
2593
  deleteBtn.disabled = true;
2091
- deleteStatus.textContent = "삭제 중…";
2594
+ deleteStatus.textContent = t("env.telemetry.deleting");
2092
2595
  deleteMyData(TELEMETRY_ENDPOINT).then((ok) => {
2093
- deleteStatus.textContent = ok ? "삭제 완료" : "삭제 실패 (다시 시도해주세요)";
2596
+ deleteStatus.textContent = ok ? t("env.telemetry.deleted") : t("env.telemetry.deleteFailedRetry");
2094
2597
  deleteBtn.disabled = false;
2095
2598
  }).catch(() => {
2096
- deleteStatus.textContent = "삭제 실패";
2599
+ deleteStatus.textContent = t("env.telemetry.deleteFailed");
2097
2600
  deleteBtn.disabled = false;
2098
2601
  });
2099
2602
  });
@@ -2103,8 +2606,8 @@ function buildTelemetrySection() {
2103
2606
  rel: "noopener noreferrer",
2104
2607
  style: "font-size:11px;color:#666;text-decoration:none"
2105
2608
  });
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", {
2609
+ privacyLink.textContent = t("env.telemetry.privacyLink");
2610
+ return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.telemetry.section")), t0Row, t0Desc, statusRow, h("div", { style: "margin-bottom:6px" }, anonIdEl), h("div", {
2108
2611
  className: "ait-btn-row",
2109
2612
  style: "align-items:center;gap:8px;margin-top:6px"
2110
2613
  }, deleteBtn, deleteStatus), h("div", { style: "margin-top:8px" }, privacyLink));
@@ -2115,15 +2618,15 @@ function renderEventsTab() {
2115
2618
  const disabled = !aitState.state.panelEditable;
2116
2619
  const container = h("div");
2117
2620
  if (disabled) container.appendChild(monitoringNotice());
2118
- const backBtn = h("button", { className: "ait-btn" }, "Trigger Back Event");
2621
+ const backBtn = h("button", { className: "ait-btn" }, t("events.btn.triggerBack"));
2119
2622
  backBtn.addEventListener("click", () => aitState.trigger("backEvent"));
2120
2623
  if (disabled) backBtn.disabled = true;
2121
- const homeBtn = h("button", { className: "ait-btn" }, "Trigger Home Event");
2624
+ const homeBtn = h("button", { className: "ait-btn" }, t("events.btn.triggerHome"));
2122
2625
  homeBtn.addEventListener("click", () => aitState.trigger("homeEvent"));
2123
2626
  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) => {
2627
+ 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
2628
  aitState.patch("auth", { isLoggedIn: v === "true" });
2126
- }, disabled), selectRow("Toss Login Integrated", ["true", "false"], String(aitState.state.auth.isTossLoginIntegrated), (v) => {
2629
+ }, disabled), selectRow(t("events.row.tossLoginIntegrated"), ["true", "false"], String(aitState.state.auth.isTossLoginIntegrated), (v) => {
2127
2630
  aitState.patch("auth", { isTossLoginIntegrated: v === "true" });
2128
2631
  }, disabled)));
2129
2632
  return container;
@@ -2267,23 +2770,23 @@ function renderIapTab() {
2267
2770
  ];
2268
2771
  if (disabled) container.appendChild(monitoringNotice());
2269
2772
  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)"));
2773
+ const pendingSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.pending", { count: pendingOrders.length })));
2774
+ if (pendingOrders.length === 0) pendingSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.pending")));
2272
2775
  else for (const o of pendingOrders) {
2273
- const completeBtn = h("button", { className: "ait-btn ait-btn-sm" }, "Complete");
2776
+ const completeBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("iap.btn.complete"));
2274
2777
  if (disabled) completeBtn.disabled = true;
2275
2778
  completeBtn.addEventListener("click", () => {
2276
2779
  IAP.completeProductGrant({ params: { orderId: o.orderId } }).catch((err) => console.error("[@ait-co/devtools] completeProductGrant error:", err));
2277
2780
  });
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));
2781
+ 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
2782
  }
2280
2783
  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)"));
2784
+ const completedSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.completed", { count: completedOrders.length })));
2785
+ if (completedOrders.length === 0) completedSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.completed")));
2283
2786
  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) => {
2787
+ 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
2788
  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) => {
2789
+ }, 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
2790
  aitState.patch("payment", { nextResult: v });
2288
2791
  }, disabled)), pendingSection, completedSection);
2289
2792
  return container;
@@ -2295,19 +2798,19 @@ function renderLocationTab() {
2295
2798
  const disabled = !s.panelEditable;
2296
2799
  const container = h("div");
2297
2800
  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) => {
2801
+ 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
2802
  const coords = {
2300
2803
  ...s.location.coords,
2301
2804
  latitude: Number(v)
2302
2805
  };
2303
2806
  aitState.patch("location", { coords });
2304
- }, disabled), inputRow("Longitude", String(s.location.coords.longitude), (v) => {
2807
+ }, disabled), inputRow(t("location.row.longitude"), String(s.location.coords.longitude), (v) => {
2305
2808
  const coords = {
2306
2809
  ...s.location.coords,
2307
2810
  longitude: Number(v)
2308
2811
  };
2309
2812
  aitState.patch("location", { coords });
2310
- }, disabled), inputRow("Accuracy", String(s.location.coords.accuracy), (v) => {
2813
+ }, disabled), inputRow(t("location.row.accuracy"), String(s.location.coords.accuracy), (v) => {
2311
2814
  const coords = {
2312
2815
  ...s.location.coords,
2313
2816
  accuracy: Number(v)
@@ -2321,15 +2824,15 @@ function renderLocationTab() {
2321
2824
  const RESULTS = [
2322
2825
  {
2323
2826
  value: "newAgreement",
2324
- label: "newAgreement (first-time agree)"
2827
+ labelKey: "notifications.option.newAgreement"
2325
2828
  },
2326
2829
  {
2327
2830
  value: "alreadyAgreed",
2328
- label: "alreadyAgreed (already opted-in)"
2831
+ labelKey: "notifications.option.alreadyAgreed"
2329
2832
  },
2330
2833
  {
2331
2834
  value: "agreementRejected",
2332
- label: "agreementRejected (user declined)"
2835
+ labelKey: "notifications.option.agreementRejected"
2333
2836
  }
2334
2837
  ];
2335
2838
  function radioRow(name, current, option, disabled) {
@@ -2343,14 +2846,14 @@ function radioRow(name, current, option, disabled) {
2343
2846
  input.addEventListener("change", () => {
2344
2847
  if (input.checked) aitState.patch("notification", { nextResult: option.value });
2345
2848
  });
2346
- return h("label", { className: "ait-row" }, input, h("span", {}, option.label));
2849
+ return h("label", { className: "ait-row" }, input, h("span", {}, t(option.labelKey)));
2347
2850
  }
2348
2851
  function renderNotificationsTab() {
2349
2852
  const s = aitState.state;
2350
2853
  const disabled = !s.panelEditable;
2351
2854
  const container = h("div");
2352
2855
  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))));
2856
+ 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
2857
  return container;
2355
2858
  }
2356
2859
  //#endregion
@@ -2373,7 +2876,7 @@ function renderPermissionsTab() {
2373
2876
  "notDetermined"
2374
2877
  ];
2375
2878
  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) => {
2879
+ 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
2880
  aitState.patch("permissions", { [name]: v });
2378
2881
  }, disabled))));
2379
2882
  return container;
@@ -2661,12 +3164,12 @@ function renderPresetsTab(refreshPanel) {
2661
3164
  if (disabled) container.appendChild(monitoringNotice());
2662
3165
  const userPresets = listUserPresets();
2663
3166
  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");
3167
+ container.append(renderSection(t("presets.section.builtIn"), builtInPresets, disabled, snapshot, refreshPanel, false));
3168
+ container.append(renderSection(t("presets.section.saved", { count: userPresets.length }), userPresets, disabled, snapshot, refreshPanel, true));
3169
+ const saveBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("presets.btn.saveCurrent"));
2667
3170
  if (disabled) saveBtn.disabled = true;
2668
3171
  saveBtn.addEventListener("click", () => {
2669
- const label = window.prompt("Preset label?");
3172
+ const label = window.prompt(t("presets.prompt.label"));
2670
3173
  if (label === null) return;
2671
3174
  try {
2672
3175
  saveUserPreset(label, captureCurrentState(aitState.state));
@@ -2676,19 +3179,19 @@ function renderPresetsTab(refreshPanel) {
2676
3179
  }
2677
3180
  refreshPanel();
2678
3181
  });
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));
3182
+ 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
3183
  return container;
2681
3184
  }
2682
3185
  function renderSection(title, presets, disabled, snapshot, refreshPanel, deletable) {
2683
3186
  const section = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, title));
2684
3187
  if (presets.length === 0) {
2685
- section.append(h("div", { style: "color:#555;font-size:12px" }, deletable ? "No saved presets yet." : "No built-in presets."));
3188
+ section.append(h("div", { style: "color:#555;font-size:12px" }, deletable ? t("presets.empty.saved") : t("presets.empty.builtIn")));
2686
3189
  return section;
2687
3190
  }
2688
3191
  for (const preset of presets) {
2689
3192
  const isActive = matchesPreset(snapshot, preset.state);
2690
3193
  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");
3194
+ const applyBtn = h("button", { className: "ait-btn ait-btn-sm" }, isActive ? t("presets.btn.reApply") : t("presets.btn.apply"));
2692
3195
  if (disabled) applyBtn.disabled = true;
2693
3196
  applyBtn.addEventListener("click", () => {
2694
3197
  applyPreset(preset.state);
@@ -2696,10 +3199,10 @@ function renderSection(title, presets, disabled, snapshot, refreshPanel, deletab
2696
3199
  });
2697
3200
  const buttons = [applyBtn];
2698
3201
  if (deletable) {
2699
- const delBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "Delete");
3202
+ const delBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("presets.btn.delete"));
2700
3203
  if (disabled) delBtn.disabled = true;
2701
3204
  delBtn.addEventListener("click", () => {
2702
- if (!window.confirm(`Delete preset "${preset.label}"?`)) return;
3205
+ if (!window.confirm(t("presets.confirm.delete", { label: preset.label }))) return;
2703
3206
  deleteUserPreset(preset.id);
2704
3207
  refreshPanel();
2705
3208
  });
@@ -2724,13 +3227,13 @@ function renderStorageTab(refreshPanel) {
2724
3227
  const key = localStorage.key(i);
2725
3228
  if (key?.startsWith(prefix)) entries.push([key.slice(14), localStorage.getItem(key) ?? ""]);
2726
3229
  }
2727
- const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "Clear All");
3230
+ const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("storage.btn.clearAll"));
2728
3231
  if (disabled) clearBtn.disabled = true;
2729
3232
  clearBtn.addEventListener("click", () => {
2730
3233
  for (const [key] of entries) localStorage.removeItem(prefix + key);
2731
3234
  refreshPanel();
2732
3235
  });
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))))));
3236
+ 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
3237
  return container;
2735
3238
  }
2736
3239
  //#endregion
@@ -3340,7 +3843,7 @@ function renderViewportTab() {
3340
3843
  heightInput.value = String(clamped);
3341
3844
  }
3342
3845
  });
3343
- 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));
3846
+ 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));
3344
3847
  }
3345
3848
  const frameCheckbox = h("input", { type: "checkbox" });
3346
3849
  frameCheckbox.checked = vp.frame;
@@ -3366,7 +3869,7 @@ function renderViewportTab() {
3366
3869
  });
3367
3870
  const size = resolveViewportSize(vp);
3368
3871
  const statusEl = h("div", { className: "ait-section" });
3369
- if (vp.preset === "none" || size.width === 0) statusEl.appendChild(h("div", { style: "color:#888;font-size:11px" }, "No viewport constraint — body fills the window."));
3872
+ if (vp.preset === "none" || size.width === 0) statusEl.appendChild(h("div", { style: "color:#888;font-size:11px" }, t("viewport.status.noConstraint")));
3370
3873
  else {
3371
3874
  const preset = vp.preset === "custom" ? null : getPreset(vp.preset);
3372
3875
  const effOrient = effectiveOrientation(vp);
@@ -3375,75 +3878,84 @@ function renderViewportTab() {
3375
3878
  const dpr = preset?.dpr ?? 1;
3376
3879
  const physW = Math.round(size.width * dpr);
3377
3880
  const physH = Math.round(size.height * dpr);
3378
- const orientDisplay = vp.orientation === "auto" ? `${effOrient} (auto)` : effOrient;
3379
- 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}`)));
3881
+ const orientDisplay = vp.orientation === "auto" ? t("viewport.orientation.autoSuffix", { orient: effOrient }) : effOrient;
3882
+ 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}`)));
3380
3883
  if (preset) {
3381
3884
  const insets = computeSafeAreaInsets(preset, landscape, vp.landscapeSide);
3382
- 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}`)));
3885
+ 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}`)));
3383
3886
  }
3384
- 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}`)));
3887
+ 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", {
3888
+ height: 48,
3889
+ type: vp.aitNavBarType
3890
+ }))));
3385
3891
  for (const row of rows) statusEl.appendChild(row);
3386
3892
  }
3387
- 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));
3893
+ 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));
3388
3894
  if (effectiveOrientation(vp) === "landscape" && vp.preset !== "none" && vp.preset !== "custom") {
3389
3895
  const notch = getPreset(vp.preset).notch;
3390
- if (notch === "notch" || notch === "dynamic-island") deviceSection.appendChild(h("div", { className: "ait-row" }, h("label", {}, "Notch side"), landscapeSideSelect));
3896
+ if (notch === "notch" || notch === "dynamic-island") deviceSection.appendChild(h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.notchSide")), landscapeSideSelect));
3391
3897
  }
3392
- 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);
3898
+ 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);
3393
3899
  return container;
3394
3900
  }
3395
3901
  //#endregion
3396
3902
  //#region src/panel/tabs/index.ts
3397
- const TABS = [
3903
+ const TAB_DEFS = [
3398
3904
  {
3399
3905
  id: "env",
3400
- label: "Environment"
3906
+ labelKey: "panel.tab.env"
3401
3907
  },
3402
3908
  {
3403
3909
  id: "presets",
3404
- label: "Presets"
3910
+ labelKey: "panel.tab.presets"
3405
3911
  },
3406
3912
  {
3407
3913
  id: "viewport",
3408
- label: "Viewport"
3914
+ labelKey: "panel.tab.viewport"
3409
3915
  },
3410
3916
  {
3411
3917
  id: "permissions",
3412
- label: "Permissions"
3918
+ labelKey: "panel.tab.permissions"
3413
3919
  },
3414
3920
  {
3415
3921
  id: "notifications",
3416
- label: "Notifications"
3922
+ labelKey: "panel.tab.notifications"
3417
3923
  },
3418
3924
  {
3419
3925
  id: "location",
3420
- label: "Location"
3926
+ labelKey: "panel.tab.location"
3421
3927
  },
3422
3928
  {
3423
3929
  id: "device",
3424
- label: "Device"
3930
+ labelKey: "panel.tab.device"
3425
3931
  },
3426
3932
  {
3427
3933
  id: "iap",
3428
- label: "IAP"
3934
+ labelKey: "panel.tab.iap"
3429
3935
  },
3430
3936
  {
3431
3937
  id: "ads",
3432
- label: "Ads"
3938
+ labelKey: "panel.tab.ads"
3433
3939
  },
3434
3940
  {
3435
3941
  id: "events",
3436
- label: "Events"
3942
+ labelKey: "panel.tab.events"
3437
3943
  },
3438
3944
  {
3439
3945
  id: "analytics",
3440
- label: "Analytics"
3946
+ labelKey: "panel.tab.analytics"
3441
3947
  },
3442
3948
  {
3443
3949
  id: "storage",
3444
- label: "Storage"
3950
+ labelKey: "panel.tab.storage"
3445
3951
  }
3446
3952
  ];
3953
+ function getTabs() {
3954
+ return TAB_DEFS.map((def) => ({
3955
+ id: def.id,
3956
+ label: t(def.labelKey)
3957
+ }));
3958
+ }
3447
3959
  function createTabRenderers(refreshPanel) {
3448
3960
  return {
3449
3961
  env: renderEnvironmentTab,
@@ -3605,6 +4117,7 @@ let injectedStyle = null;
3605
4117
  let panelSwitchTabHandler = null;
3606
4118
  let resizeHandler = null;
3607
4119
  let aitStateUnsubscribe = null;
4120
+ let localeChangeHandler = null;
3608
4121
  let tabRenderers = null;
3609
4122
  function refreshPanel() {
3610
4123
  if (!bodyEl || !tabsEl) return;
@@ -3614,7 +4127,7 @@ function refreshPanel() {
3614
4127
  bodyEl.appendChild(tabRenderers[currentTab]());
3615
4128
  } catch (err) {
3616
4129
  console.error(`[@ait-co/devtools] Error rendering tab "${currentTab}":`, err);
3617
- bodyEl.appendChild(h("div", { className: "ait-panel-tab-error" }, `Error rendering "${currentTab}" tab.`));
4130
+ bodyEl.appendChild(h("div", { className: "ait-panel-tab-error" }, t("panel.tabError", { tab: currentTab })));
3618
4131
  }
3619
4132
  tabsEl.querySelectorAll(".ait-panel-tab").forEach((el) => {
3620
4133
  el.classList.toggle("active", el.getAttribute("data-tab") === currentTab);
@@ -3630,14 +4143,14 @@ function mount() {
3630
4143
  document.head.appendChild(injectedStyle);
3631
4144
  const toggle = h("button", {
3632
4145
  className: "ait-panel-toggle",
3633
- title: "AIT DevTools"
4146
+ title: t("panel.toggle.title")
3634
4147
  }, "AIT");
3635
4148
  toggleEl = toggle;
3636
4149
  restoreButtonPosition(toggle);
3637
4150
  panelEl = h("div", { className: "ait-panel" });
3638
4151
  const closeBtn = h("button", {
3639
4152
  className: "ait-panel-close",
3640
- title: "Close"
4153
+ title: t("panel.close")
3641
4154
  }, "×");
3642
4155
  closeBtn.addEventListener("click", () => {
3643
4156
  isOpen = false;
@@ -3646,18 +4159,18 @@ function mount() {
3646
4159
  });
3647
4160
  const mockBadge = h("span", {
3648
4161
  className: `ait-mock-badge ${aitState.state.panelEditable ? "ait-mock-badge-on" : "ait-mock-badge-off"}`,
3649
- title: "Toggle panel edit mode"
3650
- }, aitState.state.panelEditable ? "EDIT" : "READ-ONLY");
4162
+ title: t("panel.editMode.toggleTitle")
4163
+ }, aitState.state.panelEditable ? t("panel.editMode.on") : t("panel.editMode.off"));
3651
4164
  mockBadge.addEventListener("click", () => {
3652
4165
  aitState.update({ panelEditable: !aitState.state.panelEditable });
3653
4166
  mockBadge.className = `ait-mock-badge ${aitState.state.panelEditable ? "ait-mock-badge-on" : "ait-mock-badge-off"}`;
3654
- mockBadge.textContent = aitState.state.panelEditable ? "EDIT" : "READ-ONLY";
4167
+ mockBadge.textContent = aitState.state.panelEditable ? t("panel.editMode.on") : t("panel.editMode.off");
3655
4168
  refreshPanel();
3656
4169
  });
3657
- 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.18`), closeBtn);
3658
- const header = h("div", { className: "ait-panel-header" }, h("span", {}, "AIT DevTools"), headerRight);
4170
+ 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.20`), closeBtn);
4171
+ const header = h("div", { className: "ait-panel-header" }, h("span", {}, t("panel.title")), headerRight);
3659
4172
  tabsEl = h("div", { className: "ait-panel-tabs" });
3660
- for (const tab of TABS) {
4173
+ for (const tab of getTabs()) {
3661
4174
  const tabEl = h("button", {
3662
4175
  className: "ait-panel-tab",
3663
4176
  "data-tab": tab.id
@@ -3710,6 +4223,23 @@ function mount() {
3710
4223
  refreshPanel();
3711
4224
  };
3712
4225
  window.addEventListener("__ait:panel-switch-tab", panelSwitchTabHandler);
4226
+ localeChangeHandler = () => {
4227
+ const savedTab = currentTab;
4228
+ const savedOpen = isOpen;
4229
+ disposePanel();
4230
+ try {
4231
+ mount();
4232
+ currentTab = savedTab;
4233
+ if (savedOpen && panelEl) {
4234
+ isOpen = true;
4235
+ panelEl.classList.add("open");
4236
+ }
4237
+ refreshPanel();
4238
+ } catch (err) {
4239
+ console.error("[@ait-co/devtools] Failed to re-mount after locale change:", err);
4240
+ }
4241
+ };
4242
+ window.addEventListener(LOCALE_CHANGE_EVENT, localeChangeHandler);
3713
4243
  refreshPanel();
3714
4244
  telemetry.init();
3715
4245
  }
@@ -3725,6 +4255,7 @@ function disposePanel() {
3725
4255
  if (typeof document === "undefined") return;
3726
4256
  if (panelSwitchTabHandler && typeof window !== "undefined") window.removeEventListener("__ait:panel-switch-tab", panelSwitchTabHandler);
3727
4257
  if (resizeHandler && typeof window !== "undefined") window.removeEventListener("resize", resizeHandler);
4258
+ if (localeChangeHandler && typeof window !== "undefined") window.removeEventListener(LOCALE_CHANGE_EVENT, localeChangeHandler);
3728
4259
  if (aitStateUnsubscribe) aitStateUnsubscribe();
3729
4260
  toggleEl?.remove();
3730
4261
  panelEl?.remove();
@@ -3733,6 +4264,7 @@ function disposePanel() {
3733
4264
  setDeviceRefreshPanel(() => {});
3734
4265
  panelSwitchTabHandler = null;
3735
4266
  resizeHandler = null;
4267
+ localeChangeHandler = null;
3736
4268
  aitStateUnsubscribe = null;
3737
4269
  toggleEl = null;
3738
4270
  panelEl = null;