jskelet 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +620 -596
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +130 -130
  5. package/docs/01-baslangic.md +291 -291
  6. package/docs/02-mimari.md +310 -309
  7. package/docs/03-routing.md +515 -515
  8. package/docs/04-render-ve-sablonlar.md +661 -661
  9. package/docs/05-islands.md +486 -486
  10. package/docs/06-cache.md +1443 -1423
  11. package/docs/07-yapilandirma.md +12 -6
  12. package/docs/08-build.md +429 -428
  13. package/docs/09-dev-araclari.md +364 -364
  14. package/docs/10-dagitim.md +338 -338
  15. package/docs/12-panel-ve-oturum.md +478 -478
  16. package/docs/README.md +83 -83
  17. package/docs/en/01-getting-started.md +298 -298
  18. package/docs/en/02-architecture.md +329 -328
  19. package/docs/en/03-routing.md +531 -531
  20. package/docs/en/04-rendering.md +669 -669
  21. package/docs/en/05-islands.md +497 -497
  22. package/docs/en/06-caching.md +1453 -1431
  23. package/docs/en/07-configuration.md +1219 -1214
  24. package/docs/en/08-build.md +447 -446
  25. package/docs/en/09-dev-tools.md +373 -373
  26. package/docs/en/10-deployment.md +340 -340
  27. package/docs/en/11-migration.md +398 -398
  28. package/docs/en/12-dashboards-and-sessions.md +488 -488
  29. package/docs/en/README.md +87 -87
  30. package/package.json +137 -137
  31. package/src/build/ensure-build.mjs +19 -19
  32. package/src/build/paths.mjs +153 -153
  33. package/src/build/resolve-peer.mjs +36 -36
  34. package/src/build/tasks/client.mjs +349 -349
  35. package/src/build/tasks/css.mjs +235 -235
  36. package/src/build/tasks/fonts.mjs +146 -146
  37. package/src/build/tasks/icons.mjs +357 -357
  38. package/src/build/tasks/images.mjs +244 -244
  39. package/src/build/tasks/precompress.mjs +78 -78
  40. package/src/build/tasks/templates.mjs +20 -20
  41. package/src/client/admin/i18n.js +764 -764
  42. package/src/client/admin/login.html +74 -74
  43. package/src/client/admin/panel.css +809 -809
  44. package/src/client/admin/panel.html +495 -495
  45. package/src/client/admin/panel.js +1251 -1251
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +745 -745
  48. package/src/client/devtools/seo.js +628 -628
  49. package/src/client/dom.js +95 -95
  50. package/src/client/form.js +192 -192
  51. package/src/client/index.js +45 -45
  52. package/src/client/registry.js +305 -305
  53. package/src/client/safe-image.js +91 -91
  54. package/src/client/shared-cookie.js +225 -225
  55. package/src/client/store.js +36 -36
  56. package/src/client/swap.js +188 -188
  57. package/src/compile/codegen.js +336 -336
  58. package/src/compile/compile-all.js +149 -149
  59. package/src/compile/errors.js +66 -66
  60. package/src/compile/expr.js +409 -409
  61. package/src/compile/index.js +17 -17
  62. package/src/compile/parse.js +541 -541
  63. package/src/compile/resolve.js +211 -211
  64. package/src/compile/scan-exports.js +51 -51
  65. package/src/config/defaults.js +17 -1
  66. package/src/config/index.js +13 -0
  67. package/src/config/pattern.js +107 -107
  68. package/src/generate.mjs +163 -163
  69. package/src/http/control-flow.js +71 -71
  70. package/src/http/cookies-entry.js +21 -21
  71. package/src/http/cookies.js +277 -277
  72. package/src/http/request-cache.js +46 -46
  73. package/src/http/request-context.js +165 -165
  74. package/src/http/shared-cookie.js +178 -178
  75. package/src/index.js +101 -101
  76. package/src/init.mjs +230 -230
  77. package/src/migrate/apply.mjs +262 -262
  78. package/src/migrate/babel.mjs +79 -79
  79. package/src/migrate/classify.mjs +155 -155
  80. package/src/migrate/config.mjs +126 -126
  81. package/src/migrate/fs-walk.mjs +191 -191
  82. package/src/migrate/parse.mjs +26 -26
  83. package/src/migrate/scan.mjs +177 -177
  84. package/src/migrate/transform/expr-source.mjs +168 -168
  85. package/src/migrate/transform/island.mjs +67 -67
  86. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  87. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  88. package/src/migrate/transform/page-split.mjs +435 -435
  89. package/src/migrate/write.mjs +81 -81
  90. package/src/migrate.mjs +171 -171
  91. package/src/runtime/alias-hooks.mjs +119 -119
  92. package/src/runtime/register.mjs +4 -4
  93. package/src/server/admin/actions.js +229 -229
  94. package/src/server/admin/auth.js +125 -125
  95. package/src/server/admin/event-log.js +151 -151
  96. package/src/server/admin/gate.js +209 -209
  97. package/src/server/admin/inventory.js +188 -188
  98. package/src/server/admin/mount.js +56 -56
  99. package/src/server/admin/router.js +216 -216
  100. package/src/server/admin/snapshot.js +241 -241
  101. package/src/server/assets.js +147 -147
  102. package/src/server/auth/handoff.js +309 -309
  103. package/src/server/cache-blob.js +70 -0
  104. package/src/server/cache-deps.js +42 -42
  105. package/src/server/cache-vary.js +113 -113
  106. package/src/server/cloudflare.js +607 -607
  107. package/src/server/create-app.js +366 -366
  108. package/src/server/data-cache.js +553 -462
  109. package/src/server/dev/report.js +485 -485
  110. package/src/server/dev/socket.js +170 -170
  111. package/src/server/dev/version-check.mjs +139 -139
  112. package/src/server/disk-cache.js +233 -0
  113. package/src/server/ejs-adapter.js +59 -59
  114. package/src/server/html-cache.js +1196 -1122
  115. package/src/server/image-optimizer.js +500 -407
  116. package/src/server/logs/access-middleware.js +66 -66
  117. package/src/server/logs/file-sink.js +193 -66
  118. package/src/server/logs/pipeline.js +165 -158
  119. package/src/server/logs/s3-put.js +214 -214
  120. package/src/server/logs/s3-sink.js +112 -112
  121. package/src/server/metadata.js +102 -102
  122. package/src/server/middleware/compression.js +205 -205
  123. package/src/server/middleware/csrf.js +134 -134
  124. package/src/server/middleware/dev-gate.js +75 -75
  125. package/src/server/middleware/headers.js +37 -37
  126. package/src/server/middleware/redirects.js +32 -32
  127. package/src/server/middleware/robots-txt.js +341 -341
  128. package/src/server/middleware/static-precompressed.js +121 -100
  129. package/src/server/middleware/trailing-slash.js +53 -53
  130. package/src/server/middleware/upstream-proxy.js +141 -141
  131. package/src/server/og-image.js +356 -356
  132. package/src/server/port-guard.js +255 -255
  133. package/src/server/prewarm.js +1082 -1058
  134. package/src/server/redis.js +588 -569
  135. package/src/server/render.js +4 -4
  136. package/src/server/router.js +157 -157
  137. package/src/server/status-page.js +265 -265
  138. package/src/server/upstream-limiter.js +376 -376
  139. package/src/server/upstream-tracking.js +166 -166
  140. package/src/shared/cookie-domain.js +66 -66
  141. package/src/start.mjs +22 -22
  142. package/src/templates/layout.ejs +30 -30
  143. package/src/templates/layout.jsk +30 -30
  144. package/src/version.mjs +31 -31
  145. package/src/views/components/loader.js +101 -101
  146. package/src/views/helpers/html.js +102 -102
  147. package/src/views/helpers/tags.js +375 -375
  148. package/types/config/defaults.d.ts +15 -1
  149. package/types/config/index.d.ts +8 -0
  150. package/types/server/cache-blob.d.ts +13 -0
  151. package/types/server/data-cache.d.ts +9 -0
  152. package/types/server/disk-cache.d.ts +36 -0
  153. package/types/server/html-cache.d.ts +26 -3
  154. package/types/server/logs/file-sink.d.ts +16 -5
  155. package/types/server/redis.d.ts +2 -1
@@ -1,764 +1,764 @@
1
- /**
2
- * Admin panelinin sözlüğü.
3
- *
4
- * Panel build hattından geçmiyor, bu yüzden hazır bir i18n kütüphanesi yok:
5
- * düz bir nesne, `{ad}` yer tutucusu ve `data-i18n` gezintisi kadarı yetiyor.
6
- *
7
- * Sunucudan gelen mesajlar **çevrilmez, kodlanır**: `/action` cevabı
8
- * `{ ok, code, params }` döner ve metin burada kurulur. Böylece sunucu tarafı
9
- * arayüz dilini hiç bilmiyor — panel iki dilde konuşurken framework'ün log'u
10
- * ve API'si tek dilde kalıyor.
11
- *
12
- * Dil seçimi `localStorage`'da: panel oturumu şifreyle açılıyor ve tercihin
13
- * sunucuya taşınmasının bir karşılığı yok.
14
- */
15
-
16
- const STORAGE_KEY = "jskelet.admin.lang";
17
-
18
- /**
19
- * Sözlükler. Dışa açık, çünkü testler iki dilin anahtar kümesinin birebir
20
- * eşleştiğini ve şablonlarda geçen her anahtarın karşılığı olduğunu doğruluyor.
21
- *
22
- * @type {Record<string, Record<string, string>>}
23
- */
24
- export const MESSAGES = {
25
- en: {
26
- /* --- kabuk --- */
27
- "nav.overview": "Overview",
28
- "nav.cache": "Cache",
29
- "nav.routes": "Routes",
30
- "nav.views": "Views",
31
- "nav.logs": "Logs",
32
- "nav.system": "System",
33
- "shell.live": "live",
34
- "shell.refresh": "Refresh",
35
- "shell.signOut": "Sign out",
36
- "shell.language": "Language",
37
- "chip.up": "up {value}",
38
- "chip.sse.live": "SSE live",
39
- "chip.sse.off": "SSE off",
40
- "chip.sse.error": "SSE error",
41
-
42
- /* --- kartlar --- */
43
- "card.htmlEntries": "HTML entries",
44
- "card.htmlMemory": "HTML in memory",
45
- "card.dataEntries": "Data entries",
46
- "card.sharedTier": "Shared tier",
47
- "card.prewarm": "Prewarm",
48
- "card.limit": "limit {count}",
49
- "card.stale": "{count} stale",
50
- "card.staleAndLimit": "{stale} stale · limit {limit}",
51
-
52
- /* --- durumlar --- */
53
- "state.on": "on",
54
- "state.off": "off",
55
- "state.bypassed": "bypassed",
56
- "state.connected": "connected",
57
- "state.disconnected": "disconnected",
58
- "state.notConfigured": "not configured",
59
- "state.idle": "idle",
60
- "state.yes": "yes",
61
- "state.no": "no",
62
- "state.none": "none",
63
- "state.default": "default",
64
- "state.unavailable": "unavailable",
65
-
66
- /* --- ısıtma --- */
67
- "prewarm.inProgress": "round in progress",
68
- "prewarm.lastRound": "last round: {done}/{total}",
69
- "prewarm.never": "no round yet",
70
-
71
- /* --- paylaşımlı kademe --- */
72
- "redis.section": "Shared tier",
73
- "redis.sub": "{address} · {errors} errors",
74
- "redis.disabled": "cache.redis.enabled is false",
75
- "redis.inspect": "Count keys in Redis",
76
- "redis.dropHtml": "Drop shared HTML keys",
77
- "redis.dropData": "Drop shared data keys",
78
- "redis.connection": "Connection",
79
- "redis.address": "Address",
80
- "redis.database": "Database",
81
- "redis.keyPrefix": "Key prefix",
82
- "redis.buildId": "Build id",
83
- "redis.sharesHtml": "Shares HTML",
84
- "redis.sharesData": "Shares data",
85
- "redis.compressed": "Compressed bodies",
86
- "redis.compressed.shared": "shared",
87
- "redis.compressed.local": "local only",
88
- "redis.broadcast": "Purge broadcast",
89
- "redis.broadcast.subscribed": "subscribed",
90
- "redis.broadcast.publish": "publish only",
91
- "redis.broadcast.local": "local only",
92
- "redis.timeout": "Command timeout",
93
- "redis.errors": "Command errors",
94
- "redis.tier": "Tier",
95
- "redis.tier.inProcess": "in-process only",
96
- "redis.replicas": "Replicas sharing this cache",
97
- "redis.suggest.title": "No shared tier.",
98
- "redis.suggest.body":
99
- "Every entry above lives in this process only, so each replica warms up " +
100
- "on its own and an invalidation reaches whichever replica received the " +
101
- "request. Adding Redis keeps the in-process cache primary and only does " +
102
- "what a single process cannot: serve HTML another replica already " +
103
- "rendered, and broadcast purges to all of them.",
104
- "redis.suggest.hint":
105
- "Worth it from the second replica onwards. On a single instance the gain " +
106
- "is close to zero, which is why it is off by default.",
107
-
108
- /* --- cloudflare --- */
109
- "cf.section": "Cloudflare",
110
- "cf.refresh": "Refresh",
111
- "cf.notConnected": "not connected",
112
- "cf.error": "error: {error}",
113
- "cf.purgeAll": "Purge everything",
114
- "cf.purgeCached": "Purge URLs cached here",
115
- "cf.devMode": "development mode (bypass cache for 3h)",
116
- "cf.kind.prefixes": "prefixes",
117
- "cf.kind.hosts": "hosts",
118
- "cf.kind.tags": "cache tags",
119
- "cf.values.placeholder":
120
- "example.com/news · space or comma separated, 100 per request",
121
- "cf.purge": "Purge",
122
- "cf.toggleTiered": "Toggle Tiered Cache",
123
- "cf.toggleReserve": "Toggle Cache Reserve",
124
- "cf.clearReserve": "Clear Cache Reserve",
125
- "cf.path.placeholder":
126
- "/news/some-post — which edges served this path (blank = whole zone)",
127
- "cf.queryAnalytics": "Query analytics",
128
- "cf.edgeHint":
129
- "Cloudflare has no endpoint that lists which edges currently hold a copy " +
130
- "of a URL, and no way to warm a chosen edge from the origin — an object " +
131
- "only enters an edge cache through a real request routed there. What you " +
132
- "get above is observation: which colos served the path from cache and " +
133
- "which went to the origin. Warming is our prewarm pass plus Tiered Cache " +
134
- "or Cache Reserve behind it.",
135
- "cf.zone": "Zone",
136
- "cf.plan": "Plan",
137
- "cf.token": "Token",
138
- "cf.token.env": "from environment",
139
- "cf.token.config": "from config",
140
- "cf.devModeLabel": "Development mode",
141
- "cf.devMode.on": "on · {time} left",
142
- "cf.cacheLevel": "Cache level",
143
- "cf.browserTtl": "Browser cache TTL",
144
- "cf.edgeTtl": "Edge cache TTL",
145
- "cf.sortQuery": "Query string sorting",
146
- "cf.alwaysOnline": "Always Online",
147
- "cf.tiered": "Tiered Cache",
148
- "cf.regionalTiered": "Regional Tiered Cache",
149
- "cf.reserve": "Cache Reserve",
150
- "cf.unavailablePlan": "unavailable on this plan",
151
- "cf.respectOrigin": "respect origin headers",
152
- "cf.querying": "Querying Cloudflare…",
153
- "cf.queryFailed": "query failed",
154
- "cf.colo": "Colo",
155
- "cf.fromCache": "From cache",
156
- "cf.toOrigin": "To origin",
157
- "cf.cacheStatus": "Cache status",
158
- "cf.requests": "Requests",
159
- "cf.share": "Share",
160
- "cf.edgeBytes": "Edge bytes",
161
- "cf.edgeNote":
162
- "{colos} colos served `{path}` in the last {hours}h — {hits} from cache, " +
163
- "{misses} not. Edges that received no request do not appear, even if they " +
164
- "hold a copy.",
165
- "cf.zoneNote":
166
- "Zone-wide cache status over the last {hours}h. Numbers come from " +
167
- "Cloudflare's sampled dataset: ratios are reliable, absolute counts are " +
168
- "estimates.",
169
- "cf.suggest.title": "Cloudflare is not connected.",
170
- "cf.suggest.body":
171
- "The cache above is the origin cache; the copy your visitors actually get " +
172
- "is at the edge. Connecting a zone lets a purge here reach both, and " +
173
- "shows the cache hit ratio that decides whether the origin cache is even " +
174
- "being asked.",
175
- "cf.suggest.hint":
176
- "Token permissions: <code>Zone.Cache Purge</code> to purge, " +
177
- "<code>Zone.Zone Settings</code> to change settings, " +
178
- "<code>Zone.Analytics</code> (read) for the hit ratio. The token is read " +
179
- "from the environment and never leaves the server.",
180
- "cf.rowPurge": "cf purge",
181
- "cf.rowPurgeTitle": "Purge this URL at Cloudflare",
182
-
183
- /* --- host --- */
184
- "host.section": "Host",
185
- "host.cpu": "CPU",
186
- "host.memory": "Memory",
187
- "host.disk": "Disk",
188
- "host.noStats": "This platform does not report filesystem stats.",
189
- "host.cpuNote": "This process across {cores} cores (since last refresh).",
190
- "host.ramShared": "This process: {rss} RSS · {heap} heap.",
191
- "host.ramOnly":
192
- "This process holds the whole cache: {rss} RSS · {heap} heap, {html} of it HTML.",
193
- "host.diskNote":
194
- "Project files on {path} (excluding node_modules) · {free} free on the volume.",
195
-
196
- /* --- işlemler --- */
197
- "actions.section": "Actions",
198
- "actions.target.placeholder": "/news/:slug · /news/some-post · invalidation target",
199
- "actions.hard": "hard (delete instead of marking stale)",
200
- "actions.invalidate": "Invalidate",
201
- "actions.prefix.placeholder": "data key prefix — empty clears every data entry",
202
- "actions.clearData": "Clear data cache",
203
- "actions.clearHtml": "Clear HTML cache",
204
- "actions.prewarm": "Prewarm all paths",
205
- "actions.hint":
206
- "Invalidation marks entries stale by default: visitors keep getting the " +
207
- "old HTML while a single background refresh runs. Hard mode deletes them, " +
208
- "so the next visitor waits for a cold render.",
209
-
210
- /* --- girdiler --- */
211
- "tab.data": "Data",
212
- "entries.section": "Entries",
213
- "entries.search": "Filter by key",
214
- "entries.empty": "No entries.",
215
- "entries.shownPartial": "{shown} of {total} shown",
216
- "entries.shown": "{total} shown",
217
- "entries.key": "Key",
218
- "entries.state": "State",
219
- "entries.size": "Size",
220
- "entries.status": "Status",
221
- "entries.expires": "Expires",
222
- "entries.deps": "Deps",
223
- "entries.encodings": "Encodings",
224
- "entries.fresh": "fresh",
225
- "entries.stale": "stale",
226
- "entries.drop": "drop",
227
- "entries.copyKey": "Copy key",
228
-
229
- /* --- alt bilgi --- */
230
- "footer.panel": "Admin panel",
231
- "footer.rotates": "Password rotates on restart",
232
-
233
- /* --- bildirimler --- */
234
- "toast.keyCopied": "Key copied",
235
- "toast.clipboard": "Clipboard is unavailable — key moved to the prefix field",
236
- "toast.needTarget": "Enter a path or pattern first.",
237
- "toast.needValue": "Enter at least one value.",
238
- "toast.noCachedPages": "No cached pages to purge.",
239
- "toast.failed": "Failed",
240
- "toast.done": "Done",
241
- "confirm.clearHtml": "Clear the entire HTML cache? Every page goes cold.",
242
- "confirm.clearData": "Clear every data entry? Upstream traffic will spike.",
243
- "confirm.purgeAll": "Purge the entire Cloudflare cache for this zone?",
244
- "confirm.purgeUrls": "Purge {count} URLs at Cloudflare?",
245
- "confirm.clearReserve":
246
- "Clear Cache Reserve? Cloudflare will refetch everything from the origin.",
247
-
248
- /* --- sunucu cevapları --- */
249
- "msg.html.cleared": "HTML cache cleared ({count} entries)",
250
- "msg.data.cleared": "Data cache cleared ({count} entries)",
251
- "msg.data.clearedPrefix": "{count} data entries dropped under `{prefix}`",
252
- "msg.target.invalid": "Target must start with `/`",
253
- "msg.html.marked": "{count} entries marked stale for `{target}`",
254
- "msg.html.dropped": "{count} entries dropped for `{target}`",
255
- "msg.key.missing": "Missing key",
256
- "msg.entry.dropped": "Dropped `{key}`",
257
- "msg.entry.absent": "`{key}` was not cached",
258
- "msg.redis.unreachable": "Redis is not reachable",
259
- "msg.redis.notConnected": "Redis is not connected",
260
- "msg.redis.htmlKeys": "{count} html keys",
261
- "msg.redis.dataKeys": "{count} data keys",
262
- "msg.redis.dbKeys": "{count} keys in db",
263
- "msg.redis.memory": "{value} used",
264
- "msg.redis.droppedHtml": "{count} shared html keys dropped",
265
- "msg.redis.droppedData": "{count} shared data keys dropped",
266
- "msg.cf.purgedEverything": "Cloudflare cache purged (everything)",
267
- "msg.cf.noPaths": "No paths given",
268
- "msg.cf.noHostname": "Could not build absolute URLs — set cache().cloudflare.hostname",
269
- "msg.cf.purgedUrls": "Purged {count} URLs at Cloudflare in {batches} request(s)",
270
- "msg.cf.nothingToPurge": "Nothing to purge",
271
- "msg.cf.purgedKeys": "Purged {count} {kind} at Cloudflare",
272
- "msg.cf.settingChanged": "Cloudflare {id} is now {value}",
273
- "msg.cf.featureChanged": "Cloudflare {feature} turned {value}",
274
- "msg.cf.reserveClearing":
275
- "Cache Reserve clear started — it runs asynchronously at Cloudflare",
276
- "msg.cf.failed": "Cloudflare: {error}",
277
- "msg.prewarm.busy": "A prewarm round is already running",
278
- "msg.prewarm.paths": "Prewarming {count} paths",
279
- "msg.prewarm.all": "Prewarming all paths",
280
- "msg.action.unknown": "Unknown action: {type}",
281
-
282
- /* --- giriş --- */
283
- "login.title": "Admin panel",
284
- "login.lead": "This run's password is printed in the server log (<code>ADMIN</code> box).",
285
- "login.placeholder": "32-character password",
286
- "login.submit": "Unlock",
287
- "login.hint": "Three failed attempts block this address for 24 hours.",
288
- "login.tooMany": "Too many attempts.",
289
- "login.invalid": "Invalid password.",
290
-
291
- "upstream.section": "Upstream",
292
- "upstream.host": "Host",
293
- "upstream.rate": "Rate",
294
- "upstream.state": "State",
295
- "upstream.empty": "No upstream hosts tracked.",
296
-
297
- "routes.section": "Registered routes",
298
- "routes.method": "Method",
299
- "routes.path": "Path",
300
- "routes.lastStatus": "Last status",
301
- "routes.lastMs": "Last ms",
302
- "routes.hits": "Hits",
303
- "routes.empty": "No routes.",
304
- "routes.modules": "Route modules",
305
- "routes.file": "File",
306
- "routes.openLogs": "Logs",
307
-
308
- "views.section": "Views",
309
- "views.path": "Path",
310
- "views.kind": "Kind",
311
- "views.search": "Filter views…",
312
- "views.empty": "No views.",
313
-
314
- "logs.section": "Live logs",
315
- "logs.search": "Search url / message…",
316
- "logs.kind.all": "all kinds",
317
- "logs.method.all": "all methods",
318
- "logs.status.all": "all status",
319
- "logs.cache.all": "all cache",
320
- "logs.path": "path / route…",
321
- "logs.pause": "pause",
322
- "logs.clear": "Clear view",
323
- "logs.empty": "No log lines.",
324
- "logs.shown": "{shown} shown · {total} buffered",
325
-
326
- /* --- birimler --- */
327
- "unit.second": "s",
328
- "unit.minute": "m",
329
- "unit.hour": "h",
330
- "unit.day": "d",
331
- "unit.ms": "ms",
332
- },
333
-
334
- tr: {
335
- "nav.overview": "Özet",
336
- "nav.cache": "Önbellek",
337
- "nav.routes": "Rotalar",
338
- "nav.views": "Şablonlar",
339
- "nav.logs": "Loglar",
340
- "nav.system": "Sistem",
341
- "shell.live": "canlı",
342
- "shell.refresh": "Yenile",
343
- "shell.signOut": "Çıkış",
344
- "shell.language": "Dil",
345
- "chip.up": "{value} açık",
346
- "chip.sse.live": "SSE canlı",
347
- "chip.sse.off": "SSE kapalı",
348
- "chip.sse.error": "SSE hata",
349
-
350
- "card.htmlEntries": "HTML girdisi",
351
- "card.htmlMemory": "Bellekteki HTML",
352
- "card.dataEntries": "Veri girdisi",
353
- "card.sharedTier": "Paylaşımlı kademe",
354
- "card.prewarm": "Isıtma",
355
- "card.limit": "sınır {count}",
356
- "card.stale": "{count} bayat",
357
- "card.staleAndLimit": "{stale} bayat · sınır {limit}",
358
-
359
- "state.on": "açık",
360
- "state.off": "kapalı",
361
- "state.bypassed": "devre dışı",
362
- "state.connected": "bağlı",
363
- "state.disconnected": "bağlantı yok",
364
- "state.notConfigured": "yapılandırılmadı",
365
- "state.idle": "boşta",
366
- "state.yes": "evet",
367
- "state.no": "hayır",
368
- "state.none": "yok",
369
- "state.default": "varsayılan",
370
- "state.unavailable": "okunamıyor",
371
-
372
- "prewarm.inProgress": "tur sürüyor",
373
- "prewarm.lastRound": "son tur: {done}/{total}",
374
- "prewarm.never": "henüz tur yok",
375
-
376
- "redis.section": "Paylaşımlı kademe",
377
- "redis.sub": "{address} · {errors} hata",
378
- "redis.disabled": "cache.redis.enabled kapalı",
379
- "redis.inspect": "Redis'teki anahtarları say",
380
- "redis.dropHtml": "Paylaşımlı HTML anahtarlarını düşür",
381
- "redis.dropData": "Paylaşımlı veri anahtarlarını düşür",
382
- "redis.connection": "Bağlantı",
383
- "redis.address": "Adres",
384
- "redis.database": "Veritabanı",
385
- "redis.keyPrefix": "Anahtar öneki",
386
- "redis.buildId": "Build kimliği",
387
- "redis.sharesHtml": "HTML paylaşımı",
388
- "redis.sharesData": "Veri paylaşımı",
389
- "redis.compressed": "Sıkıştırılmış gövdeler",
390
- "redis.compressed.shared": "paylaşılıyor",
391
- "redis.compressed.local": "yalnızca yerel",
392
- "redis.broadcast": "Purge yayını",
393
- "redis.broadcast.subscribed": "abone",
394
- "redis.broadcast.publish": "yalnızca yayın",
395
- "redis.broadcast.local": "yalnızca yerel",
396
- "redis.timeout": "Komut zaman aşımı",
397
- "redis.errors": "Komut hatası",
398
- "redis.tier": "Kademe",
399
- "redis.tier.inProcess": "yalnızca süreç içi",
400
- "redis.replicas": "Bu önbelleği paylaşan kopya",
401
- "redis.suggest.title": "Paylaşımlı kademe yok.",
402
- "redis.suggest.body":
403
- "Yukarıdaki her girdi yalnızca bu süreçte yaşıyor: her kopya kendi başına " +
404
- "ısınıyor ve bir invalidation yalnızca isteği alan kopyaya ulaşıyor. " +
405
- "Redis eklendiğinde süreç içi önbellek birincil kalır; Redis yalnızca tek " +
406
- "bir sürecin yapamayacağını yapar — başka bir kopyanın render ettiği " +
407
- "HTML'i servis etmek ve purge'ü hepsine duyurmak.",
408
- "redis.suggest.hint":
409
- "İkinci kopyadan itibaren değer üretir. Tek instance'ta kazanç sıfıra " +
410
- "yakın; varsayılan olarak kapalı olmasının sebebi bu.",
411
-
412
- "cf.section": "Cloudflare",
413
- "cf.refresh": "Yenile",
414
- "cf.notConnected": "bağlı değil",
415
- "cf.error": "hata: {error}",
416
- "cf.purgeAll": "Her şeyi düşür",
417
- "cf.purgeCached": "Buradaki URL'leri düşür",
418
- "cf.devMode": "development mode (3 saat cache baypası)",
419
- "cf.kind.prefixes": "prefix",
420
- "cf.kind.hosts": "host",
421
- "cf.kind.tags": "cache tag",
422
- "cf.values.placeholder":
423
- "example.com/haber · boşluk ya da virgülle, istek başına 100 tane",
424
- "cf.purge": "Düşür",
425
- "cf.toggleTiered": "Tiered Cache'i çevir",
426
- "cf.toggleReserve": "Cache Reserve'ü çevir",
427
- "cf.clearReserve": "Cache Reserve'ü boşalt",
428
- "cf.path.placeholder":
429
- "/haber/bir-yazi — bu yolu hangi edge'ler servis etti (boş = tüm zone)",
430
- "cf.queryAnalytics": "Analitiği sorgula",
431
- "cf.edgeHint":
432
- "Cloudflare'de bir URL'in kopyasının şu an hangi edge'lerde durduğunu " +
433
- "veren bir uç yok; seçtiğin bir edge'i origin'den ısıtmanın da yolu yok — " +
434
- "bir obje o koloya yönlenen gerçek bir istekle cache'e giriyor. " +
435
- "Yukarıdaki sayılar gözlem: hangi kolo yolu cache'ten, hangisi origin'den " +
436
- "servis etti. Isıtmanın karşılığı bizim prewarm turumuz ve arkasındaki " +
437
- "Tiered Cache ya da Cache Reserve.",
438
- "cf.zone": "Zone",
439
- "cf.plan": "Plan",
440
- "cf.token": "Token",
441
- "cf.token.env": "ortamdan",
442
- "cf.token.config": "config'ten",
443
- "cf.devModeLabel": "Development mode",
444
- "cf.devMode.on": "açık · {time} kaldı",
445
- "cf.cacheLevel": "Cache level",
446
- "cf.browserTtl": "Tarayıcı cache TTL",
447
- "cf.edgeTtl": "Edge cache TTL",
448
- "cf.sortQuery": "Query string sıralaması",
449
- "cf.alwaysOnline": "Always Online",
450
- "cf.tiered": "Tiered Cache",
451
- "cf.regionalTiered": "Regional Tiered Cache",
452
- "cf.reserve": "Cache Reserve",
453
- "cf.unavailablePlan": "bu planda yok",
454
- "cf.respectOrigin": "origin başlıklarına uy",
455
- "cf.querying": "Cloudflare sorgulanıyor…",
456
- "cf.queryFailed": "sorgu başarısız",
457
- "cf.colo": "Kolo",
458
- "cf.fromCache": "Cache'ten",
459
- "cf.toOrigin": "Origin'e",
460
- "cf.cacheStatus": "Cache durumu",
461
- "cf.requests": "İstek",
462
- "cf.share": "Pay",
463
- "cf.edgeBytes": "Edge baytı",
464
- "cf.edgeNote":
465
- "Son {hours} saatte `{path}` yolunu {colos} kolo servis etti — {hits} " +
466
- "cache'ten, {misses} değil. Hiç istek almamış bir edge, kopyası olsa da " +
467
- "listede görünmez.",
468
- "cf.zoneNote":
469
- "Son {hours} saatte zone genelinde cache durumu. Sayılar Cloudflare'in " +
470
- "örneklemeli veri kümesinden: oranlar güvenilir, mutlak sayılar yaklaşık.",
471
- "cf.suggest.title": "Cloudflare bağlı değil.",
472
- "cf.suggest.body":
473
- "Yukarıdaki önbellek origin önbelleği; ziyaretçinin gerçekten aldığı kopya " +
474
- "edge'de duruyor. Bir zone bağlandığında buradan yapılan purge ikisine de " +
475
- "ulaşır ve origin önbelleğine hiç sorulup sorulmadığını belirleyen cache " +
476
- "isabet oranı görünür.",
477
- "cf.suggest.hint":
478
- "Token izinleri: purge için <code>Zone.Cache Purge</code>, ayarlar için " +
479
- "<code>Zone.Zone Settings</code>, isabet oranı için " +
480
- "<code>Zone.Analytics</code> (salt okunur). Token ortamdan okunuyor ve " +
481
- "sunucudan hiç çıkmıyor.",
482
- "cf.rowPurge": "cf düşür",
483
- "cf.rowPurgeTitle": "Bu URL'i Cloudflare'de düşür",
484
-
485
- "host.section": "Makine",
486
- "host.cpu": "CPU",
487
- "host.memory": "Bellek",
488
- "host.disk": "Disk",
489
- "host.noStats": "Bu platform dosya sistemi bilgisi vermiyor.",
490
- "host.cpuNote": "Bu sürecin {cores} çekirdek üzerindeki payı (son yenilemeden beri).",
491
- "host.ramShared": "Bu süreç: {rss} RSS · {heap} heap.",
492
- "host.ramOnly":
493
- "Önbelleğin tamamı bu süreçte: {rss} RSS · {heap} heap, {html} kadarı HTML.",
494
- "host.diskNote":
495
- "{path} üzerindeki proje dosyaları (node_modules hariç) · birimde {free} boş.",
496
-
497
- "actions.section": "İşlemler",
498
- "actions.target.placeholder": "/haber/:slug · /haber/bir-yazi · invalidation hedefi",
499
- "actions.hard": "hard (bayat işaretlemek yerine sil)",
500
- "actions.invalidate": "Invalidate",
501
- "actions.prefix.placeholder": "veri anahtarı öneki — boş bırakılırsa tüm veri girdileri",
502
- "actions.clearData": "Veri önbelleğini boşalt",
503
- "actions.clearHtml": "HTML önbelleğini boşalt",
504
- "actions.prewarm": "Bütün yolları ısıt",
505
- "actions.hint":
506
- "Invalidation varsayılan olarak girdileri bayat işaretler: arkada tek bir " +
507
- "tazeleme koşarken ziyaretçiler eski HTML'i almaya devam eder. Hard mod " +
508
- "girdiyi siler, yani sıradaki ziyaretçi soğuk render'ı bekler.",
509
-
510
- "tab.data": "Veri",
511
- "entries.section": "Girdiler",
512
- "entries.search": "Anahtara göre filtrele",
513
- "entries.empty": "Girdi yok.",
514
- "entries.shownPartial": "{total} eşleşmenin {shown} tanesi",
515
- "entries.shown": "{total} eşleşme",
516
- "entries.key": "Anahtar",
517
- "entries.state": "Durum",
518
- "entries.size": "Boyut",
519
- "entries.status": "Kod",
520
- "entries.expires": "Kalan",
521
- "entries.deps": "Bağımlılık",
522
- "entries.encodings": "Kodlamalar",
523
- "entries.fresh": "taze",
524
- "entries.stale": "bayat",
525
- "entries.drop": "düşür",
526
- "entries.copyKey": "Anahtarı kopyala",
527
-
528
- "footer.panel": "Yönetim paneli",
529
- "footer.rotates": "Şifre her restart'ta değişir",
530
-
531
- "toast.keyCopied": "Anahtar kopyalandı",
532
- "toast.clipboard": "Pano kullanılamıyor — anahtar önek alanına taşındı",
533
- "toast.needTarget": "Önce bir yol ya da desen gir.",
534
- "toast.needValue": "En az bir değer gir.",
535
- "toast.noCachedPages": "Düşürülecek önbelleklenmiş sayfa yok.",
536
- "toast.failed": "Başarısız",
537
- "toast.done": "Tamam",
538
- "confirm.clearHtml": "Bütün HTML önbelleği boşaltılsın mı? Her sayfa soğur.",
539
- "confirm.clearData": "Bütün veri girdileri silinsin mi? Upstream trafiği sıçrar.",
540
- "confirm.purgeAll": "Bu zone'un Cloudflare önbelleğinin tamamı düşürülsün mü?",
541
- "confirm.purgeUrls": "{count} URL Cloudflare'de düşürülsün mü?",
542
- "confirm.clearReserve":
543
- "Cache Reserve boşaltılsın mı? Cloudflare her şeyi origin'den yeniden çeker.",
544
-
545
- "msg.html.cleared": "HTML önbelleği boşaltıldı ({count} girdi)",
546
- "msg.data.cleared": "Veri önbelleği boşaltıldı ({count} girdi)",
547
- "msg.data.clearedPrefix": "`{prefix}` altındaki {count} veri girdisi düşürüldü",
548
- "msg.target.invalid": "Hedef `/` ile başlamalı",
549
- "msg.html.marked": "`{target}` için {count} girdi bayat işaretlendi",
550
- "msg.html.dropped": "`{target}` için {count} girdi silindi",
551
- "msg.key.missing": "Anahtar eksik",
552
- "msg.entry.dropped": "`{key}` düşürüldü",
553
- "msg.entry.absent": "`{key}` önbellekte değildi",
554
- "msg.redis.unreachable": "Redis'e ulaşılamıyor",
555
- "msg.redis.notConnected": "Redis bağlı değil",
556
- "msg.redis.htmlKeys": "{count} html anahtarı",
557
- "msg.redis.dataKeys": "{count} veri anahtarı",
558
- "msg.redis.dbKeys": "veritabanında {count} anahtar",
559
- "msg.redis.memory": "{value} kullanımda",
560
- "msg.redis.droppedHtml": "{count} paylaşımlı html anahtarı düşürüldü",
561
- "msg.redis.droppedData": "{count} paylaşımlı veri anahtarı düşürüldü",
562
- "msg.cf.purgedEverything": "Cloudflare önbelleği düşürüldü (her şey)",
563
- "msg.cf.noPaths": "Yol verilmedi",
564
- "msg.cf.noHostname": "Tam URL kurulamadı — cache().cloudflare.hostname ayarla",
565
- "msg.cf.purgedUrls": "{batches} istekte {count} URL Cloudflare'de düşürüldü",
566
- "msg.cf.nothingToPurge": "Düşürülecek bir şey yok",
567
- "msg.cf.purgedKeys": "{count} {kind} Cloudflare'de düşürüldü",
568
- "msg.cf.settingChanged": "Cloudflare {id} artık {value}",
569
- "msg.cf.featureChanged": "Cloudflare {feature} {value} yapıldı",
570
- "msg.cf.reserveClearing":
571
- "Cache Reserve boşaltması başladı — Cloudflare'de arkada koşuyor",
572
- "msg.cf.failed": "Cloudflare: {error}",
573
- "msg.prewarm.busy": "Zaten bir ısıtma turu koşuyor",
574
- "msg.prewarm.paths": "{count} yol ısıtılıyor",
575
- "msg.prewarm.all": "Bütün yollar ısıtılıyor",
576
- "msg.action.unknown": "Bilinmeyen işlem: {type}",
577
-
578
- "login.title": "Yönetim paneli",
579
- "login.lead": "Bu turun şifresi sunucu logundaki <code>ADMIN</code> kutusunda.",
580
- "login.placeholder": "32 haneli şifre",
581
- "login.submit": "Aç",
582
- "login.hint": "Üç başarısız deneme bu adresi 24 saat engeller.",
583
- "login.tooMany": "Çok fazla deneme.",
584
- "login.invalid": "Şifre geçersiz.",
585
-
586
- "upstream.section": "Upstream",
587
- "upstream.host": "Host",
588
- "upstream.rate": "Hız",
589
- "upstream.state": "Durum",
590
- "upstream.empty": "İzlenen upstream host yok.",
591
-
592
- "routes.section": "Kayıtlı rotalar",
593
- "routes.method": "Method",
594
- "routes.path": "Yol",
595
- "routes.lastStatus": "Son status",
596
- "routes.lastMs": "Son ms",
597
- "routes.hits": "İsabet",
598
- "routes.empty": "Rota yok.",
599
- "routes.modules": "Route modülleri",
600
- "routes.file": "Dosya",
601
- "routes.openLogs": "Loglar",
602
-
603
- "views.section": "Şablonlar",
604
- "views.path": "Yol",
605
- "views.kind": "Tür",
606
- "views.search": "Şablon ara…",
607
- "views.empty": "Şablon yok.",
608
-
609
- "logs.section": "Canlı loglar",
610
- "logs.search": "url / mesaj ara…",
611
- "logs.kind.all": "tüm türler",
612
- "logs.method.all": "tüm methodlar",
613
- "logs.status.all": "tüm status",
614
- "logs.cache.all": "tüm cache",
615
- "logs.path": "yol / rota…",
616
- "logs.pause": "duraklat",
617
- "logs.clear": "Görünümü temizle",
618
- "logs.empty": "Log satırı yok.",
619
- "logs.shown": "{shown} görünüyor · {total} tamponda",
620
-
621
- "unit.second": "sn",
622
- "unit.minute": "dk",
623
- "unit.hour": "sa",
624
- "unit.day": "g",
625
- "unit.ms": "ms",
626
- },
627
- };
628
-
629
- export const LANGUAGES = /** @type {const} */ (["en", "tr"]);
630
-
631
- /** @type {Record<string, string>} */
632
- export const LANGUAGE_NAMES = { en: "English", tr: "Türkçe" };
633
-
634
- /**
635
- * Kayıtlı tercih yoksa tarayıcının diline bakılır: paneli Türkçe bir makinede
636
- * açan biri için varsayılanın İngilizce olmasının bir gerekçesi yok.
637
- *
638
- * @returns {string}
639
- */
640
- function detect() {
641
- // Tamamı korumalı: panel `file://` üzerinden ya da sıkı bir gizlilik
642
- // kipinde açılmış olabilir, ve bu modül testlerde tarayıcı dışında da
643
- // import ediliyor.
644
- // `document` kontrolü tarayıcı dışında `localStorage`'a hiç dokunmamak için:
645
- // Node'da bu global'i okumak uyarı basıyor.
646
- try {
647
- if (typeof document !== "undefined") {
648
- const stored = localStorage.getItem(STORAGE_KEY);
649
- if (stored && MESSAGES[stored]) return stored;
650
- }
651
- } catch {
652
- // Depolama yoksa tercih bu sekmeye özel kalır.
653
- }
654
-
655
- try {
656
- const preferred = navigator.languages ?? [navigator.language];
657
- for (const tag of preferred) {
658
- const base = String(tag ?? "").toLowerCase().split("-")[0];
659
- if (MESSAGES[base]) return base;
660
- }
661
- } catch {
662
- // Tarayıcı dışında `navigator` yok.
663
- }
664
-
665
- return "en";
666
- }
667
-
668
- let current = detect();
669
-
670
- /** @returns {string} */
671
- export function getLanguage() {
672
- return current;
673
- }
674
-
675
- /**
676
- * @param {string} language
677
- */
678
- export function setLanguage(language) {
679
- if (!MESSAGES[language]) return;
680
- current = language;
681
-
682
- try {
683
- localStorage.setItem(STORAGE_KEY, language);
684
- } catch {
685
- // Yazamıyorsak seçim yalnızca bu sekme için geçerli olur.
686
- }
687
-
688
- document.documentElement.lang = language;
689
- }
690
-
691
- /**
692
- * Çeviri. Eksik anahtar **İngilizceye düşer**, o da yoksa anahtarın kendisi
693
- * döner: yarım çevrilmiş bir sözlük paneli boş etiketlerle bırakmasın.
694
- *
695
- * @param {string} key
696
- * @param {Record<string, string | number>} [params]
697
- * @returns {string}
698
- */
699
- export function t(key, params) {
700
- const template = MESSAGES[current]?.[key] ?? MESSAGES.en[key] ?? key;
701
- if (!params) return template;
702
-
703
- return template.replace(/\{(\w+)\}/g, (match, name) =>
704
- name in params ? String(params[name]) : match,
705
- );
706
- }
707
-
708
- /**
709
- * `data-i18n` taşıyan her düğümü çevirir.
710
- *
711
- * Üç öznitelik var: `data-i18n` metni, `data-i18n-ph` placeholder'ı,
712
- * `data-i18n-html` ise içinde `<code>` geçen açıklama metinlerini yazar —
713
- * kaynağı sözlüğün kendisi olduğu için sabit metin, kullanıcı verisi değil.
714
- *
715
- * @param {ParentNode} [root]
716
- */
717
- export function applyTranslations(root = document) {
718
- document.documentElement.lang = current;
719
-
720
- for (const node of root.querySelectorAll("[data-i18n]")) {
721
- node.textContent = t(node.getAttribute("data-i18n") ?? "");
722
- }
723
-
724
- for (const node of root.querySelectorAll("[data-i18n-html]")) {
725
- node.innerHTML = t(node.getAttribute("data-i18n-html") ?? "");
726
- }
727
-
728
- for (const node of root.querySelectorAll("[data-i18n-ph]")) {
729
- node.setAttribute("placeholder", t(node.getAttribute("data-i18n-ph") ?? ""));
730
- }
731
-
732
- for (const node of root.querySelectorAll("[data-i18n-title]")) {
733
- node.setAttribute("title", t(node.getAttribute("data-i18n-title") ?? ""));
734
- }
735
- }
736
-
737
- /**
738
- * Header'daki dil seçici. Çevirisi olan diller sözlükten geliyor, yani üçüncü
739
- * bir dil eklemek yalnızca `MESSAGES`'a bir giriş demek.
740
- *
741
- * @param {(language: string) => void} onChange
742
- * @returns {HTMLSelectElement}
743
- */
744
- export function languageSelect(onChange) {
745
- const select = document.createElement("select");
746
- select.className = "lang";
747
- select.setAttribute("aria-label", t("shell.language"));
748
-
749
- for (const language of LANGUAGES) {
750
- const option = document.createElement("option");
751
- option.value = language;
752
- option.textContent = LANGUAGE_NAMES[language];
753
- option.selected = language === current;
754
- select.append(option);
755
- }
756
-
757
- select.addEventListener("change", () => {
758
- setLanguage(select.value);
759
- select.setAttribute("aria-label", t("shell.language"));
760
- onChange(select.value);
761
- });
762
-
763
- return select;
764
- }
1
+ /**
2
+ * Admin panelinin sözlüğü.
3
+ *
4
+ * Panel build hattından geçmiyor, bu yüzden hazır bir i18n kütüphanesi yok:
5
+ * düz bir nesne, `{ad}` yer tutucusu ve `data-i18n` gezintisi kadarı yetiyor.
6
+ *
7
+ * Sunucudan gelen mesajlar **çevrilmez, kodlanır**: `/action` cevabı
8
+ * `{ ok, code, params }` döner ve metin burada kurulur. Böylece sunucu tarafı
9
+ * arayüz dilini hiç bilmiyor — panel iki dilde konuşurken framework'ün log'u
10
+ * ve API'si tek dilde kalıyor.
11
+ *
12
+ * Dil seçimi `localStorage`'da: panel oturumu şifreyle açılıyor ve tercihin
13
+ * sunucuya taşınmasının bir karşılığı yok.
14
+ */
15
+
16
+ const STORAGE_KEY = "jskelet.admin.lang";
17
+
18
+ /**
19
+ * Sözlükler. Dışa açık, çünkü testler iki dilin anahtar kümesinin birebir
20
+ * eşleştiğini ve şablonlarda geçen her anahtarın karşılığı olduğunu doğruluyor.
21
+ *
22
+ * @type {Record<string, Record<string, string>>}
23
+ */
24
+ export const MESSAGES = {
25
+ en: {
26
+ /* --- kabuk --- */
27
+ "nav.overview": "Overview",
28
+ "nav.cache": "Cache",
29
+ "nav.routes": "Routes",
30
+ "nav.views": "Views",
31
+ "nav.logs": "Logs",
32
+ "nav.system": "System",
33
+ "shell.live": "live",
34
+ "shell.refresh": "Refresh",
35
+ "shell.signOut": "Sign out",
36
+ "shell.language": "Language",
37
+ "chip.up": "up {value}",
38
+ "chip.sse.live": "SSE live",
39
+ "chip.sse.off": "SSE off",
40
+ "chip.sse.error": "SSE error",
41
+
42
+ /* --- kartlar --- */
43
+ "card.htmlEntries": "HTML entries",
44
+ "card.htmlMemory": "HTML in memory",
45
+ "card.dataEntries": "Data entries",
46
+ "card.sharedTier": "Shared tier",
47
+ "card.prewarm": "Prewarm",
48
+ "card.limit": "limit {count}",
49
+ "card.stale": "{count} stale",
50
+ "card.staleAndLimit": "{stale} stale · limit {limit}",
51
+
52
+ /* --- durumlar --- */
53
+ "state.on": "on",
54
+ "state.off": "off",
55
+ "state.bypassed": "bypassed",
56
+ "state.connected": "connected",
57
+ "state.disconnected": "disconnected",
58
+ "state.notConfigured": "not configured",
59
+ "state.idle": "idle",
60
+ "state.yes": "yes",
61
+ "state.no": "no",
62
+ "state.none": "none",
63
+ "state.default": "default",
64
+ "state.unavailable": "unavailable",
65
+
66
+ /* --- ısıtma --- */
67
+ "prewarm.inProgress": "round in progress",
68
+ "prewarm.lastRound": "last round: {done}/{total}",
69
+ "prewarm.never": "no round yet",
70
+
71
+ /* --- paylaşımlı kademe --- */
72
+ "redis.section": "Shared tier",
73
+ "redis.sub": "{address} · {errors} errors",
74
+ "redis.disabled": "cache.redis.enabled is false",
75
+ "redis.inspect": "Count keys in Redis",
76
+ "redis.dropHtml": "Drop shared HTML keys",
77
+ "redis.dropData": "Drop shared data keys",
78
+ "redis.connection": "Connection",
79
+ "redis.address": "Address",
80
+ "redis.database": "Database",
81
+ "redis.keyPrefix": "Key prefix",
82
+ "redis.buildId": "Build id",
83
+ "redis.sharesHtml": "Shares HTML",
84
+ "redis.sharesData": "Shares data",
85
+ "redis.compressed": "Compressed bodies",
86
+ "redis.compressed.shared": "shared",
87
+ "redis.compressed.local": "local only",
88
+ "redis.broadcast": "Purge broadcast",
89
+ "redis.broadcast.subscribed": "subscribed",
90
+ "redis.broadcast.publish": "publish only",
91
+ "redis.broadcast.local": "local only",
92
+ "redis.timeout": "Command timeout",
93
+ "redis.errors": "Command errors",
94
+ "redis.tier": "Tier",
95
+ "redis.tier.inProcess": "in-process only",
96
+ "redis.replicas": "Replicas sharing this cache",
97
+ "redis.suggest.title": "No shared tier.",
98
+ "redis.suggest.body":
99
+ "Every entry above lives in this process only, so each replica warms up " +
100
+ "on its own and an invalidation reaches whichever replica received the " +
101
+ "request. Adding Redis keeps the in-process cache primary and only does " +
102
+ "what a single process cannot: serve HTML another replica already " +
103
+ "rendered, and broadcast purges to all of them.",
104
+ "redis.suggest.hint":
105
+ "Worth it from the second replica onwards. On a single instance the gain " +
106
+ "is close to zero, which is why it is off by default.",
107
+
108
+ /* --- cloudflare --- */
109
+ "cf.section": "Cloudflare",
110
+ "cf.refresh": "Refresh",
111
+ "cf.notConnected": "not connected",
112
+ "cf.error": "error: {error}",
113
+ "cf.purgeAll": "Purge everything",
114
+ "cf.purgeCached": "Purge URLs cached here",
115
+ "cf.devMode": "development mode (bypass cache for 3h)",
116
+ "cf.kind.prefixes": "prefixes",
117
+ "cf.kind.hosts": "hosts",
118
+ "cf.kind.tags": "cache tags",
119
+ "cf.values.placeholder":
120
+ "example.com/news · space or comma separated, 100 per request",
121
+ "cf.purge": "Purge",
122
+ "cf.toggleTiered": "Toggle Tiered Cache",
123
+ "cf.toggleReserve": "Toggle Cache Reserve",
124
+ "cf.clearReserve": "Clear Cache Reserve",
125
+ "cf.path.placeholder":
126
+ "/news/some-post — which edges served this path (blank = whole zone)",
127
+ "cf.queryAnalytics": "Query analytics",
128
+ "cf.edgeHint":
129
+ "Cloudflare has no endpoint that lists which edges currently hold a copy " +
130
+ "of a URL, and no way to warm a chosen edge from the origin — an object " +
131
+ "only enters an edge cache through a real request routed there. What you " +
132
+ "get above is observation: which colos served the path from cache and " +
133
+ "which went to the origin. Warming is our prewarm pass plus Tiered Cache " +
134
+ "or Cache Reserve behind it.",
135
+ "cf.zone": "Zone",
136
+ "cf.plan": "Plan",
137
+ "cf.token": "Token",
138
+ "cf.token.env": "from environment",
139
+ "cf.token.config": "from config",
140
+ "cf.devModeLabel": "Development mode",
141
+ "cf.devMode.on": "on · {time} left",
142
+ "cf.cacheLevel": "Cache level",
143
+ "cf.browserTtl": "Browser cache TTL",
144
+ "cf.edgeTtl": "Edge cache TTL",
145
+ "cf.sortQuery": "Query string sorting",
146
+ "cf.alwaysOnline": "Always Online",
147
+ "cf.tiered": "Tiered Cache",
148
+ "cf.regionalTiered": "Regional Tiered Cache",
149
+ "cf.reserve": "Cache Reserve",
150
+ "cf.unavailablePlan": "unavailable on this plan",
151
+ "cf.respectOrigin": "respect origin headers",
152
+ "cf.querying": "Querying Cloudflare…",
153
+ "cf.queryFailed": "query failed",
154
+ "cf.colo": "Colo",
155
+ "cf.fromCache": "From cache",
156
+ "cf.toOrigin": "To origin",
157
+ "cf.cacheStatus": "Cache status",
158
+ "cf.requests": "Requests",
159
+ "cf.share": "Share",
160
+ "cf.edgeBytes": "Edge bytes",
161
+ "cf.edgeNote":
162
+ "{colos} colos served `{path}` in the last {hours}h — {hits} from cache, " +
163
+ "{misses} not. Edges that received no request do not appear, even if they " +
164
+ "hold a copy.",
165
+ "cf.zoneNote":
166
+ "Zone-wide cache status over the last {hours}h. Numbers come from " +
167
+ "Cloudflare's sampled dataset: ratios are reliable, absolute counts are " +
168
+ "estimates.",
169
+ "cf.suggest.title": "Cloudflare is not connected.",
170
+ "cf.suggest.body":
171
+ "The cache above is the origin cache; the copy your visitors actually get " +
172
+ "is at the edge. Connecting a zone lets a purge here reach both, and " +
173
+ "shows the cache hit ratio that decides whether the origin cache is even " +
174
+ "being asked.",
175
+ "cf.suggest.hint":
176
+ "Token permissions: <code>Zone.Cache Purge</code> to purge, " +
177
+ "<code>Zone.Zone Settings</code> to change settings, " +
178
+ "<code>Zone.Analytics</code> (read) for the hit ratio. The token is read " +
179
+ "from the environment and never leaves the server.",
180
+ "cf.rowPurge": "cf purge",
181
+ "cf.rowPurgeTitle": "Purge this URL at Cloudflare",
182
+
183
+ /* --- host --- */
184
+ "host.section": "Host",
185
+ "host.cpu": "CPU",
186
+ "host.memory": "Memory",
187
+ "host.disk": "Disk",
188
+ "host.noStats": "This platform does not report filesystem stats.",
189
+ "host.cpuNote": "This process across {cores} cores (since last refresh).",
190
+ "host.ramShared": "This process: {rss} RSS · {heap} heap.",
191
+ "host.ramOnly":
192
+ "This process holds the whole cache: {rss} RSS · {heap} heap, {html} of it HTML.",
193
+ "host.diskNote":
194
+ "Project files on {path} (excluding node_modules) · {free} free on the volume.",
195
+
196
+ /* --- işlemler --- */
197
+ "actions.section": "Actions",
198
+ "actions.target.placeholder": "/news/:slug · /news/some-post · invalidation target",
199
+ "actions.hard": "hard (delete instead of marking stale)",
200
+ "actions.invalidate": "Invalidate",
201
+ "actions.prefix.placeholder": "data key prefix — empty clears every data entry",
202
+ "actions.clearData": "Clear data cache",
203
+ "actions.clearHtml": "Clear HTML cache",
204
+ "actions.prewarm": "Prewarm all paths",
205
+ "actions.hint":
206
+ "Invalidation marks entries stale by default: visitors keep getting the " +
207
+ "old HTML while a single background refresh runs. Hard mode deletes them, " +
208
+ "so the next visitor waits for a cold render.",
209
+
210
+ /* --- girdiler --- */
211
+ "tab.data": "Data",
212
+ "entries.section": "Entries",
213
+ "entries.search": "Filter by key",
214
+ "entries.empty": "No entries.",
215
+ "entries.shownPartial": "{shown} of {total} shown",
216
+ "entries.shown": "{total} shown",
217
+ "entries.key": "Key",
218
+ "entries.state": "State",
219
+ "entries.size": "Size",
220
+ "entries.status": "Status",
221
+ "entries.expires": "Expires",
222
+ "entries.deps": "Deps",
223
+ "entries.encodings": "Encodings",
224
+ "entries.fresh": "fresh",
225
+ "entries.stale": "stale",
226
+ "entries.drop": "drop",
227
+ "entries.copyKey": "Copy key",
228
+
229
+ /* --- alt bilgi --- */
230
+ "footer.panel": "Admin panel",
231
+ "footer.rotates": "Password rotates on restart",
232
+
233
+ /* --- bildirimler --- */
234
+ "toast.keyCopied": "Key copied",
235
+ "toast.clipboard": "Clipboard is unavailable — key moved to the prefix field",
236
+ "toast.needTarget": "Enter a path or pattern first.",
237
+ "toast.needValue": "Enter at least one value.",
238
+ "toast.noCachedPages": "No cached pages to purge.",
239
+ "toast.failed": "Failed",
240
+ "toast.done": "Done",
241
+ "confirm.clearHtml": "Clear the entire HTML cache? Every page goes cold.",
242
+ "confirm.clearData": "Clear every data entry? Upstream traffic will spike.",
243
+ "confirm.purgeAll": "Purge the entire Cloudflare cache for this zone?",
244
+ "confirm.purgeUrls": "Purge {count} URLs at Cloudflare?",
245
+ "confirm.clearReserve":
246
+ "Clear Cache Reserve? Cloudflare will refetch everything from the origin.",
247
+
248
+ /* --- sunucu cevapları --- */
249
+ "msg.html.cleared": "HTML cache cleared ({count} entries)",
250
+ "msg.data.cleared": "Data cache cleared ({count} entries)",
251
+ "msg.data.clearedPrefix": "{count} data entries dropped under `{prefix}`",
252
+ "msg.target.invalid": "Target must start with `/`",
253
+ "msg.html.marked": "{count} entries marked stale for `{target}`",
254
+ "msg.html.dropped": "{count} entries dropped for `{target}`",
255
+ "msg.key.missing": "Missing key",
256
+ "msg.entry.dropped": "Dropped `{key}`",
257
+ "msg.entry.absent": "`{key}` was not cached",
258
+ "msg.redis.unreachable": "Redis is not reachable",
259
+ "msg.redis.notConnected": "Redis is not connected",
260
+ "msg.redis.htmlKeys": "{count} html keys",
261
+ "msg.redis.dataKeys": "{count} data keys",
262
+ "msg.redis.dbKeys": "{count} keys in db",
263
+ "msg.redis.memory": "{value} used",
264
+ "msg.redis.droppedHtml": "{count} shared html keys dropped",
265
+ "msg.redis.droppedData": "{count} shared data keys dropped",
266
+ "msg.cf.purgedEverything": "Cloudflare cache purged (everything)",
267
+ "msg.cf.noPaths": "No paths given",
268
+ "msg.cf.noHostname": "Could not build absolute URLs — set cache().cloudflare.hostname",
269
+ "msg.cf.purgedUrls": "Purged {count} URLs at Cloudflare in {batches} request(s)",
270
+ "msg.cf.nothingToPurge": "Nothing to purge",
271
+ "msg.cf.purgedKeys": "Purged {count} {kind} at Cloudflare",
272
+ "msg.cf.settingChanged": "Cloudflare {id} is now {value}",
273
+ "msg.cf.featureChanged": "Cloudflare {feature} turned {value}",
274
+ "msg.cf.reserveClearing":
275
+ "Cache Reserve clear started — it runs asynchronously at Cloudflare",
276
+ "msg.cf.failed": "Cloudflare: {error}",
277
+ "msg.prewarm.busy": "A prewarm round is already running",
278
+ "msg.prewarm.paths": "Prewarming {count} paths",
279
+ "msg.prewarm.all": "Prewarming all paths",
280
+ "msg.action.unknown": "Unknown action: {type}",
281
+
282
+ /* --- giriş --- */
283
+ "login.title": "Admin panel",
284
+ "login.lead": "This run's password is printed in the server log (<code>ADMIN</code> box).",
285
+ "login.placeholder": "32-character password",
286
+ "login.submit": "Unlock",
287
+ "login.hint": "Three failed attempts block this address for 24 hours.",
288
+ "login.tooMany": "Too many attempts.",
289
+ "login.invalid": "Invalid password.",
290
+
291
+ "upstream.section": "Upstream",
292
+ "upstream.host": "Host",
293
+ "upstream.rate": "Rate",
294
+ "upstream.state": "State",
295
+ "upstream.empty": "No upstream hosts tracked.",
296
+
297
+ "routes.section": "Registered routes",
298
+ "routes.method": "Method",
299
+ "routes.path": "Path",
300
+ "routes.lastStatus": "Last status",
301
+ "routes.lastMs": "Last ms",
302
+ "routes.hits": "Hits",
303
+ "routes.empty": "No routes.",
304
+ "routes.modules": "Route modules",
305
+ "routes.file": "File",
306
+ "routes.openLogs": "Logs",
307
+
308
+ "views.section": "Views",
309
+ "views.path": "Path",
310
+ "views.kind": "Kind",
311
+ "views.search": "Filter views…",
312
+ "views.empty": "No views.",
313
+
314
+ "logs.section": "Live logs",
315
+ "logs.search": "Search url / message…",
316
+ "logs.kind.all": "all kinds",
317
+ "logs.method.all": "all methods",
318
+ "logs.status.all": "all status",
319
+ "logs.cache.all": "all cache",
320
+ "logs.path": "path / route…",
321
+ "logs.pause": "pause",
322
+ "logs.clear": "Clear view",
323
+ "logs.empty": "No log lines.",
324
+ "logs.shown": "{shown} shown · {total} buffered",
325
+
326
+ /* --- birimler --- */
327
+ "unit.second": "s",
328
+ "unit.minute": "m",
329
+ "unit.hour": "h",
330
+ "unit.day": "d",
331
+ "unit.ms": "ms",
332
+ },
333
+
334
+ tr: {
335
+ "nav.overview": "Özet",
336
+ "nav.cache": "Önbellek",
337
+ "nav.routes": "Rotalar",
338
+ "nav.views": "Şablonlar",
339
+ "nav.logs": "Loglar",
340
+ "nav.system": "Sistem",
341
+ "shell.live": "canlı",
342
+ "shell.refresh": "Yenile",
343
+ "shell.signOut": "Çıkış",
344
+ "shell.language": "Dil",
345
+ "chip.up": "{value} açık",
346
+ "chip.sse.live": "SSE canlı",
347
+ "chip.sse.off": "SSE kapalı",
348
+ "chip.sse.error": "SSE hata",
349
+
350
+ "card.htmlEntries": "HTML girdisi",
351
+ "card.htmlMemory": "Bellekteki HTML",
352
+ "card.dataEntries": "Veri girdisi",
353
+ "card.sharedTier": "Paylaşımlı kademe",
354
+ "card.prewarm": "Isıtma",
355
+ "card.limit": "sınır {count}",
356
+ "card.stale": "{count} bayat",
357
+ "card.staleAndLimit": "{stale} bayat · sınır {limit}",
358
+
359
+ "state.on": "açık",
360
+ "state.off": "kapalı",
361
+ "state.bypassed": "devre dışı",
362
+ "state.connected": "bağlı",
363
+ "state.disconnected": "bağlantı yok",
364
+ "state.notConfigured": "yapılandırılmadı",
365
+ "state.idle": "boşta",
366
+ "state.yes": "evet",
367
+ "state.no": "hayır",
368
+ "state.none": "yok",
369
+ "state.default": "varsayılan",
370
+ "state.unavailable": "okunamıyor",
371
+
372
+ "prewarm.inProgress": "tur sürüyor",
373
+ "prewarm.lastRound": "son tur: {done}/{total}",
374
+ "prewarm.never": "henüz tur yok",
375
+
376
+ "redis.section": "Paylaşımlı kademe",
377
+ "redis.sub": "{address} · {errors} hata",
378
+ "redis.disabled": "cache.redis.enabled kapalı",
379
+ "redis.inspect": "Redis'teki anahtarları say",
380
+ "redis.dropHtml": "Paylaşımlı HTML anahtarlarını düşür",
381
+ "redis.dropData": "Paylaşımlı veri anahtarlarını düşür",
382
+ "redis.connection": "Bağlantı",
383
+ "redis.address": "Adres",
384
+ "redis.database": "Veritabanı",
385
+ "redis.keyPrefix": "Anahtar öneki",
386
+ "redis.buildId": "Build kimliği",
387
+ "redis.sharesHtml": "HTML paylaşımı",
388
+ "redis.sharesData": "Veri paylaşımı",
389
+ "redis.compressed": "Sıkıştırılmış gövdeler",
390
+ "redis.compressed.shared": "paylaşılıyor",
391
+ "redis.compressed.local": "yalnızca yerel",
392
+ "redis.broadcast": "Purge yayını",
393
+ "redis.broadcast.subscribed": "abone",
394
+ "redis.broadcast.publish": "yalnızca yayın",
395
+ "redis.broadcast.local": "yalnızca yerel",
396
+ "redis.timeout": "Komut zaman aşımı",
397
+ "redis.errors": "Komut hatası",
398
+ "redis.tier": "Kademe",
399
+ "redis.tier.inProcess": "yalnızca süreç içi",
400
+ "redis.replicas": "Bu önbelleği paylaşan kopya",
401
+ "redis.suggest.title": "Paylaşımlı kademe yok.",
402
+ "redis.suggest.body":
403
+ "Yukarıdaki her girdi yalnızca bu süreçte yaşıyor: her kopya kendi başına " +
404
+ "ısınıyor ve bir invalidation yalnızca isteği alan kopyaya ulaşıyor. " +
405
+ "Redis eklendiğinde süreç içi önbellek birincil kalır; Redis yalnızca tek " +
406
+ "bir sürecin yapamayacağını yapar — başka bir kopyanın render ettiği " +
407
+ "HTML'i servis etmek ve purge'ü hepsine duyurmak.",
408
+ "redis.suggest.hint":
409
+ "İkinci kopyadan itibaren değer üretir. Tek instance'ta kazanç sıfıra " +
410
+ "yakın; varsayılan olarak kapalı olmasının sebebi bu.",
411
+
412
+ "cf.section": "Cloudflare",
413
+ "cf.refresh": "Yenile",
414
+ "cf.notConnected": "bağlı değil",
415
+ "cf.error": "hata: {error}",
416
+ "cf.purgeAll": "Her şeyi düşür",
417
+ "cf.purgeCached": "Buradaki URL'leri düşür",
418
+ "cf.devMode": "development mode (3 saat cache baypası)",
419
+ "cf.kind.prefixes": "prefix",
420
+ "cf.kind.hosts": "host",
421
+ "cf.kind.tags": "cache tag",
422
+ "cf.values.placeholder":
423
+ "example.com/haber · boşluk ya da virgülle, istek başına 100 tane",
424
+ "cf.purge": "Düşür",
425
+ "cf.toggleTiered": "Tiered Cache'i çevir",
426
+ "cf.toggleReserve": "Cache Reserve'ü çevir",
427
+ "cf.clearReserve": "Cache Reserve'ü boşalt",
428
+ "cf.path.placeholder":
429
+ "/haber/bir-yazi — bu yolu hangi edge'ler servis etti (boş = tüm zone)",
430
+ "cf.queryAnalytics": "Analitiği sorgula",
431
+ "cf.edgeHint":
432
+ "Cloudflare'de bir URL'in kopyasının şu an hangi edge'lerde durduğunu " +
433
+ "veren bir uç yok; seçtiğin bir edge'i origin'den ısıtmanın da yolu yok — " +
434
+ "bir obje o koloya yönlenen gerçek bir istekle cache'e giriyor. " +
435
+ "Yukarıdaki sayılar gözlem: hangi kolo yolu cache'ten, hangisi origin'den " +
436
+ "servis etti. Isıtmanın karşılığı bizim prewarm turumuz ve arkasındaki " +
437
+ "Tiered Cache ya da Cache Reserve.",
438
+ "cf.zone": "Zone",
439
+ "cf.plan": "Plan",
440
+ "cf.token": "Token",
441
+ "cf.token.env": "ortamdan",
442
+ "cf.token.config": "config'ten",
443
+ "cf.devModeLabel": "Development mode",
444
+ "cf.devMode.on": "açık · {time} kaldı",
445
+ "cf.cacheLevel": "Cache level",
446
+ "cf.browserTtl": "Tarayıcı cache TTL",
447
+ "cf.edgeTtl": "Edge cache TTL",
448
+ "cf.sortQuery": "Query string sıralaması",
449
+ "cf.alwaysOnline": "Always Online",
450
+ "cf.tiered": "Tiered Cache",
451
+ "cf.regionalTiered": "Regional Tiered Cache",
452
+ "cf.reserve": "Cache Reserve",
453
+ "cf.unavailablePlan": "bu planda yok",
454
+ "cf.respectOrigin": "origin başlıklarına uy",
455
+ "cf.querying": "Cloudflare sorgulanıyor…",
456
+ "cf.queryFailed": "sorgu başarısız",
457
+ "cf.colo": "Kolo",
458
+ "cf.fromCache": "Cache'ten",
459
+ "cf.toOrigin": "Origin'e",
460
+ "cf.cacheStatus": "Cache durumu",
461
+ "cf.requests": "İstek",
462
+ "cf.share": "Pay",
463
+ "cf.edgeBytes": "Edge baytı",
464
+ "cf.edgeNote":
465
+ "Son {hours} saatte `{path}` yolunu {colos} kolo servis etti — {hits} " +
466
+ "cache'ten, {misses} değil. Hiç istek almamış bir edge, kopyası olsa da " +
467
+ "listede görünmez.",
468
+ "cf.zoneNote":
469
+ "Son {hours} saatte zone genelinde cache durumu. Sayılar Cloudflare'in " +
470
+ "örneklemeli veri kümesinden: oranlar güvenilir, mutlak sayılar yaklaşık.",
471
+ "cf.suggest.title": "Cloudflare bağlı değil.",
472
+ "cf.suggest.body":
473
+ "Yukarıdaki önbellek origin önbelleği; ziyaretçinin gerçekten aldığı kopya " +
474
+ "edge'de duruyor. Bir zone bağlandığında buradan yapılan purge ikisine de " +
475
+ "ulaşır ve origin önbelleğine hiç sorulup sorulmadığını belirleyen cache " +
476
+ "isabet oranı görünür.",
477
+ "cf.suggest.hint":
478
+ "Token izinleri: purge için <code>Zone.Cache Purge</code>, ayarlar için " +
479
+ "<code>Zone.Zone Settings</code>, isabet oranı için " +
480
+ "<code>Zone.Analytics</code> (salt okunur). Token ortamdan okunuyor ve " +
481
+ "sunucudan hiç çıkmıyor.",
482
+ "cf.rowPurge": "cf düşür",
483
+ "cf.rowPurgeTitle": "Bu URL'i Cloudflare'de düşür",
484
+
485
+ "host.section": "Makine",
486
+ "host.cpu": "CPU",
487
+ "host.memory": "Bellek",
488
+ "host.disk": "Disk",
489
+ "host.noStats": "Bu platform dosya sistemi bilgisi vermiyor.",
490
+ "host.cpuNote": "Bu sürecin {cores} çekirdek üzerindeki payı (son yenilemeden beri).",
491
+ "host.ramShared": "Bu süreç: {rss} RSS · {heap} heap.",
492
+ "host.ramOnly":
493
+ "Önbelleğin tamamı bu süreçte: {rss} RSS · {heap} heap, {html} kadarı HTML.",
494
+ "host.diskNote":
495
+ "{path} üzerindeki proje dosyaları (node_modules hariç) · birimde {free} boş.",
496
+
497
+ "actions.section": "İşlemler",
498
+ "actions.target.placeholder": "/haber/:slug · /haber/bir-yazi · invalidation hedefi",
499
+ "actions.hard": "hard (bayat işaretlemek yerine sil)",
500
+ "actions.invalidate": "Invalidate",
501
+ "actions.prefix.placeholder": "veri anahtarı öneki — boş bırakılırsa tüm veri girdileri",
502
+ "actions.clearData": "Veri önbelleğini boşalt",
503
+ "actions.clearHtml": "HTML önbelleğini boşalt",
504
+ "actions.prewarm": "Bütün yolları ısıt",
505
+ "actions.hint":
506
+ "Invalidation varsayılan olarak girdileri bayat işaretler: arkada tek bir " +
507
+ "tazeleme koşarken ziyaretçiler eski HTML'i almaya devam eder. Hard mod " +
508
+ "girdiyi siler, yani sıradaki ziyaretçi soğuk render'ı bekler.",
509
+
510
+ "tab.data": "Veri",
511
+ "entries.section": "Girdiler",
512
+ "entries.search": "Anahtara göre filtrele",
513
+ "entries.empty": "Girdi yok.",
514
+ "entries.shownPartial": "{total} eşleşmenin {shown} tanesi",
515
+ "entries.shown": "{total} eşleşme",
516
+ "entries.key": "Anahtar",
517
+ "entries.state": "Durum",
518
+ "entries.size": "Boyut",
519
+ "entries.status": "Kod",
520
+ "entries.expires": "Kalan",
521
+ "entries.deps": "Bağımlılık",
522
+ "entries.encodings": "Kodlamalar",
523
+ "entries.fresh": "taze",
524
+ "entries.stale": "bayat",
525
+ "entries.drop": "düşür",
526
+ "entries.copyKey": "Anahtarı kopyala",
527
+
528
+ "footer.panel": "Yönetim paneli",
529
+ "footer.rotates": "Şifre her restart'ta değişir",
530
+
531
+ "toast.keyCopied": "Anahtar kopyalandı",
532
+ "toast.clipboard": "Pano kullanılamıyor — anahtar önek alanına taşındı",
533
+ "toast.needTarget": "Önce bir yol ya da desen gir.",
534
+ "toast.needValue": "En az bir değer gir.",
535
+ "toast.noCachedPages": "Düşürülecek önbelleklenmiş sayfa yok.",
536
+ "toast.failed": "Başarısız",
537
+ "toast.done": "Tamam",
538
+ "confirm.clearHtml": "Bütün HTML önbelleği boşaltılsın mı? Her sayfa soğur.",
539
+ "confirm.clearData": "Bütün veri girdileri silinsin mi? Upstream trafiği sıçrar.",
540
+ "confirm.purgeAll": "Bu zone'un Cloudflare önbelleğinin tamamı düşürülsün mü?",
541
+ "confirm.purgeUrls": "{count} URL Cloudflare'de düşürülsün mü?",
542
+ "confirm.clearReserve":
543
+ "Cache Reserve boşaltılsın mı? Cloudflare her şeyi origin'den yeniden çeker.",
544
+
545
+ "msg.html.cleared": "HTML önbelleği boşaltıldı ({count} girdi)",
546
+ "msg.data.cleared": "Veri önbelleği boşaltıldı ({count} girdi)",
547
+ "msg.data.clearedPrefix": "`{prefix}` altındaki {count} veri girdisi düşürüldü",
548
+ "msg.target.invalid": "Hedef `/` ile başlamalı",
549
+ "msg.html.marked": "`{target}` için {count} girdi bayat işaretlendi",
550
+ "msg.html.dropped": "`{target}` için {count} girdi silindi",
551
+ "msg.key.missing": "Anahtar eksik",
552
+ "msg.entry.dropped": "`{key}` düşürüldü",
553
+ "msg.entry.absent": "`{key}` önbellekte değildi",
554
+ "msg.redis.unreachable": "Redis'e ulaşılamıyor",
555
+ "msg.redis.notConnected": "Redis bağlı değil",
556
+ "msg.redis.htmlKeys": "{count} html anahtarı",
557
+ "msg.redis.dataKeys": "{count} veri anahtarı",
558
+ "msg.redis.dbKeys": "veritabanında {count} anahtar",
559
+ "msg.redis.memory": "{value} kullanımda",
560
+ "msg.redis.droppedHtml": "{count} paylaşımlı html anahtarı düşürüldü",
561
+ "msg.redis.droppedData": "{count} paylaşımlı veri anahtarı düşürüldü",
562
+ "msg.cf.purgedEverything": "Cloudflare önbelleği düşürüldü (her şey)",
563
+ "msg.cf.noPaths": "Yol verilmedi",
564
+ "msg.cf.noHostname": "Tam URL kurulamadı — cache().cloudflare.hostname ayarla",
565
+ "msg.cf.purgedUrls": "{batches} istekte {count} URL Cloudflare'de düşürüldü",
566
+ "msg.cf.nothingToPurge": "Düşürülecek bir şey yok",
567
+ "msg.cf.purgedKeys": "{count} {kind} Cloudflare'de düşürüldü",
568
+ "msg.cf.settingChanged": "Cloudflare {id} artık {value}",
569
+ "msg.cf.featureChanged": "Cloudflare {feature} {value} yapıldı",
570
+ "msg.cf.reserveClearing":
571
+ "Cache Reserve boşaltması başladı — Cloudflare'de arkada koşuyor",
572
+ "msg.cf.failed": "Cloudflare: {error}",
573
+ "msg.prewarm.busy": "Zaten bir ısıtma turu koşuyor",
574
+ "msg.prewarm.paths": "{count} yol ısıtılıyor",
575
+ "msg.prewarm.all": "Bütün yollar ısıtılıyor",
576
+ "msg.action.unknown": "Bilinmeyen işlem: {type}",
577
+
578
+ "login.title": "Yönetim paneli",
579
+ "login.lead": "Bu turun şifresi sunucu logundaki <code>ADMIN</code> kutusunda.",
580
+ "login.placeholder": "32 haneli şifre",
581
+ "login.submit": "Aç",
582
+ "login.hint": "Üç başarısız deneme bu adresi 24 saat engeller.",
583
+ "login.tooMany": "Çok fazla deneme.",
584
+ "login.invalid": "Şifre geçersiz.",
585
+
586
+ "upstream.section": "Upstream",
587
+ "upstream.host": "Host",
588
+ "upstream.rate": "Hız",
589
+ "upstream.state": "Durum",
590
+ "upstream.empty": "İzlenen upstream host yok.",
591
+
592
+ "routes.section": "Kayıtlı rotalar",
593
+ "routes.method": "Method",
594
+ "routes.path": "Yol",
595
+ "routes.lastStatus": "Son status",
596
+ "routes.lastMs": "Son ms",
597
+ "routes.hits": "İsabet",
598
+ "routes.empty": "Rota yok.",
599
+ "routes.modules": "Route modülleri",
600
+ "routes.file": "Dosya",
601
+ "routes.openLogs": "Loglar",
602
+
603
+ "views.section": "Şablonlar",
604
+ "views.path": "Yol",
605
+ "views.kind": "Tür",
606
+ "views.search": "Şablon ara…",
607
+ "views.empty": "Şablon yok.",
608
+
609
+ "logs.section": "Canlı loglar",
610
+ "logs.search": "url / mesaj ara…",
611
+ "logs.kind.all": "tüm türler",
612
+ "logs.method.all": "tüm methodlar",
613
+ "logs.status.all": "tüm status",
614
+ "logs.cache.all": "tüm cache",
615
+ "logs.path": "yol / rota…",
616
+ "logs.pause": "duraklat",
617
+ "logs.clear": "Görünümü temizle",
618
+ "logs.empty": "Log satırı yok.",
619
+ "logs.shown": "{shown} görünüyor · {total} tamponda",
620
+
621
+ "unit.second": "sn",
622
+ "unit.minute": "dk",
623
+ "unit.hour": "sa",
624
+ "unit.day": "g",
625
+ "unit.ms": "ms",
626
+ },
627
+ };
628
+
629
+ export const LANGUAGES = /** @type {const} */ (["en", "tr"]);
630
+
631
+ /** @type {Record<string, string>} */
632
+ export const LANGUAGE_NAMES = { en: "English", tr: "Türkçe" };
633
+
634
+ /**
635
+ * Kayıtlı tercih yoksa tarayıcının diline bakılır: paneli Türkçe bir makinede
636
+ * açan biri için varsayılanın İngilizce olmasının bir gerekçesi yok.
637
+ *
638
+ * @returns {string}
639
+ */
640
+ function detect() {
641
+ // Tamamı korumalı: panel `file://` üzerinden ya da sıkı bir gizlilik
642
+ // kipinde açılmış olabilir, ve bu modül testlerde tarayıcı dışında da
643
+ // import ediliyor.
644
+ // `document` kontrolü tarayıcı dışında `localStorage`'a hiç dokunmamak için:
645
+ // Node'da bu global'i okumak uyarı basıyor.
646
+ try {
647
+ if (typeof document !== "undefined") {
648
+ const stored = localStorage.getItem(STORAGE_KEY);
649
+ if (stored && MESSAGES[stored]) return stored;
650
+ }
651
+ } catch {
652
+ // Depolama yoksa tercih bu sekmeye özel kalır.
653
+ }
654
+
655
+ try {
656
+ const preferred = navigator.languages ?? [navigator.language];
657
+ for (const tag of preferred) {
658
+ const base = String(tag ?? "").toLowerCase().split("-")[0];
659
+ if (MESSAGES[base]) return base;
660
+ }
661
+ } catch {
662
+ // Tarayıcı dışında `navigator` yok.
663
+ }
664
+
665
+ return "en";
666
+ }
667
+
668
+ let current = detect();
669
+
670
+ /** @returns {string} */
671
+ export function getLanguage() {
672
+ return current;
673
+ }
674
+
675
+ /**
676
+ * @param {string} language
677
+ */
678
+ export function setLanguage(language) {
679
+ if (!MESSAGES[language]) return;
680
+ current = language;
681
+
682
+ try {
683
+ localStorage.setItem(STORAGE_KEY, language);
684
+ } catch {
685
+ // Yazamıyorsak seçim yalnızca bu sekme için geçerli olur.
686
+ }
687
+
688
+ document.documentElement.lang = language;
689
+ }
690
+
691
+ /**
692
+ * Çeviri. Eksik anahtar **İngilizceye düşer**, o da yoksa anahtarın kendisi
693
+ * döner: yarım çevrilmiş bir sözlük paneli boş etiketlerle bırakmasın.
694
+ *
695
+ * @param {string} key
696
+ * @param {Record<string, string | number>} [params]
697
+ * @returns {string}
698
+ */
699
+ export function t(key, params) {
700
+ const template = MESSAGES[current]?.[key] ?? MESSAGES.en[key] ?? key;
701
+ if (!params) return template;
702
+
703
+ return template.replace(/\{(\w+)\}/g, (match, name) =>
704
+ name in params ? String(params[name]) : match,
705
+ );
706
+ }
707
+
708
+ /**
709
+ * `data-i18n` taşıyan her düğümü çevirir.
710
+ *
711
+ * Üç öznitelik var: `data-i18n` metni, `data-i18n-ph` placeholder'ı,
712
+ * `data-i18n-html` ise içinde `<code>` geçen açıklama metinlerini yazar —
713
+ * kaynağı sözlüğün kendisi olduğu için sabit metin, kullanıcı verisi değil.
714
+ *
715
+ * @param {ParentNode} [root]
716
+ */
717
+ export function applyTranslations(root = document) {
718
+ document.documentElement.lang = current;
719
+
720
+ for (const node of root.querySelectorAll("[data-i18n]")) {
721
+ node.textContent = t(node.getAttribute("data-i18n") ?? "");
722
+ }
723
+
724
+ for (const node of root.querySelectorAll("[data-i18n-html]")) {
725
+ node.innerHTML = t(node.getAttribute("data-i18n-html") ?? "");
726
+ }
727
+
728
+ for (const node of root.querySelectorAll("[data-i18n-ph]")) {
729
+ node.setAttribute("placeholder", t(node.getAttribute("data-i18n-ph") ?? ""));
730
+ }
731
+
732
+ for (const node of root.querySelectorAll("[data-i18n-title]")) {
733
+ node.setAttribute("title", t(node.getAttribute("data-i18n-title") ?? ""));
734
+ }
735
+ }
736
+
737
+ /**
738
+ * Header'daki dil seçici. Çevirisi olan diller sözlükten geliyor, yani üçüncü
739
+ * bir dil eklemek yalnızca `MESSAGES`'a bir giriş demek.
740
+ *
741
+ * @param {(language: string) => void} onChange
742
+ * @returns {HTMLSelectElement}
743
+ */
744
+ export function languageSelect(onChange) {
745
+ const select = document.createElement("select");
746
+ select.className = "lang";
747
+ select.setAttribute("aria-label", t("shell.language"));
748
+
749
+ for (const language of LANGUAGES) {
750
+ const option = document.createElement("option");
751
+ option.value = language;
752
+ option.textContent = LANGUAGE_NAMES[language];
753
+ option.selected = language === current;
754
+ select.append(option);
755
+ }
756
+
757
+ select.addEventListener("change", () => {
758
+ setLanguage(select.value);
759
+ select.setAttribute("aria-label", t("shell.language"));
760
+ onChange(select.value);
761
+ });
762
+
763
+ return select;
764
+ }