jskelet 0.2.3 → 0.2.4

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.
@@ -7,7 +7,11 @@
7
7
  *
8
8
  * Yenileme WebSocket değil, kısa aralıklı `fetch` ile: panel nadiren ve kısa
9
9
  * süre açık kalıyor, kalıcı bir kanal açmanın karşılığı yok.
10
+ *
11
+ * Bütün görünen metin `i18n.js` üzerinden geçiyor; sunucudan gelen cevaplar da
12
+ * metin değil `code` taşıyor, yani dil değişimi tek yerden hallediliyor.
10
13
  */
14
+ import { applyTranslations, languageSelect, t } from "./i18n.js";
11
15
 
12
16
  const REFRESH_MS = 3000;
13
17
 
@@ -42,10 +46,14 @@ function formatBytes(bytes) {
42
46
  */
43
47
  function formatDuration(seconds) {
44
48
  const total = Math.max(0, Math.round(seconds));
45
- if (total < 60) return `${total}s`;
46
- if (total < 3600) return `${Math.floor(total / 60)}m ${total % 60}s`;
47
- if (total < 86_400) return `${Math.floor(total / 3600)}h ${Math.floor((total % 3600) / 60)}m`;
48
- return `${Math.floor(total / 86_400)}d ${Math.floor((total % 86_400) / 3600)}h`;
49
+ const [s, m, h, d] = [t("unit.second"), t("unit.minute"), t("unit.hour"), t("unit.day")];
50
+
51
+ if (total < 60) return `${total}${s}`;
52
+ if (total < 3600) return `${Math.floor(total / 60)}${m} ${total % 60}${s}`;
53
+ if (total < 86_400) {
54
+ return `${Math.floor(total / 3600)}${h} ${Math.floor((total % 3600) / 60)}${m}`;
55
+ }
56
+ return `${Math.floor(total / 86_400)}${d} ${Math.floor((total % 86_400) / 3600)}${h}`;
49
57
  }
50
58
 
51
59
  /**
@@ -117,14 +125,39 @@ async function act(body) {
117
125
 
118
126
  if (!alive(response)) return;
119
127
 
120
- const result = await response.json().catch(() => ({ ok: false, message: "Failed" }));
121
- toast(result.message ?? (result.ok ? "Done" : "Failed"), result.ok !== false);
128
+ const result = await response.json().catch(() => null);
129
+ toast(describe(result), result?.ok !== false);
122
130
  await load();
123
131
 
124
132
  // Cloudflare ayarları değişmiş olabilir; genel bakış önbelleği atlanır.
125
133
  if (String(body.type ?? "").startsWith("cf:")) await loadCloudflare(true);
126
134
  }
127
135
 
136
+ /**
137
+ * Sunucu cevabını metne çevirir.
138
+ *
139
+ * `/action` dil taşımıyor: `{ ok, code, params }` dönüyor ve cümle burada
140
+ * kuruluyor. Redis sayımı gibi parçalı cevaplar `parts` ile geliyor, çünkü
141
+ * "veritabanında kaç anahtar var" bilgisi her zaman okunamıyor ve eksik parçayı
142
+ * cümleden düşürmek tek düzgün yol.
143
+ *
144
+ * @param {any} result
145
+ * @returns {string}
146
+ */
147
+ function describe(result) {
148
+ if (!result) return t("toast.failed");
149
+
150
+ if (Array.isArray(result.parts)) {
151
+ const parts = result.parts
152
+ .map((/** @type {any} */ part) => t(`msg.${part.code}`, part.params))
153
+ .filter(Boolean);
154
+ if (parts.length) return parts.join(" · ");
155
+ }
156
+
157
+ if (result.code) return t(`msg.${result.code}`, result.params);
158
+ return t(result.ok ? "toast.done" : "toast.failed");
159
+ }
160
+
128
161
  /* ------------------------------------------------------------------- çizim */
129
162
 
130
163
  function render() {
@@ -144,16 +177,19 @@ function render() {
144
177
 
145
178
  $("env").textContent = proc.env;
146
179
  $("pid").textContent = `pid ${proc.pid}`;
147
- $("uptime").textContent = `up ${formatDuration(proc.uptime)}`;
180
+ $("uptime").textContent = t("chip.up", { value: formatDuration(proc.uptime) });
148
181
  $("memory").textContent = `rss ${formatBytes(proc.memory.rss)}`;
149
182
 
150
183
  $("html-size").textContent = html.size.toLocaleString();
151
- $("html-sub").textContent = `limit ${html.maxEntries.toLocaleString()}`;
184
+ $("html-sub").textContent = t("card.limit", { count: html.maxEntries.toLocaleString() });
152
185
  $("html-bytes").textContent = formatBytes(html.bytes);
153
- $("html-stale").textContent = `${html.stale} stale`;
186
+ $("html-stale").textContent = t("card.stale", { count: html.stale });
154
187
 
155
188
  $("data-size").textContent = data.size.toLocaleString();
156
- $("data-sub").textContent = `${data.stale} stale · limit ${data.maxEntries.toLocaleString()}`;
189
+ $("data-sub").textContent = t("card.staleAndLimit", {
190
+ stale: data.stale,
191
+ limit: data.maxEntries.toLocaleString(),
192
+ });
157
193
 
158
194
  const redisState = !redis.enabled
159
195
  ? "off"
@@ -163,22 +199,22 @@ function render() {
163
199
  ? "connected"
164
200
  : "disconnected";
165
201
 
166
- $("redis-state").textContent = redisState;
202
+ $("redis-state").textContent = t(`state.${redisState}`);
167
203
  $("redis-sub").textContent = redis.enabled
168
- ? `${redis.address} · ${redis.errors} errors`
169
- : "cache.redis.enabled is false";
204
+ ? t("redis.sub", { address: redis.address, errors: redis.errors })
205
+ : t("redis.disabled");
170
206
 
171
207
  renderRedis(redis, redisState);
172
208
  renderHost(host, proc, redis);
173
209
 
174
210
  const done = prewarm.done ?? 0;
175
211
  const total = prewarm.total ?? 0;
176
- $("prewarm-state").textContent = prewarm.active ? `${done}/${total}` : "idle";
212
+ $("prewarm-state").textContent = prewarm.active ? `${done}/${total}` : t("state.idle");
177
213
  $("prewarm-sub").textContent = prewarm.active
178
- ? "round in progress"
214
+ ? t("prewarm.inProgress")
179
215
  : total
180
- ? `last round: ${done}/${total}`
181
- : "no round yet";
216
+ ? t("prewarm.lastRound", { done, total })
217
+ : t("prewarm.never");
182
218
 
183
219
  renderTable();
184
220
  }
@@ -212,10 +248,10 @@ function renderCloudflare() {
212
248
  const zone = cloudflare.zone ?? null;
213
249
 
214
250
  $("cf-heading").textContent = ok
215
- ? [zone?.name, zone?.plan].filter(Boolean).join(" · ") || "connected"
251
+ ? [zone?.name, zone?.plan].filter(Boolean).join(" · ") || t("state.connected")
216
252
  : status.configured
217
- ? `error: ${cloudflare.error}`
218
- : "not connected";
253
+ ? t("cf.error", { error: cloudflare.error })
254
+ : t("cf.notConnected");
219
255
 
220
256
  $("cf-tools").hidden = !ok;
221
257
  $("cf-suggest").hidden = ok || status.configured;
@@ -231,24 +267,28 @@ function renderCloudflare() {
231
267
 
232
268
  /** @type {[string, string, string?][]} */
233
269
  const rows = [
234
- ["Zone", `${zone?.name ?? "—"} (${status.zoneId ?? "?"})`],
235
- ["Plan", zone?.plan ?? "—"],
236
- ["Token", status.tokenSource === "env" ? "from environment" : "from config", "on"],
270
+ [t("cf.zone"), `${zone?.name ?? "—"} (${status.zoneId ?? "?"})`],
271
+ [t("cf.plan"), zone?.plan ?? "—"],
237
272
  [
238
- "Development mode",
273
+ t("cf.token"),
274
+ status.tokenSource === "env" ? t("cf.token.env") : t("cf.token.config"),
275
+ "on",
276
+ ],
277
+ [
278
+ t("cf.devModeLabel"),
239
279
  development
240
- ? `on · ${formatDuration(settings.developmentModeRemaining)} left`
241
- : "off",
280
+ ? t("cf.devMode.on", { time: formatDuration(settings.developmentModeRemaining) })
281
+ : t("state.off"),
242
282
  development ? "bad" : "on",
243
283
  ],
244
- ["Cache level", String(settings.cacheLevel ?? "—")],
245
- ["Browser cache TTL", ttl(settings.browserCacheTtl)],
246
- ["Edge cache TTL", ttl(settings.edgeCacheTtl)],
247
- ["Query string sorting", String(settings.sortQueryString ?? "—")],
248
- ["Always Online", String(settings.alwaysOnline ?? "—")],
249
- ...feature("Tiered Cache", settings.tieredCaching),
250
- ...feature("Regional Tiered Cache", settings.regionalTieredCache),
251
- ...feature("Cache Reserve", settings.cacheReserve),
284
+ [t("cf.cacheLevel"), String(settings.cacheLevel ?? "—")],
285
+ [t("cf.browserTtl"), ttl(settings.browserCacheTtl)],
286
+ [t("cf.edgeTtl"), ttl(settings.edgeCacheTtl)],
287
+ [t("cf.sortQuery"), String(settings.sortQueryString ?? "—")],
288
+ [t("cf.alwaysOnline"), String(settings.alwaysOnline ?? "—")],
289
+ ...feature(t("cf.tiered"), settings.tieredCaching),
290
+ ...feature(t("cf.regionalTiered"), settings.regionalTieredCache),
291
+ ...feature(t("cf.reserve"), settings.cacheReserve),
252
292
  ];
253
293
 
254
294
  $("cf-kv").replaceChildren(...rows.map(kvRow));
@@ -264,9 +304,10 @@ function renderCloudflare() {
264
304
  */
265
305
  function feature(label, value) {
266
306
  if (value === null || value === undefined) {
267
- return [[label, "unavailable on this plan", "off"]];
307
+ return [[label, t("cf.unavailablePlan"), "off"]];
268
308
  }
269
- return [[label, value, value === "on" ? "on" : "off"]];
309
+ const on = value === "on";
310
+ return [[label, on ? t("state.on") : t("state.off"), on ? "on" : "off"]];
270
311
  }
271
312
 
272
313
  /**
@@ -277,7 +318,7 @@ function ttl(seconds) {
277
318
  const value = Number(seconds);
278
319
  if (!Number.isFinite(value)) return "—";
279
320
  // Cloudflare 0'ı "Respect Existing Headers" olarak kullanıyor.
280
- if (value === 0) return "respect origin headers";
321
+ if (value === 0) return t("cf.respectOrigin");
281
322
  return formatDuration(value);
282
323
  }
283
324
 
@@ -305,7 +346,7 @@ async function loadCloudflareAnalytics() {
305
346
  const target = $("cf-report");
306
347
 
307
348
  target.hidden = false;
308
- target.textContent = "Querying Cloudflare…";
349
+ target.textContent = t("cf.querying");
309
350
 
310
351
  const response = await fetch("cloudflare/analytics", {
311
352
  method: "POST",
@@ -321,20 +362,35 @@ async function loadCloudflareAnalytics() {
321
362
  const result = await response.json().catch(() => null);
322
363
 
323
364
  if (!result?.ok) {
324
- target.textContent = `Cloudflare: ${result?.error ?? "query failed"}`;
365
+ target.textContent = t("msg.cf.failed", { error: result?.error ?? t("cf.queryFailed") });
325
366
  return;
326
367
  }
327
368
 
328
- target.replaceChildren(
369
+ // Son dökümü dil değişiminde yeniden kurmak için saklanır: tablo ağdan
370
+ // geliyor ve dil değişti diye Cloudflare'e yeniden gitmek gereksiz.
371
+ lastReport = result;
372
+ renderReport();
373
+ }
374
+
375
+ /** @type {any} */
376
+ let lastReport = null;
377
+
378
+ function renderReport() {
379
+ if (!lastReport) return;
380
+ const result = lastReport;
381
+
382
+ $("cf-report").replaceChildren(
329
383
  result.colos ? edgeTable(result) : statusTable(result),
330
384
  note(
331
385
  result.colos
332
- ? `${result.colos.length} colos served \`${result.path}\` in the last ` +
333
- `${result.hours}h — ${result.hits} from cache, ${result.misses} not. ` +
334
- "Edges that received no request do not appear, even if they hold a copy."
335
- : `Zone-wide cache status over the last ${result.hours}h. ` +
336
- "Numbers come from Cloudflare's sampled dataset: ratios are reliable, " +
337
- "absolute counts are estimates.",
386
+ ? t("cf.edgeNote", {
387
+ colos: result.colos.length,
388
+ path: result.path,
389
+ hours: result.hours,
390
+ hits: result.hits,
391
+ misses: result.misses,
392
+ })
393
+ : t("cf.zoneNote", { hours: result.hours }),
338
394
  ),
339
395
  );
340
396
  }
@@ -345,7 +401,7 @@ async function loadCloudflareAnalytics() {
345
401
  */
346
402
  function edgeTable(result) {
347
403
  return buildTable(
348
- ["Colo", "From cache", "To origin"],
404
+ [t("cf.colo"), t("cf.fromCache"), t("cf.toOrigin")],
349
405
  result.colos.map((/** @type {any} */ row) => [
350
406
  row.colo,
351
407
  String(row.hits),
@@ -362,7 +418,7 @@ function statusTable(result) {
362
418
  const total = result.rows.reduce((sum, /** @type {any} */ row) => sum + row.requests, 0) || 1;
363
419
 
364
420
  return buildTable(
365
- ["Cache status", "Requests", "Share", "Edge bytes"],
421
+ [t("cf.cacheStatus"), t("cf.requests"), t("cf.share"), t("cf.edgeBytes")],
366
422
  result.rows.map((/** @type {any} */ row) => [
367
423
  row.status,
368
424
  row.requests.toLocaleString(),
@@ -425,37 +481,53 @@ function note(text) {
425
481
  * @param {string} state
426
482
  */
427
483
  function renderRedis(redis, state) {
428
- $("redis-heading").textContent = redis.enabled ? state : "not configured";
484
+ $("redis-heading").textContent = redis.enabled ? t(`state.${state}`) : t("state.notConfigured");
429
485
  $("redis-actions").hidden = !redis.enabled;
430
486
  $("redis-suggest").hidden = redis.enabled;
431
487
 
432
488
  /** @type {[string, string, string?][]} */
433
489
  const rows = redis.enabled
434
490
  ? [
435
- ["Connection", state, redis.connected && !redis.bypassed ? "on" : "bad"],
436
- ["Address", `${redis.address}${redis.secure ? " (TLS)" : ""}`],
437
- ["Database", redis.db ?? "default"],
438
- ["Key prefix", `${redis.keyPrefix}:${redis.namespace}`],
439
- ["Build id", redis.buildId || "dev"],
440
- ["Shares HTML", redis.html ? "yes" : "no", redis.html ? "on" : "off"],
441
- ["Shares data", redis.data ? "yes" : "no", redis.data ? "on" : "off"],
442
491
  [
443
- "Compressed bodies",
444
- redis.storeEncoded ? "shared" : "local only",
492
+ t("redis.connection"),
493
+ t(`state.${state}`),
494
+ redis.connected && !redis.bypassed ? "on" : "bad",
495
+ ],
496
+ [t("redis.address"), `${redis.address}${redis.secure ? " (TLS)" : ""}`],
497
+ [t("redis.database"), redis.db ?? t("state.default")],
498
+ [t("redis.keyPrefix"), `${redis.keyPrefix}:${redis.namespace}`],
499
+ [t("redis.buildId"), redis.buildId || "dev"],
500
+ [
501
+ t("redis.sharesHtml"),
502
+ redis.html ? t("state.yes") : t("state.no"),
503
+ redis.html ? "on" : "off",
504
+ ],
505
+ [
506
+ t("redis.sharesData"),
507
+ redis.data ? t("state.yes") : t("state.no"),
508
+ redis.data ? "on" : "off",
509
+ ],
510
+ [
511
+ t("redis.compressed"),
512
+ redis.storeEncoded ? t("redis.compressed.shared") : t("redis.compressed.local"),
445
513
  redis.storeEncoded ? "on" : "off",
446
514
  ],
447
515
  [
448
- "Purge broadcast",
449
- redis.events ? (redis.subscribed ? "subscribed" : "publish only") : "off",
516
+ t("redis.broadcast"),
517
+ redis.events
518
+ ? redis.subscribed
519
+ ? t("redis.broadcast.subscribed")
520
+ : t("redis.broadcast.publish")
521
+ : t("state.off"),
450
522
  redis.events && redis.subscribed ? "on" : "off",
451
523
  ],
452
- ["Command timeout", `${redis.commandTimeoutMs} ms`],
453
- ["Command errors", String(redis.errors), redis.errors ? "bad" : "on"],
524
+ [t("redis.timeout"), `${redis.commandTimeoutMs} ${t("unit.ms")}`],
525
+ [t("redis.errors"), String(redis.errors), redis.errors ? "bad" : "on"],
454
526
  ]
455
527
  : [
456
- ["Tier", "in-process only"],
457
- ["Replicas sharing this cache", "none"],
458
- ["Purge broadcast", "local only", "off"],
528
+ [t("redis.tier"), t("redis.tier.inProcess")],
529
+ [t("redis.replicas"), t("state.none")],
530
+ [t("redis.broadcast"), t("redis.broadcast.local"), "off"],
459
531
  ];
460
532
 
461
533
  $("redis-kv").replaceChildren(...rows.map(kvRow));
@@ -484,14 +556,16 @@ function renderHost(host, proc, redis) {
484
556
  `${formatBytes(host.memory.used)} / ${formatBytes(host.memory.total)}`;
485
557
  meter("ram-bar", ramUsed);
486
558
  $("ram-note").textContent = redis.connected
487
- ? `This process: ${formatBytes(proc.memory.rss)} RSS.`
488
- : `This process holds the whole cache: ${formatBytes(proc.memory.rss)} RSS, ` +
489
- `${formatBytes(latest.html.bytes)} of it HTML.`;
559
+ ? t("host.ramShared", { rss: formatBytes(proc.memory.rss) })
560
+ : t("host.ramOnly", {
561
+ rss: formatBytes(proc.memory.rss),
562
+ html: formatBytes(latest.html.bytes),
563
+ });
490
564
 
491
565
  if (!host.disk) {
492
- $("disk-text").textContent = "unavailable";
566
+ $("disk-text").textContent = t("state.unavailable");
493
567
  meter("disk-bar", 0);
494
- $("disk-note").textContent = "This platform does not report filesystem stats.";
568
+ $("disk-note").textContent = t("host.noStats");
495
569
  return;
496
570
  }
497
571
 
@@ -500,8 +574,10 @@ function renderHost(host, proc, redis) {
500
574
  `${formatBytes(host.disk.total - host.disk.free)} / ${formatBytes(host.disk.total)}`;
501
575
  meter("disk-bar", diskUsed);
502
576
  // Önbellek diske yazılmıyor; disk build çıktısı ve log için önemli.
503
- $("disk-note").textContent =
504
- `${formatBytes(host.disk.free)} free on ${host.disk.path} — build output and logs live here.`;
577
+ $("disk-note").textContent = t("host.diskNote", {
578
+ free: formatBytes(host.disk.free),
579
+ path: host.disk.path,
580
+ });
505
581
  }
506
582
 
507
583
  /**
@@ -522,13 +598,31 @@ function renderTable() {
522
598
 
523
599
  $("listed").textContent =
524
600
  source.matched > source.entries.length
525
- ? `${source.entries.length} of ${source.matched} shown`
526
- : `${source.matched} shown`;
601
+ ? t("entries.shownPartial", { shown: source.entries.length, total: source.matched })
602
+ : t("entries.shown", { total: source.matched });
527
603
 
528
- thead.innerHTML =
604
+ const columns =
529
605
  tab === "html"
530
- ? `<tr><th>Key</th><th>State</th><th class="num">Size</th><th class="num">Status</th><th class="num">Expires</th><th class="num">Deps</th><th>Encodings</th><th></th></tr>`
531
- : `<tr><th>Key</th><th>State</th><th class="num">Expires</th><th></th></tr>`;
606
+ ? [
607
+ ["entries.key"],
608
+ ["entries.state"],
609
+ ["entries.size", "num"],
610
+ ["entries.status", "num"],
611
+ ["entries.expires", "num"],
612
+ ["entries.deps", "num"],
613
+ ["entries.encodings"],
614
+ [null],
615
+ ]
616
+ : [["entries.key"], ["entries.state"], ["entries.expires", "num"], [null]];
617
+
618
+ const headRow = document.createElement("tr");
619
+ for (const [key, className] of columns) {
620
+ const th = document.createElement("th");
621
+ if (className) th.className = className;
622
+ if (key) th.textContent = t(key);
623
+ headRow.append(th);
624
+ }
625
+ thead.replaceChildren(headRow);
532
626
 
533
627
  tbody.replaceChildren(
534
628
  ...source.entries.map((/** @type {any} */ entry) => row(entry)),
@@ -563,7 +657,7 @@ function row(entry) {
563
657
  const copy = document.createElement("button");
564
658
  copy.className = "as-text";
565
659
  copy.type = "button";
566
- copy.title = "Copy key";
660
+ copy.title = t("entries.copyKey");
567
661
  copy.textContent = entry.key;
568
662
  copy.addEventListener("click", () => void copyKey(entry.key));
569
663
  key.append(copy);
@@ -574,7 +668,7 @@ function row(entry) {
574
668
  const state = document.createElement("td");
575
669
  const tag = document.createElement("span");
576
670
  tag.className = entry.stale ? "tag stale" : "tag fresh";
577
- tag.textContent = entry.stale ? "stale" : "fresh";
671
+ tag.textContent = entry.stale ? t("entries.stale") : t("entries.fresh");
578
672
  state.append(tag);
579
673
  tr.append(state);
580
674
 
@@ -598,8 +692,8 @@ function row(entry) {
598
692
  if (tab === "html" && cloudflare?.ok) {
599
693
  const purge = document.createElement("button");
600
694
  purge.className = "tiny";
601
- purge.textContent = "cf purge";
602
- purge.title = "Purge this URL at Cloudflare";
695
+ purge.textContent = t("cf.rowPurge");
696
+ purge.title = t("cf.rowPurgeTitle");
603
697
  purge.addEventListener("click", () => {
604
698
  void act({ type: "cf:purge-urls", paths: [entry.url] });
605
699
  });
@@ -608,7 +702,7 @@ function row(entry) {
608
702
 
609
703
  const drop = document.createElement("button");
610
704
  drop.className = "tiny danger";
611
- drop.textContent = "drop";
705
+ drop.textContent = t("entries.drop");
612
706
  drop.addEventListener("click", () => {
613
707
  void act({ type: tab === "html" ? "html:drop" : "data:drop", key: entry.key });
614
708
  });
@@ -628,11 +722,11 @@ function row(entry) {
628
722
  async function copyKey(key) {
629
723
  try {
630
724
  await navigator.clipboard.writeText(key);
631
- toast("Key copied");
725
+ toast(t("toast.keyCopied"));
632
726
  } catch {
633
727
  $("prefix").value = key;
634
728
  $("prefix").focus();
635
- toast("Clipboard is unavailable — key moved to the prefix field", false);
729
+ toast(t("toast.clipboard"), false);
636
730
  }
637
731
  }
638
732
 
@@ -694,7 +788,7 @@ for (const name of /** @type {const} */ (["html", "data"])) {
694
788
  $("invalidate").addEventListener("click", () => {
695
789
  const target = $("target").value.trim();
696
790
  if (!target) {
697
- toast("Enter a path or pattern first.", false);
791
+ toast(t("toast.needTarget"), false);
698
792
  return;
699
793
  }
700
794
 
@@ -702,13 +796,13 @@ $("invalidate").addEventListener("click", () => {
702
796
  });
703
797
 
704
798
  $("clear-html").addEventListener("click", () => {
705
- if (!confirm("Clear the entire HTML cache? Every page goes cold.")) return;
799
+ if (!confirm(t("confirm.clearHtml"))) return;
706
800
  void act({ type: "html:clear" });
707
801
  });
708
802
 
709
803
  $("clear-data").addEventListener("click", () => {
710
804
  const prefix = $("prefix").value.trim();
711
- if (!prefix && !confirm("Clear every data entry? Upstream traffic will spike.")) {
805
+ if (!prefix && !confirm(t("confirm.clearData"))) {
712
806
  return;
713
807
  }
714
808
 
@@ -734,7 +828,7 @@ $("redis-data").addEventListener("click", () =>
734
828
  $("cf-refresh").addEventListener("click", () => void loadCloudflare(true));
735
829
 
736
830
  $("cf-purge-all").addEventListener("click", () => {
737
- if (!confirm("Purge the entire Cloudflare cache for this zone?")) return;
831
+ if (!confirm(t("confirm.purgeAll"))) return;
738
832
  void act({ type: "cf:purge-everything" });
739
833
  });
740
834
 
@@ -743,18 +837,18 @@ $("cf-purge-all").addEventListener("click", () => {
743
837
  $("cf-purge-cached").addEventListener("click", () => {
744
838
  const paths = (latest?.html.entries ?? []).map((/** @type {any} */ entry) => entry.url);
745
839
  if (!paths.length) {
746
- toast("No cached pages to purge.", false);
840
+ toast(t("toast.noCachedPages"), false);
747
841
  return;
748
842
  }
749
843
 
750
- if (!confirm(`Purge ${paths.length} URLs at Cloudflare?`)) return;
844
+ if (!confirm(t("confirm.purgeUrls", { count: paths.length }))) return;
751
845
  void act({ type: "cf:purge-urls", paths });
752
846
  });
753
847
 
754
848
  $("cf-purge-keys").addEventListener("click", () => {
755
849
  const values = $("cf-values").value.trim();
756
850
  if (!values) {
757
- toast("Enter at least one value.", false);
851
+ toast(t("toast.needValue"), false);
758
852
  return;
759
853
  }
760
854
 
@@ -777,9 +871,7 @@ $("cf-reserve").addEventListener("click", () => {
777
871
  });
778
872
 
779
873
  $("cf-reserve-clear").addEventListener("click", () => {
780
- if (!confirm("Clear Cache Reserve? Cloudflare will refetch everything from the origin.")) {
781
- return;
782
- }
874
+ if (!confirm(t("confirm.clearReserve"))) return;
783
875
  void act({ type: "cf:clear-reserve" });
784
876
  });
785
877
 
@@ -803,6 +895,21 @@ document.addEventListener("visibilitychange", () => {
803
895
  }
804
896
  });
805
897
 
898
+ /* -------------------------------------------------------------------- dil */
899
+
900
+ // Dil değişimi hiçbir isteğe yol açmaz: statik metinler sözlükten yeniden
901
+ // yazılır, dinamik olanlar elde duran son dökümle tekrar çizilir.
902
+ $("language").append(
903
+ languageSelect(() => {
904
+ applyTranslations();
905
+ if (latest) render();
906
+ renderCloudflare();
907
+ renderReport();
908
+ }),
909
+ );
910
+
911
+ applyTranslations();
912
+
806
913
  void load();
807
914
  void loadCloudflare();
808
915
  startPolling();