jskelet 0.6.3 → 0.6.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.
Files changed (153) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +628 -620
  3. package/LICENSE +21 -21
  4. package/README.md +2 -0
  5. package/bin/jskelet.mjs +130 -130
  6. package/docs/01-baslangic.md +291 -291
  7. package/docs/02-mimari.md +310 -310
  8. package/docs/03-routing.md +515 -515
  9. package/docs/04-render-ve-sablonlar.md +667 -661
  10. package/docs/05-islands.md +486 -486
  11. package/docs/06-cache.md +1467 -1443
  12. package/docs/07-yapilandirma.md +1208 -1197
  13. package/docs/08-build.md +429 -429
  14. package/docs/09-dev-araclari.md +364 -364
  15. package/docs/10-dagitim.md +348 -338
  16. package/docs/12-panel-ve-oturum.md +479 -478
  17. package/docs/README.md +83 -83
  18. package/docs/en/01-getting-started.md +298 -298
  19. package/docs/en/02-architecture.md +329 -329
  20. package/docs/en/03-routing.md +531 -531
  21. package/docs/en/04-rendering.md +675 -669
  22. package/docs/en/05-islands.md +497 -497
  23. package/docs/en/06-caching.md +1476 -1453
  24. package/docs/en/07-configuration.md +1229 -1219
  25. package/docs/en/08-build.md +447 -447
  26. package/docs/en/09-dev-tools.md +373 -373
  27. package/docs/en/10-deployment.md +351 -340
  28. package/docs/en/11-migration.md +398 -398
  29. package/docs/en/12-dashboards-and-sessions.md +489 -488
  30. package/docs/en/README.md +87 -87
  31. package/package.json +137 -137
  32. package/src/build/ensure-build.mjs +19 -19
  33. package/src/build/paths.mjs +153 -153
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +349 -349
  36. package/src/build/tasks/css.mjs +235 -235
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +357 -357
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/build/tasks/templates.mjs +20 -20
  42. package/src/client/admin/i18n.js +764 -764
  43. package/src/client/admin/login.html +74 -74
  44. package/src/client/admin/panel.css +809 -809
  45. package/src/client/admin/panel.html +495 -495
  46. package/src/client/admin/panel.js +1251 -1251
  47. package/src/client/devtools/report.html +185 -185
  48. package/src/client/devtools/report.js +745 -745
  49. package/src/client/devtools/seo.js +628 -628
  50. package/src/client/dom.js +95 -95
  51. package/src/client/form.js +192 -192
  52. package/src/client/index.js +45 -45
  53. package/src/client/registry.js +305 -305
  54. package/src/client/safe-image.js +91 -91
  55. package/src/client/shared-cookie.js +225 -225
  56. package/src/client/store.js +36 -36
  57. package/src/client/swap.js +188 -188
  58. package/src/compile/codegen.js +336 -336
  59. package/src/compile/compile-all.js +149 -149
  60. package/src/compile/errors.js +66 -66
  61. package/src/compile/expr.js +409 -409
  62. package/src/compile/index.js +17 -17
  63. package/src/compile/parse.js +541 -541
  64. package/src/compile/resolve.js +211 -211
  65. package/src/compile/scan-exports.js +51 -51
  66. package/src/config/defaults.js +541 -534
  67. package/src/config/index.js +1500 -1469
  68. package/src/config/pattern.js +107 -107
  69. package/src/generate.mjs +163 -163
  70. package/src/http/control-flow.js +71 -71
  71. package/src/http/cookies-entry.js +21 -21
  72. package/src/http/cookies.js +277 -277
  73. package/src/http/request-cache.js +46 -46
  74. package/src/http/request-context.js +165 -165
  75. package/src/http/shared-cookie.js +178 -178
  76. package/src/index.js +101 -101
  77. package/src/init.mjs +232 -230
  78. package/src/migrate/apply.mjs +262 -262
  79. package/src/migrate/babel.mjs +79 -79
  80. package/src/migrate/classify.mjs +155 -155
  81. package/src/migrate/config.mjs +126 -126
  82. package/src/migrate/fs-walk.mjs +191 -191
  83. package/src/migrate/parse.mjs +26 -26
  84. package/src/migrate/scan.mjs +177 -177
  85. package/src/migrate/transform/expr-source.mjs +168 -168
  86. package/src/migrate/transform/island.mjs +67 -67
  87. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  88. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  89. package/src/migrate/transform/page-split.mjs +435 -435
  90. package/src/migrate/write.mjs +81 -81
  91. package/src/migrate.mjs +171 -171
  92. package/src/runtime/alias-hooks.mjs +119 -119
  93. package/src/runtime/register.mjs +4 -4
  94. package/src/server/admin/actions.js +229 -229
  95. package/src/server/admin/auth.js +125 -125
  96. package/src/server/admin/event-log.js +151 -151
  97. package/src/server/admin/gate.js +209 -209
  98. package/src/server/admin/inventory.js +188 -188
  99. package/src/server/admin/mount.js +56 -56
  100. package/src/server/admin/router.js +216 -216
  101. package/src/server/admin/snapshot.js +241 -241
  102. package/src/server/assets.js +147 -147
  103. package/src/server/auth/handoff.js +309 -309
  104. package/src/server/cache-blob.js +70 -70
  105. package/src/server/cache-control.js +45 -0
  106. package/src/server/cache-deps.js +42 -42
  107. package/src/server/cache-vary.js +113 -113
  108. package/src/server/cloudflare.js +607 -607
  109. package/src/server/create-app.js +366 -366
  110. package/src/server/data-cache.js +553 -553
  111. package/src/server/dev/report.js +485 -485
  112. package/src/server/dev/socket.js +170 -170
  113. package/src/server/dev/version-check.mjs +139 -139
  114. package/src/server/disk-cache.js +233 -233
  115. package/src/server/ejs-adapter.js +59 -59
  116. package/src/server/html-cache.js +1196 -1196
  117. package/src/server/image-optimizer.js +500 -500
  118. package/src/server/logs/access-middleware.js +66 -66
  119. package/src/server/logs/file-sink.js +193 -193
  120. package/src/server/logs/pipeline.js +165 -165
  121. package/src/server/logs/s3-put.js +214 -214
  122. package/src/server/logs/s3-sink.js +112 -112
  123. package/src/server/metadata.js +102 -102
  124. package/src/server/middleware/compression.js +205 -205
  125. package/src/server/middleware/csrf.js +134 -134
  126. package/src/server/middleware/dev-gate.js +75 -75
  127. package/src/server/middleware/headers.js +37 -37
  128. package/src/server/middleware/redirects.js +32 -32
  129. package/src/server/middleware/robots-txt.js +341 -341
  130. package/src/server/middleware/static-precompressed.js +121 -121
  131. package/src/server/middleware/trailing-slash.js +53 -53
  132. package/src/server/middleware/upstream-proxy.js +141 -141
  133. package/src/server/og-image.js +369 -356
  134. package/src/server/port-guard.js +255 -255
  135. package/src/server/prewarm.js +1082 -1082
  136. package/src/server/redis.js +588 -588
  137. package/src/server/render.js +910 -910
  138. package/src/server/router.js +157 -157
  139. package/src/server/status-page.js +265 -265
  140. package/src/server/upstream-limiter.js +376 -376
  141. package/src/server/upstream-tracking.js +166 -166
  142. package/src/shared/cookie-domain.js +66 -66
  143. package/src/start.mjs +22 -22
  144. package/src/templates/layout.ejs +30 -30
  145. package/src/templates/layout.jsk +30 -30
  146. package/src/version.mjs +31 -31
  147. package/src/views/components/loader.js +101 -101
  148. package/src/views/helpers/html.js +102 -102
  149. package/src/views/helpers/tags.js +375 -375
  150. package/types/config/defaults.d.ts +6 -0
  151. package/types/config/index.d.ts +6 -0
  152. package/types/server/cache-control.d.ts +28 -0
  153. package/types/server/og-image.d.ts +5 -0
@@ -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
+ }