@privateclaw/privateclaw-relay 0.1.3 → 0.1.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.
@@ -0,0 +1,997 @@
1
+ const BUNDLES = {
2
+ en: {
3
+ meta: {
4
+ nativeLabel: "English",
5
+ htmlLang: "en",
6
+ },
7
+ site: {
8
+ documentTitle: "PrivateClaw | Private rooms for your OpenClaw",
9
+ brandTagline: "Private rooms for your OpenClaw",
10
+ languageLabel: "Language",
11
+ navGithub: "GitHub",
12
+ navBetaGroup: "Beta Google Group",
13
+ navTelegramGroup: "Telegram group chat",
14
+ heroBadge: "Private • Encrypted • Invite-only",
15
+ heroTitle: "Bring your people into one private OpenClaw room.",
16
+ heroBody:
17
+ "PrivateClaw turns one shared OpenClaw into a beautiful private room for the people you actually trust. Scan once, join instantly, and let the relay carry ciphertext only.",
18
+ heroPrimaryCta: "Open web chat",
19
+ heroSecondaryCta: "Join the beta group",
20
+ heroDesktopHint:
21
+ "Web chat works on desktop and phone. On desktop, pasting an invite or selecting a saved QR image is usually the fastest way in.",
22
+ heroMobileHint:
23
+ "Web chat works here too, so you can open the PrivateClaw room right away from this phone.",
24
+ appComingSoon: "iOS public beta + Android closed alpha",
25
+ iosComingSoon: "Join iOS public beta",
26
+ androidComingSoon: "Join Android closed alpha",
27
+ androidBetaNote:
28
+ "Android closed alpha is delivered through Google Play. Join the Google Group first or Play will say the test is unavailable.",
29
+ previewStatus: "Live preview",
30
+ previewTitle: "A room that feels personal, not public.",
31
+ previewBody:
32
+ "Keep family, teammates, or friends around the OpenClaw you already trust, without moving the conversation into a public social feed.",
33
+ heroStats: [
34
+ {
35
+ value: "One shared OpenClaw",
36
+ label: "Invite your favorite people into the same assistant without exposing the room to strangers.",
37
+ },
38
+ {
39
+ value: "Encrypted end to end",
40
+ label: "The relay routes traffic, but it does not get your decrypted chat content.",
41
+ },
42
+ {
43
+ value: "Made for real moments",
44
+ label: "Trip planning, private family talks, team check-ins, and fun group sessions all feel natural.",
45
+ },
46
+ ],
47
+ previewMessages: [
48
+ {
49
+ speaker: "RiverCat",
50
+ role: "member",
51
+ text: "Can we plan the weekend trip here instead of the big public group?",
52
+ },
53
+ {
54
+ speaker: "PrivateClaw",
55
+ role: "assistant",
56
+ text: "Absolutely. I can keep the ideas, routes, and packing lists together for everyone in this room.",
57
+ },
58
+ {
59
+ speaker: "SkyFox",
60
+ role: "member",
61
+ text: "Nice. One OpenClaw, one room, and no random timeline noise.",
62
+ },
63
+ ],
64
+ featuresKicker: "Why people love it",
65
+ featuresTitle: "Private by default, warm by design.",
66
+ featuresBody:
67
+ "PrivateClaw is for people who want secure spaces that still feel relaxed, social, and fun around one shared OpenClaw.",
68
+ features: [
69
+ {
70
+ eyebrow: "Invite only",
71
+ title: "One scan. One room.",
72
+ body: "A room starts from a QR code or invite link, so you choose who gets in and when.",
73
+ },
74
+ {
75
+ eyebrow: "Shared assistant",
76
+ title: "One OpenClaw for everyone you trust.",
77
+ body: "Friends, family, or teammates can all talk with the same OpenClaw session and enjoy the same context together.",
78
+ },
79
+ {
80
+ eyebrow: "Private routing",
81
+ title: "The relay is just a courier.",
82
+ body: "Messages stay encrypted between the app and your OpenClaw side of the session, so the relay only forwards ciphertext.",
83
+ },
84
+ {
85
+ eyebrow: "Mobile first",
86
+ title: "Feels like a real chat app.",
87
+ body: "Slash commands, media, group presence, and gentle motion all make it feel familiar on a phone.",
88
+ },
89
+ ],
90
+ scenariosKicker: "Built for real life",
91
+ scenariosTitle: "Security without losing the fun.",
92
+ scenariosBody:
93
+ "PrivateClaw is designed for close circles that want to keep using OpenClaw together without turning to public social software.",
94
+ scenarios: [
95
+ {
96
+ eyebrow: "Family",
97
+ title: "Keep private family planning together.",
98
+ body: "Share schedules, ideas, and travel plans around one OpenClaw room that feels calm and personal.",
99
+ },
100
+ {
101
+ eyebrow: "Friends",
102
+ title: "Use group chat for the fun stuff too.",
103
+ body: "Play with prompts, brainstorm gifts, or build shared plans with the same assistant in the same room.",
104
+ },
105
+ {
106
+ eyebrow: "Teams",
107
+ title: "Open a short-lived secure side channel.",
108
+ body: "Spin up an invite-only room when you need a focused conversation without dragging people into another public chat silo.",
109
+ },
110
+ ],
111
+ setupKicker: "Quick setup",
112
+ setupTitle: "Get the provider ready in five short steps.",
113
+ setupBody:
114
+ "If you already run OpenClaw, PrivateClaw is mostly a quick plugin setup. The public relay is already preconfigured, so the relay override is only needed for your own deployment.",
115
+ setupSteps: [
116
+ {
117
+ step: "Step 1",
118
+ title: "Install the provider",
119
+ body: "Add the official PrivateClaw plugin from npm.",
120
+ commands: ["openclaw plugins install @privateclaw/privateclaw@latest"],
121
+ },
122
+ {
123
+ step: "Step 2",
124
+ title: "Enable it",
125
+ body: "Turn the plugin on in your OpenClaw install.",
126
+ commands: ["openclaw plugins enable privateclaw"],
127
+ },
128
+ {
129
+ step: "Step 3",
130
+ title: "Optional: use your own relay",
131
+ body: "Skip this if you are happy with the default public relay at https://relay.privateclaw.us.",
132
+ commands: [
133
+ "openclaw config set plugins.entries.privateclaw.config.relayBaseUrl https://your-relay.example.com",
134
+ ],
135
+ },
136
+ {
137
+ step: "Step 4",
138
+ title: "Restart the gateway",
139
+ body: "Restart the running openclaw start process or whichever service hosts your gateway so the new plugin and config are reloaded.",
140
+ },
141
+ {
142
+ step: "Step 5",
143
+ title: "Generate a QR invite",
144
+ body: "In any OpenClaw chat channel, create a room invite and let people scan it into PrivateClaw.",
145
+ commands: ["/privateclaw", "/privateclaw group"],
146
+ note: "Use `/privateclaw group` when you want multiple people sharing the same room.",
147
+ },
148
+ ],
149
+ betaKicker: "Early access",
150
+ betaTitle: "Join the PrivateClaw beta circle.",
151
+ betaBody:
152
+ "iOS public beta is live on TestFlight, and Android closed alpha is live on Google Play. Join the Google Group first so Google Play will admit you to the Android test.",
153
+ betaPrimaryCta: "Join the Google Group",
154
+ betaTelegramCta: "Join the Telegram chat",
155
+ betaFootnote:
156
+ "Android closed alpha requires Google Group membership first. iOS public beta is open through TestFlight, and Telegram stays open for the early community.",
157
+ footerLine: "Private rooms, shared OpenClaw, and a calmer way to chat.",
158
+ footerDisclaimer: "PrivateClaw is independent and not affiliated with OpenClaw.",
159
+ footerPrivacy: "Privacy",
160
+ footerTerms: "Terms",
161
+ footerSupport: "Beta updates live in the Google Group.",
162
+ footerCopyright: "Copyright GG AI Studio 2026.",
163
+ },
164
+ chat: {
165
+ documentTitle: "PrivateClaw Web Chat",
166
+ headerTagline: "Web chat",
167
+ disconnectButton: "Disconnect",
168
+ desktopNoteTitle: "Desktop works too",
169
+ desktopNoteBody:
170
+ "This room is fully usable on larger screens as well. On desktop, pasting the invite or choosing a saved QR image is often the quickest way to join.",
171
+ connectKicker: "Secure pairing",
172
+ connectTitle: "Paste your PrivateClaw invite.",
173
+ connectBody:
174
+ "Paste the invite text, QR payload, or full announcement message from OpenClaw to enter the room.",
175
+ changeInviteButton: "Use another invite",
176
+ statusPanelTitle: "End-to-end encrypted",
177
+ statusIdle: "Waiting for an invite.",
178
+ inviteInputLabel: "Invite",
179
+ inviteInputHelp:
180
+ "You can paste a raw privateclaw:// link, a base64 payload, JSON, or even a full message that contains the invite.",
181
+ inviteInputPlaceholder: "Paste invite or announcement here",
182
+ scanButton: "Scan QR",
183
+ scanImageButton: "Use QR image",
184
+ scanHelp:
185
+ "Use your camera on supported browsers, or choose a QR screenshot from your device.",
186
+ scannerTitle: "Scan a PrivateClaw QR",
187
+ scannerBody:
188
+ "Point your camera at a PrivateClaw invite QR. The room will open as soon as it is recognized.",
189
+ scannerCloseButton: "Close",
190
+ scannerStatusStarting: "Opening camera…",
191
+ scannerStatusScanning: "Scanning for a PrivateClaw invite…",
192
+ scannerStatusFound: "Invite found. Connecting…",
193
+ connectButton: "Connect securely",
194
+ providerLabel: "Provider",
195
+ expiresLabel: "Expires",
196
+ modeLabel: "Mode",
197
+ relayLabel: "Relay",
198
+ identityLabel: "Identity",
199
+ participantsLabel: "Participants",
200
+ betaGroupButton: "Join the beta Google Group",
201
+ emptyTitle: "Your private room will appear here",
202
+ emptyBody:
203
+ "Once connected, messages, slash commands, and media from OpenClaw stay inside this encrypted session.",
204
+ draftAttachmentsLabel: "Ready to send",
205
+ sendButton: "Send",
206
+ composerPlaceholder: "Message your room…",
207
+ commandSheetTitle: "Slash commands",
208
+ commandSheetClose: "Close",
209
+ commandButtonAria: "Open slash commands",
210
+ attachButtonAria: "Attach files",
211
+ providerUnknown: "PrivateClaw",
212
+ relayUnknown: "Default relay",
213
+ identityUnknown: "Private guest",
214
+ modePrivate: "1:1 room",
215
+ modeGroup: "Shared room",
216
+ modeGroupMuted: "Shared room · Bot muted",
217
+ statusLabelIdle: "Idle",
218
+ statusLabelConnecting: "Connecting",
219
+ statusLabelReconnecting: "Reconnecting",
220
+ statusLabelRelayAttached: "Handshaking",
221
+ statusLabelActive: "Connected",
222
+ statusLabelClosed: "Closed",
223
+ statusLabelError: "Needs attention",
224
+ relayConnecting: "Connecting to the relay…",
225
+ relayHandshake: "Relay connected. Finishing encrypted handshake…",
226
+ relayConnectionError: "Connection problem: {reason}",
227
+ relaySessionClosed: "This session has closed.",
228
+ relaySessionClosedWithReason: "This session has closed: {reason}",
229
+ relayError: "Relay error: {reason}",
230
+ relayUnknownEvent: "Unexpected relay event: {reason}",
231
+ relayUnknownPayload: "Unexpected encrypted payload: {reason}",
232
+ welcomeFallback: "PrivateClaw connected.",
233
+ customRelayWarningTitle: "Custom relay server",
234
+ customRelayWarningBody:
235
+ "This invite points to {relayLabel} instead of the default PrivateClaw relay. Continue only if you trust this server.",
236
+ sessionDisconnected:
237
+ "Session disconnected. Paste a new invite when you want to start again.",
238
+ sessionRenewedNotice: "Session renewed until {time}.",
239
+ connectFailed: "The invite could not be parsed.",
240
+ invalidInviteVersion: "This invite version is not supported by the web client.",
241
+ sessionKeyLengthError: "The session key must be 32 bytes.",
242
+ browserCryptoUnavailable:
243
+ "This browser does not support the Web Crypto features PrivateClaw needs.",
244
+ scanUnsupported:
245
+ "This browser cannot decode QR codes yet. Paste the invite or try a newer browser.",
246
+ scanCameraUnsupported:
247
+ "Camera scanning is not available here. Try choosing a QR image or pasting the invite.",
248
+ scanPermissionDenied:
249
+ "Camera access was blocked. Allow camera permission or choose a QR image instead.",
250
+ scanPickerFallback:
251
+ "Live camera scanning is not available here, so PrivateClaw opened your camera or photo library instead.",
252
+ scanNoCodeFound: "No QR code was found in that image.",
253
+ scanReadFailed: "Could not read that QR image.",
254
+ fileTooLarge: "{name} is larger than 5 MB and was skipped.",
255
+ fileReadError: "Could not read {name}.",
256
+ sendFailed: "Could not send: {reason}",
257
+ notConnected: "Connect to a room before sending messages.",
258
+ noCommandsYet: "Slash commands will appear after the room handshake finishes.",
259
+ assistantLabel: "PrivateClaw",
260
+ systemLabel: "System",
261
+ youLabel: "You",
262
+ peerLabelFallback: "Participant",
263
+ pendingLabel: "Thinking…",
264
+ mutedLabel: "Bot muted",
265
+ commandSourceOpenclaw: "OpenClaw",
266
+ commandSourcePlugin: "Plugin",
267
+ commandSourcePrivateclaw: "PrivateClaw",
268
+ commandArgHint: "Needs arguments",
269
+ commandSendNow: "Tap to send now",
270
+ draftRemoveAttachment: "Remove attachment",
271
+ downloadAttachment: "Download",
272
+ attachmentNoPreview: "Preview unavailable in the browser",
273
+ toastConnected: "Secure room connected.",
274
+ toastInviteReady: "Invite loaded. Starting secure connection…",
275
+ toastDisconnected: "PrivateClaw disconnected.",
276
+ toastCommandInserted: "Command inserted.",
277
+ toastCommandSent: "Command sent.",
278
+ toastCopiedNothing: "Nothing to send yet.",
279
+ expiresUnknown: "Unknown",
280
+ desktopBanner: "Desktop preview",
281
+ },
282
+ },
283
+ "zh-CN": {
284
+ meta: {
285
+ nativeLabel: "简体中文",
286
+ htmlLang: "zh-CN",
287
+ },
288
+ site: {
289
+ documentTitle: "PrivateClaw | 给 OpenClaw 的私密聊天室",
290
+ brandTagline: "给 OpenClaw 的私密聊天室",
291
+ languageLabel: "语言",
292
+ navGithub: "GitHub",
293
+ navBetaGroup: "Google 内测群组",
294
+ navTelegramGroup: "Telegram 群聊",
295
+ heroBadge: "私密 • 加密 • 邀请制",
296
+ heroTitle: "把你信任的人带进同一个私密 OpenClaw 房间。",
297
+ heroBody:
298
+ "PrivateClaw 把一套共享的 OpenClaw 变成只属于你们的小房间。扫一次码就能进入,中继只负责转发密文,不读取聊天内容。",
299
+ heroPrimaryCta: "打开网页聊天",
300
+ heroSecondaryCta: "加入内测群组",
301
+ heroDesktopHint: "网页聊天支持桌面和手机访问。在桌面端,直接粘贴邀请或选择一张二维码截图通常是最快的进入方式。",
302
+ heroMobileHint: "网页聊天在手机上也完全可用,你现在就可以直接打开 PrivateClaw 房间。",
303
+ appComingSoon: "iOS 公开测试 + Android 封闭 alpha",
304
+ iosComingSoon: "加入 iOS 公开测试",
305
+ androidComingSoon: "加入 Android 封闭 alpha",
306
+ androidBetaNote:
307
+ "Android 封闭 alpha 通过 Google Play 分发。请先加入 Google 群组,否则 Google Play 会提示你暂时无法参与测试。",
308
+ previewStatus: "实时预览",
309
+ previewTitle: "像私人聊天室,而不是公共社交软件。",
310
+ previewBody:
311
+ "把家人、朋友或队友带到你已经信任的 OpenClaw 身边,不必把对话放进公开社交平台。",
312
+ heroStats: [
313
+ {
314
+ value: "一套共享 OpenClaw",
315
+ label: "把你最在意的人邀请到同一个助手里,而不是把房间暴露给陌生人。",
316
+ },
317
+ {
318
+ value: "端到端加密",
319
+ label: "中继只转发流量,拿不到解密后的聊天内容。",
320
+ },
321
+ {
322
+ value: "适合真实场景",
323
+ label: "家庭沟通、旅行计划、团队小群和一起玩 AI 都很自然。",
324
+ },
325
+ ],
326
+ previewMessages: [
327
+ {
328
+ speaker: "流萤狐",
329
+ role: "member",
330
+ text: "这次周末出行我们在这里聊吧,不放到那个大群里了。",
331
+ },
332
+ {
333
+ speaker: "PrivateClaw",
334
+ role: "assistant",
335
+ text: "没问题,我可以把行程、路线和打包清单都留在这个房间里,方便大家一起看。",
336
+ },
337
+ {
338
+ speaker: "晴空猫",
339
+ role: "member",
340
+ text: "不错,一套 OpenClaw、一个房间,没有时间线噪音。",
341
+ },
342
+ ],
343
+ featuresKicker: "为什么大家会喜欢",
344
+ featuresTitle: "默认私密,但体验很轻松。",
345
+ featuresBody: "PrivateClaw 适合想要安全感、又不想失去聊天乐趣的人。",
346
+ features: [
347
+ {
348
+ eyebrow: "邀请制",
349
+ title: "扫一扫,就进入同一个房间。",
350
+ body: "每个房间都从二维码或邀请链接开始,谁能加入、什么时候加入,都由你决定。",
351
+ },
352
+ {
353
+ eyebrow: "共享助手",
354
+ title: "你信任的人共用一套 OpenClaw。",
355
+ body: "家人、朋友或队友都能围绕同一个 OpenClaw 会话聊天,共享上下文和乐趣。",
356
+ },
357
+ {
358
+ eyebrow: "私密转发",
359
+ title: "中继只是快递员。",
360
+ body: "消息在 App 和 OpenClaw 侧之间保持加密,中继只负责传递密文。",
361
+ },
362
+ {
363
+ eyebrow: "移动优先",
364
+ title: "体验像真正的聊天应用。",
365
+ body: "斜杠命令、媒体消息、群成员状态和流畅的手机界面都已经准备好了。",
366
+ },
367
+ ],
368
+ scenariosKicker: "贴近真实使用",
369
+ scenariosTitle: "要安全,也要有一起玩的乐趣。",
370
+ scenariosBody:
371
+ "PrivateClaw 适合那些想继续一起使用 OpenClaw、又不想把沟通放在公开社交软件中的小圈子。",
372
+ scenarios: [
373
+ {
374
+ eyebrow: "家庭",
375
+ title: "把家里的计划留在家里。",
376
+ body: "日程、旅行、清单和各种家务讨论都能放在一个安静的小房间里。",
377
+ },
378
+ {
379
+ eyebrow: "朋友",
380
+ title: "一起聊天,也一起玩 AI。",
381
+ body: "脑暴礼物、做攻略、一起试 prompt,都能围绕同一个助手完成。",
382
+ },
383
+ {
384
+ eyebrow: "团队",
385
+ title: "临时拉起安全的小通道。",
386
+ body: "当你需要一个短期、专注、非公开的讨论空间时,随时开一个邀请制房间。",
387
+ },
388
+ ],
389
+ setupKicker: "快速配置",
390
+ setupTitle: "五个步骤,把 Provider 准备好。",
391
+ setupBody:
392
+ "如果你已经在使用 OpenClaw,PrivateClaw 基本就是一次轻量插件配置。默认公共 relay 已经预设好,只有在你想切到自己的部署时,才需要额外写 relay 地址。",
393
+ setupSteps: [
394
+ {
395
+ step: "步骤 1",
396
+ title: "安装 Provider",
397
+ body: "通过 npm 安装官方 PrivateClaw 插件。",
398
+ commands: ["openclaw plugins install @privateclaw/privateclaw@latest"],
399
+ },
400
+ {
401
+ step: "步骤 2",
402
+ title: "启用插件",
403
+ body: "在你的 OpenClaw 安装里把它打开。",
404
+ commands: ["openclaw plugins enable privateclaw"],
405
+ },
406
+ {
407
+ step: "步骤 3",
408
+ title: "可选:切到自己的 relay",
409
+ body: "如果你直接使用默认公共 relay `https://relay.privateclaw.us`,这一步可以跳过。",
410
+ commands: [
411
+ "openclaw config set plugins.entries.privateclaw.config.relayBaseUrl https://your-relay.example.com",
412
+ ],
413
+ },
414
+ {
415
+ step: "步骤 4",
416
+ title: "重启网关",
417
+ body: "重启正在运行的 `openclaw start` 进程,或者你用来托管 gateway 的服务,让插件和配置重新加载。",
418
+ },
419
+ {
420
+ step: "步骤 5",
421
+ title: "生成二维码邀请",
422
+ body: "在任意 OpenClaw 聊天渠道里生成房间邀请,然后让大家扫码进入 PrivateClaw。",
423
+ commands: ["/privateclaw", "/privateclaw group"],
424
+ note: "如果你想让多人共享同一个房间,就使用 `/privateclaw group`。",
425
+ },
426
+ ],
427
+ betaKicker: "抢先体验",
428
+ betaTitle: "加入 PrivateClaw 内测圈。",
429
+ betaBody:
430
+ "iOS 公开测试已经在 TestFlight 开放,Android 封闭 alpha 也已经在 Google Play 上线。想参与 Android 测试的话,请先加入 Google 群组。",
431
+ betaPrimaryCta: "加入 Google 群组",
432
+ betaTelegramCta: "加入 Telegram 群聊",
433
+ betaFootnote:
434
+ "Android 封闭 alpha 需要先加入 Google Group;iOS 公开测试可以直接通过 TestFlight 参与,Telegram 交流群也持续开放。",
435
+ footerLine: "私密房间、共享 OpenClaw、更安静的聊天方式。",
436
+ footerDisclaimer: "PrivateClaw 与 OpenClaw 没有附属关系。",
437
+ footerPrivacy: "隐私",
438
+ footerTerms: "条款",
439
+ footerSupport: "最新内测动态会发布在 Google 群组。",
440
+ footerCopyright: "Copyright GG AI Studio 2026.",
441
+ },
442
+ chat: {
443
+ documentTitle: "PrivateClaw 网页聊天",
444
+ headerTagline: "网页聊天",
445
+ disconnectButton: "断开连接",
446
+ desktopNoteTitle: "桌面端也可以直接使用",
447
+ desktopNoteBody: "这个房间在大屏上同样可用。在桌面端,直接粘贴邀请,或者选择一张已经保存的二维码图片,通常是最快的加入方式。",
448
+ connectKicker: "安全配对",
449
+ connectTitle: "粘贴你的 PrivateClaw 邀请。",
450
+ connectBody: "把 OpenClaw 发来的邀请文本、二维码 payload,或者包含邀请链接的整段消息粘贴进来即可。",
451
+ changeInviteButton: "更换邀请",
452
+ statusPanelTitle: "端到端加密",
453
+ statusIdle: "等待粘贴邀请。",
454
+ inviteInputLabel: "邀请内容",
455
+ inviteInputHelp: "支持 privateclaw:// 链接、base64 payload、JSON,或包含邀请的完整消息。",
456
+ inviteInputPlaceholder: "在这里粘贴邀请或整段公告",
457
+ scanButton: "扫码连接",
458
+ scanImageButton: "识别二维码图片",
459
+ scanHelp: "在支持的浏览器中可直接调用相机扫码,也可以选择设备里的二维码截图。",
460
+ scannerTitle: "扫描 PrivateClaw 二维码",
461
+ scannerBody: "把摄像头对准 PrivateClaw 邀请二维码,识别成功后会自动开始连接。",
462
+ scannerCloseButton: "关闭",
463
+ scannerStatusStarting: "正在打开相机…",
464
+ scannerStatusScanning: "正在识别 PrivateClaw 邀请二维码…",
465
+ scannerStatusFound: "已识别邀请,正在连接…",
466
+ connectButton: "安全连接",
467
+ providerLabel: "提供方",
468
+ expiresLabel: "过期时间",
469
+ modeLabel: "模式",
470
+ relayLabel: "Relay",
471
+ identityLabel: "身份",
472
+ participantsLabel: "成员",
473
+ betaGroupButton: "加入 Google 内测群组",
474
+ emptyTitle: "你的私密房间会显示在这里",
475
+ emptyBody: "连接成功后,OpenClaw 的消息、斜杠命令和媒体内容都会留在这个加密会话里。",
476
+ draftAttachmentsLabel: "待发送附件",
477
+ sendButton: "发送",
478
+ composerPlaceholder: "给房间发送消息…",
479
+ commandSheetTitle: "斜杠命令",
480
+ commandSheetClose: "关闭",
481
+ commandButtonAria: "打开斜杠命令",
482
+ attachButtonAria: "添加文件",
483
+ providerUnknown: "PrivateClaw",
484
+ relayUnknown: "默认 relay",
485
+ identityUnknown: "Private guest",
486
+ modePrivate: "单聊房间",
487
+ modeGroup: "共享房间",
488
+ modeGroupMuted: "共享房间 · 机器人已静音",
489
+ statusLabelIdle: "空闲",
490
+ statusLabelConnecting: "连接中",
491
+ statusLabelReconnecting: "重连中",
492
+ statusLabelRelayAttached: "握手中",
493
+ statusLabelActive: "已连接",
494
+ statusLabelClosed: "已关闭",
495
+ statusLabelError: "需要处理",
496
+ relayConnecting: "正在连接中继服务…",
497
+ relayHandshake: "已连接中继,正在完成加密握手…",
498
+ relayConnectionError: "连接异常:{reason}",
499
+ relaySessionClosed: "当前会话已关闭。",
500
+ relaySessionClosedWithReason: "当前会话已关闭:{reason}",
501
+ relayError: "中继错误:{reason}",
502
+ relayUnknownEvent: "收到未知中继事件:{reason}",
503
+ relayUnknownPayload: "收到未知加密载荷:{reason}",
504
+ welcomeFallback: "PrivateClaw 已连接。",
505
+ customRelayWarningTitle: "自定义 relay 服务器",
506
+ customRelayWarningBody:
507
+ "这个邀请使用的是 {relayLabel},而不是默认的 PrivateClaw relay。只有在你信任这台服务器时才继续。",
508
+ sessionDisconnected: "会话已断开。需要继续时,请重新粘贴新的邀请。",
509
+ sessionRenewedNotice: "会话已续期至 {time}。",
510
+ connectFailed: "无法解析这条邀请。",
511
+ invalidInviteVersion: "这个邀请版本暂时不受网页客户端支持。",
512
+ sessionKeyLengthError: "会话密钥长度必须是 32 字节。",
513
+ browserCryptoUnavailable: "当前浏览器不支持 PrivateClaw 所需的 Web Crypto 能力。",
514
+ scanUnsupported: "当前浏览器暂时不支持识别二维码。请直接粘贴邀请,或换一个更新的浏览器。",
515
+ scanCameraUnsupported: "当前环境无法直接调用相机扫码。你可以选择二维码图片,或直接粘贴邀请。",
516
+ scanPermissionDenied: "相机权限被拒绝了。请允许相机访问,或改用二维码图片。",
517
+ scanPickerFallback: "当前环境不支持实时相机扫码,PrivateClaw 已改为打开相机或相册供你选择二维码图片。",
518
+ scanNoCodeFound: "这张图片里没有识别到二维码。",
519
+ scanReadFailed: "读取这张二维码图片失败。",
520
+ fileTooLarge: "{name} 超过 5 MB,已跳过。",
521
+ fileReadError: "读取 {name} 失败。",
522
+ sendFailed: "发送失败:{reason}",
523
+ notConnected: "请先连接房间,再发送消息。",
524
+ noCommandsYet: "斜杠命令会在握手完成后出现。",
525
+ assistantLabel: "PrivateClaw",
526
+ systemLabel: "系统",
527
+ youLabel: "你",
528
+ peerLabelFallback: "成员",
529
+ pendingLabel: "思考中…",
530
+ mutedLabel: "机器人已静音",
531
+ commandSourceOpenclaw: "OpenClaw",
532
+ commandSourcePlugin: "插件",
533
+ commandSourcePrivateclaw: "PrivateClaw",
534
+ commandArgHint: "需要参数",
535
+ commandSendNow: "点击可立即发送",
536
+ draftRemoveAttachment: "移除附件",
537
+ downloadAttachment: "下载",
538
+ attachmentNoPreview: "浏览器中无法预览",
539
+ toastConnected: "安全房间已连接。",
540
+ toastInviteReady: "邀请已载入,正在建立安全连接…",
541
+ toastDisconnected: "PrivateClaw 已断开。",
542
+ toastCommandInserted: "命令已插入输入框。",
543
+ toastCommandSent: "命令已发送。",
544
+ toastCopiedNothing: "现在还没有可发送的内容。",
545
+ expiresUnknown: "未知",
546
+ desktopBanner: "桌面预览",
547
+ },
548
+ },
549
+ "zh-Hant": {
550
+ meta: {
551
+ nativeLabel: "繁體中文",
552
+ htmlLang: "zh-Hant",
553
+ },
554
+ site: {
555
+ documentTitle: "PrivateClaw | 給 OpenClaw 的私密聊天室",
556
+ brandTagline: "給 OpenClaw 的私密聊天室",
557
+ languageLabel: "語言",
558
+ navGithub: "GitHub",
559
+ navBetaGroup: "Google 內測群組",
560
+ navTelegramGroup: "Telegram 群聊",
561
+ heroBadge: "私密 • 加密 • 邀請制",
562
+ heroTitle: "把你信任的人帶進同一個私密 OpenClaw 房間。",
563
+ heroBody:
564
+ "PrivateClaw 把一套共享的 OpenClaw 變成只屬於你們的小房間。掃一次碼就能進入,中繼只負責轉送密文,不讀取聊天內容。",
565
+ heroPrimaryCta: "開啟網頁聊天",
566
+ heroSecondaryCta: "加入內測群組",
567
+ heroDesktopHint: "網頁聊天支援桌面與手機。在桌面端,直接貼上邀請,或選擇一張已儲存的 QR 圖片,通常是最快的進入方式。",
568
+ heroMobileHint: "網頁聊天在手機上也完全可用,你現在就可以直接開啟 PrivateClaw 房間。",
569
+ appComingSoon: "iOS 公開測試 + Android 封閉 alpha",
570
+ iosComingSoon: "加入 iOS 公開測試",
571
+ androidComingSoon: "加入 Android 封閉 alpha",
572
+ androidBetaNote:
573
+ "Android 封閉 alpha 透過 Google Play 發放。請先加入 Google 群組,否則 Google Play 會顯示你目前無法參與測試。",
574
+ previewStatus: "即時預覽",
575
+ previewTitle: "像私人聊天室,而不是公開社群軟體。",
576
+ previewBody:
577
+ "把家人、朋友或隊友帶到你已經信任的 OpenClaw 身邊,不必把對話放進公開社群平台。",
578
+ heroStats: [
579
+ {
580
+ value: "一套共享 OpenClaw",
581
+ label: "把你最在意的人邀請到同一個助手裡,而不是把房間暴露給陌生人。",
582
+ },
583
+ {
584
+ value: "端對端加密",
585
+ label: "中繼只轉送流量,拿不到解密後的聊天內容。",
586
+ },
587
+ {
588
+ value: "貼近真實場景",
589
+ label: "家庭溝通、旅行規劃、團隊小群和一起玩 AI 都很自然。",
590
+ },
591
+ ],
592
+ previewMessages: [
593
+ {
594
+ speaker: "流螢狐",
595
+ role: "member",
596
+ text: "這次週末出遊我們在這裡聊吧,不放到那個大群裡了。",
597
+ },
598
+ {
599
+ speaker: "PrivateClaw",
600
+ role: "assistant",
601
+ text: "沒問題,我可以把行程、路線和打包清單都留在這個房間裡,方便大家一起看。",
602
+ },
603
+ {
604
+ speaker: "晴空貓",
605
+ role: "member",
606
+ text: "不錯,一套 OpenClaw、一個房間,沒有時間線噪音。",
607
+ },
608
+ ],
609
+ featuresKicker: "為什麼大家會喜歡",
610
+ featuresTitle: "預設私密,但體驗很輕鬆。",
611
+ featuresBody: "PrivateClaw 適合想要安全感、又不想失去聊天樂趣的人。",
612
+ features: [
613
+ {
614
+ eyebrow: "邀請制",
615
+ title: "掃一下,就進入同一個房間。",
616
+ body: "每個房間都從 QR code 或邀請連結開始,誰能加入、什麼時候加入,都由你決定。",
617
+ },
618
+ {
619
+ eyebrow: "共享助手",
620
+ title: "你信任的人共用一套 OpenClaw。",
621
+ body: "家人、朋友或隊友都能圍繞同一個 OpenClaw 會話聊天,共享上下文和樂趣。",
622
+ },
623
+ {
624
+ eyebrow: "私密轉送",
625
+ title: "中繼只是快遞員。",
626
+ body: "訊息在 App 與 OpenClaw 端之間保持加密,中繼只負責傳遞密文。",
627
+ },
628
+ {
629
+ eyebrow: "行動優先",
630
+ title: "體驗像真正的聊天 App。",
631
+ body: "斜槓命令、媒體訊息、群成員狀態和流暢的手機介面都已經準備好了。",
632
+ },
633
+ ],
634
+ scenariosKicker: "貼近真實使用",
635
+ scenariosTitle: "要安全,也要有一起玩的樂趣。",
636
+ scenariosBody:
637
+ "PrivateClaw 適合那些想繼續一起使用 OpenClaw、又不想把溝通放在公開社群軟體中的小圈子。",
638
+ scenarios: [
639
+ {
640
+ eyebrow: "家庭",
641
+ title: "把家裡的計畫留在家裡。",
642
+ body: "行程、旅行、清單和各種家務討論都能放在一個安靜的小房間裡。",
643
+ },
644
+ {
645
+ eyebrow: "朋友",
646
+ title: "一起聊天,也一起玩 AI。",
647
+ body: "腦暴禮物、做攻略、一起試 prompt,都能圍繞同一個助手完成。",
648
+ },
649
+ {
650
+ eyebrow: "團隊",
651
+ title: "臨時拉起安全的小通道。",
652
+ body: "當你需要一個短期、專注、非公開的討論空間時,隨時開一個邀請制房間。",
653
+ },
654
+ ],
655
+ setupKicker: "快速設定",
656
+ setupTitle: "五個步驟,把 Provider 準備好。",
657
+ setupBody:
658
+ "如果你已經在使用 OpenClaw,PrivateClaw 基本上只需要一次輕量的外掛設定。預設公共 relay 已經內建,只有當你想切到自己的部署時,才需要額外設定 relay 位址。",
659
+ setupSteps: [
660
+ {
661
+ step: "步驟 1",
662
+ title: "安裝 Provider",
663
+ body: "透過 npm 安裝官方 PrivateClaw 外掛。",
664
+ commands: ["openclaw plugins install @privateclaw/privateclaw@latest"],
665
+ },
666
+ {
667
+ step: "步驟 2",
668
+ title: "啟用外掛",
669
+ body: "在你的 OpenClaw 安裝裡把它打開。",
670
+ commands: ["openclaw plugins enable privateclaw"],
671
+ },
672
+ {
673
+ step: "步驟 3",
674
+ title: "可選:切到自己的 relay",
675
+ body: "如果你直接使用預設公共 relay `https://relay.privateclaw.us`,這一步可以略過。",
676
+ commands: [
677
+ "openclaw config set plugins.entries.privateclaw.config.relayBaseUrl https://your-relay.example.com",
678
+ ],
679
+ },
680
+ {
681
+ step: "步驟 4",
682
+ title: "重新啟動 gateway",
683
+ body: "重新啟動正在執行的 `openclaw start` 行程,或你用來託管 gateway 的服務,讓外掛與設定重新載入。",
684
+ },
685
+ {
686
+ step: "步驟 5",
687
+ title: "產生 QR 邀請",
688
+ body: "在任意 OpenClaw 聊天渠道裡建立房間邀請,然後讓大家掃碼加入 PrivateClaw。",
689
+ commands: ["/privateclaw", "/privateclaw group"],
690
+ note: "如果你想讓多人共享同一個房間,就使用 `/privateclaw group`。",
691
+ },
692
+ ],
693
+ betaKicker: "搶先體驗",
694
+ betaTitle: "加入 PrivateClaw 內測圈。",
695
+ betaBody:
696
+ "iOS 公開測試已經在 TestFlight 開放,Android 封閉 alpha 也已經在 Google Play 上線。想參與 Android 測試的話,請先加入 Google 群組。",
697
+ betaPrimaryCta: "加入 Google 群組",
698
+ betaTelegramCta: "加入 Telegram 群聊",
699
+ betaFootnote:
700
+ "Android 封閉 alpha 需要先加入 Google Group;iOS 公開測試可以直接透過 TestFlight 參與,Telegram 群聊也持續開放。",
701
+ footerLine: "私密房間、共享 OpenClaw、更安靜的聊天方式。",
702
+ footerDisclaimer: "PrivateClaw 與 OpenClaw 沒有附屬關係。",
703
+ footerPrivacy: "隱私",
704
+ footerTerms: "條款",
705
+ footerSupport: "最新內測動態會發佈在 Google 群組。",
706
+ footerCopyright: "Copyright GG AI Studio 2026.",
707
+ },
708
+ chat: {
709
+ documentTitle: "PrivateClaw 網頁聊天",
710
+ headerTagline: "網頁聊天",
711
+ disconnectButton: "中斷連線",
712
+ desktopNoteTitle: "桌面端也可以直接使用",
713
+ desktopNoteBody: "這個房間在大螢幕上一樣可用。在桌面端,直接貼上邀請,或選擇一張已經存好的 QR 圖片,通常是最快的加入方式。",
714
+ connectKicker: "安全配對",
715
+ connectTitle: "貼上你的 PrivateClaw 邀請。",
716
+ connectBody: "把 OpenClaw 發來的邀請文字、QR payload,或包含邀請連結的整段訊息貼進來即可。",
717
+ changeInviteButton: "更換邀請",
718
+ statusPanelTitle: "端對端加密",
719
+ statusIdle: "等待貼上邀請。",
720
+ inviteInputLabel: "邀請內容",
721
+ inviteInputHelp: "支援 privateclaw:// 連結、base64 payload、JSON,或包含邀請的完整訊息。",
722
+ inviteInputPlaceholder: "在這裡貼上邀請或整段公告",
723
+ scanButton: "掃碼連線",
724
+ scanImageButton: "辨識 QR 圖片",
725
+ scanHelp: "在支援的瀏覽器中可直接呼叫相機掃碼,也可以選擇裝置裡的 QR 截圖。",
726
+ scannerTitle: "掃描 PrivateClaw QR 碼",
727
+ scannerBody: "把鏡頭對準 PrivateClaw 邀請 QR 碼,辨識成功後會自動開始連線。",
728
+ scannerCloseButton: "關閉",
729
+ scannerStatusStarting: "正在開啟相機…",
730
+ scannerStatusScanning: "正在辨識 PrivateClaw 邀請 QR 碼…",
731
+ scannerStatusFound: "已辨識邀請,正在連線…",
732
+ connectButton: "安全連線",
733
+ providerLabel: "提供方",
734
+ expiresLabel: "到期時間",
735
+ modeLabel: "模式",
736
+ relayLabel: "Relay",
737
+ identityLabel: "身份",
738
+ participantsLabel: "成員",
739
+ betaGroupButton: "加入 Google 內測群組",
740
+ emptyTitle: "你的私密房間會顯示在這裡",
741
+ emptyBody: "連線成功後,OpenClaw 的訊息、斜槓命令與媒體內容都會留在這個加密會話裡。",
742
+ draftAttachmentsLabel: "待傳送附件",
743
+ sendButton: "傳送",
744
+ composerPlaceholder: "對房間傳送訊息…",
745
+ commandSheetTitle: "斜槓命令",
746
+ commandSheetClose: "關閉",
747
+ commandButtonAria: "打開斜槓命令",
748
+ attachButtonAria: "加入檔案",
749
+ providerUnknown: "PrivateClaw",
750
+ relayUnknown: "預設 relay",
751
+ identityUnknown: "Private guest",
752
+ modePrivate: "單聊房間",
753
+ modeGroup: "共享房間",
754
+ modeGroupMuted: "共享房間 · 機器人已靜音",
755
+ statusLabelIdle: "閒置",
756
+ statusLabelConnecting: "連線中",
757
+ statusLabelReconnecting: "重連中",
758
+ statusLabelRelayAttached: "握手中",
759
+ statusLabelActive: "已連線",
760
+ statusLabelClosed: "已關閉",
761
+ statusLabelError: "需要處理",
762
+ relayConnecting: "正在連接中繼服務…",
763
+ relayHandshake: "已連接中繼,正在完成加密握手…",
764
+ relayConnectionError: "連線異常:{reason}",
765
+ relaySessionClosed: "目前會話已關閉。",
766
+ relaySessionClosedWithReason: "目前會話已關閉:{reason}",
767
+ relayError: "中繼錯誤:{reason}",
768
+ relayUnknownEvent: "收到未知中繼事件:{reason}",
769
+ relayUnknownPayload: "收到未知加密載荷:{reason}",
770
+ welcomeFallback: "PrivateClaw 已連線。",
771
+ customRelayWarningTitle: "自訂 relay 伺服器",
772
+ customRelayWarningBody:
773
+ "這個邀請使用的是 {relayLabel},而不是預設的 PrivateClaw relay。只有在你信任這台伺服器時才繼續。",
774
+ sessionDisconnected: "會話已中斷。需要繼續時,請重新貼上新的邀請。",
775
+ sessionRenewedNotice: "會話已續期至 {time}。",
776
+ connectFailed: "無法解析這條邀請。",
777
+ invalidInviteVersion: "這個邀請版本暫時不支援網頁客戶端。",
778
+ sessionKeyLengthError: "會話金鑰長度必須是 32 位元組。",
779
+ browserCryptoUnavailable: "目前瀏覽器不支援 PrivateClaw 所需的 Web Crypto 能力。",
780
+ scanUnsupported: "目前瀏覽器暫時不支援辨識 QR 碼。請直接貼上邀請,或換用更新的瀏覽器。",
781
+ scanCameraUnsupported: "目前環境無法直接呼叫相機掃碼。你可以選擇 QR 圖片,或直接貼上邀請。",
782
+ scanPermissionDenied: "相機權限被拒絕了。請允許相機存取,或改用 QR 圖片。",
783
+ scanPickerFallback: "目前環境不支援即時鏡頭掃碼,PrivateClaw 已改為開啟鏡頭或相簿供你選擇 QR 圖片。",
784
+ scanNoCodeFound: "這張圖片裡沒有辨識到 QR 碼。",
785
+ scanReadFailed: "讀取這張 QR 圖片失敗。",
786
+ fileTooLarge: "{name} 超過 5 MB,已跳過。",
787
+ fileReadError: "讀取 {name} 失敗。",
788
+ sendFailed: "傳送失敗:{reason}",
789
+ notConnected: "請先連接房間,再傳送訊息。",
790
+ noCommandsYet: "斜槓命令會在握手完成後出現。",
791
+ assistantLabel: "PrivateClaw",
792
+ systemLabel: "系統",
793
+ youLabel: "你",
794
+ peerLabelFallback: "成員",
795
+ pendingLabel: "思考中…",
796
+ mutedLabel: "機器人已靜音",
797
+ commandSourceOpenclaw: "OpenClaw",
798
+ commandSourcePlugin: "外掛",
799
+ commandSourcePrivateclaw: "PrivateClaw",
800
+ commandArgHint: "需要參數",
801
+ commandSendNow: "點擊可立即傳送",
802
+ draftRemoveAttachment: "移除附件",
803
+ downloadAttachment: "下載",
804
+ attachmentNoPreview: "瀏覽器中無法預覽",
805
+ toastConnected: "安全房間已連線。",
806
+ toastInviteReady: "邀請已載入,正在建立安全連線…",
807
+ toastDisconnected: "PrivateClaw 已中斷。",
808
+ toastCommandInserted: "命令已插入輸入框。",
809
+ toastCommandSent: "命令已傳送。",
810
+ toastCopiedNothing: "現在還沒有可傳送的內容。",
811
+ expiresUnknown: "未知",
812
+ desktopBanner: "桌面預覽",
813
+ },
814
+ },
815
+ };
816
+
817
+ const LOCALE_KEY = "privateclaw.site.locale";
818
+ const LOCALE_ORDER = ["en", "zh-CN", "zh-Hant"];
819
+ const localeListeners = new Set();
820
+ let currentLocale = detectLocale();
821
+
822
+ function readStoredLocale() {
823
+ try {
824
+ return window.localStorage.getItem(LOCALE_KEY);
825
+ } catch (error) {
826
+ console.warn("PrivateClaw could not read the stored locale.", error);
827
+ return null;
828
+ }
829
+ }
830
+
831
+ function writeStoredLocale(locale) {
832
+ try {
833
+ window.localStorage.setItem(LOCALE_KEY, locale);
834
+ } catch (error) {
835
+ console.warn("PrivateClaw could not persist the locale.", error);
836
+ }
837
+ }
838
+
839
+ function detectLocale() {
840
+ const stored = normalizeLocale(readStoredLocale());
841
+ if (stored) {
842
+ return stored;
843
+ }
844
+
845
+ const requested = [
846
+ ...(navigator.languages || []),
847
+ navigator.language || "en",
848
+ ];
849
+ for (const locale of requested) {
850
+ const normalized = normalizeLocale(locale);
851
+ if (normalized) {
852
+ return normalized;
853
+ }
854
+ }
855
+ return "en";
856
+ }
857
+
858
+ function normalizeLocale(input) {
859
+ if (typeof input !== "string" || input.trim() === "") {
860
+ return null;
861
+ }
862
+
863
+ const lowered = input.trim().toLowerCase();
864
+ if (lowered === "en" || lowered.startsWith("en-")) {
865
+ return "en";
866
+ }
867
+ if (
868
+ lowered === "zh-hant" ||
869
+ lowered.includes("hant") ||
870
+ lowered.startsWith("zh-tw") ||
871
+ lowered.startsWith("zh-hk") ||
872
+ lowered.startsWith("zh-mo")
873
+ ) {
874
+ return "zh-Hant";
875
+ }
876
+ if (lowered === "zh" || lowered.startsWith("zh-cn") || lowered.startsWith("zh-sg") || lowered.startsWith("zh-hans")) {
877
+ return "zh-CN";
878
+ }
879
+ if (LOCALE_ORDER.includes(input)) {
880
+ return input;
881
+ }
882
+ return null;
883
+ }
884
+
885
+ function getByPath(bundle, keyPath) {
886
+ return keyPath.split(".").reduce((value, segment) => {
887
+ if (value && typeof value === "object" && segment in value) {
888
+ return value[segment];
889
+ }
890
+ return undefined;
891
+ }, bundle);
892
+ }
893
+
894
+ function interpolate(template, values = {}) {
895
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
896
+ if (Object.hasOwn(values, key)) {
897
+ return String(values[key]);
898
+ }
899
+ return match;
900
+ });
901
+ }
902
+
903
+ function applyDocumentLanguage() {
904
+ if (typeof document !== "undefined") {
905
+ document.documentElement.lang = BUNDLES[currentLocale].meta.htmlLang;
906
+ }
907
+ }
908
+
909
+ export function getLocale() {
910
+ return currentLocale;
911
+ }
912
+
913
+ export function setLocale(locale) {
914
+ const nextLocale = normalizeLocale(locale) || "en";
915
+ if (nextLocale === currentLocale) {
916
+ return;
917
+ }
918
+ currentLocale = nextLocale;
919
+ writeStoredLocale(nextLocale);
920
+ applyDocumentLanguage();
921
+ for (const listener of localeListeners) {
922
+ listener(nextLocale);
923
+ }
924
+ }
925
+
926
+ export function getBundle(locale = currentLocale) {
927
+ return BUNDLES[locale] || BUNDLES.en;
928
+ }
929
+
930
+ export function getValue(keyPath, locale = currentLocale) {
931
+ const localized = getByPath(getBundle(locale), keyPath);
932
+ if (localized !== undefined) {
933
+ return localized;
934
+ }
935
+ return getByPath(BUNDLES.en, keyPath);
936
+ }
937
+
938
+ export function t(keyPath, values) {
939
+ const value = getValue(keyPath);
940
+ if (typeof value !== "string") {
941
+ return keyPath;
942
+ }
943
+ return interpolate(value, values);
944
+ }
945
+
946
+ export function onLocaleChange(listener) {
947
+ localeListeners.add(listener);
948
+ return () => localeListeners.delete(listener);
949
+ }
950
+
951
+ export function getLocaleOptions() {
952
+ return LOCALE_ORDER.map((locale) => ({
953
+ value: locale,
954
+ label: BUNDLES[locale].meta.nativeLabel,
955
+ }));
956
+ }
957
+
958
+ export function bindLocaleSelect(select) {
959
+ if (!(select instanceof HTMLSelectElement)) {
960
+ throw new TypeError("Expected a select element for locale binding.");
961
+ }
962
+
963
+ select.replaceChildren();
964
+ for (const option of getLocaleOptions()) {
965
+ const optionElement = document.createElement("option");
966
+ optionElement.value = option.value;
967
+ optionElement.textContent = option.label;
968
+ select.append(optionElement);
969
+ }
970
+ select.value = currentLocale;
971
+
972
+ const syncValue = (locale) => {
973
+ if (select.value !== locale) {
974
+ select.value = locale;
975
+ }
976
+ };
977
+ const unsubscribe = onLocaleChange(syncValue);
978
+ select.addEventListener("change", () => {
979
+ setLocale(select.value);
980
+ });
981
+ return unsubscribe;
982
+ }
983
+
984
+ export function applyTranslations(root = document) {
985
+ applyDocumentLanguage();
986
+ const elements = root.querySelectorAll("[data-i18n]");
987
+ for (const element of elements) {
988
+ const keyPath = element.getAttribute("data-i18n");
989
+ if (!keyPath) {
990
+ continue;
991
+ }
992
+ const value = t(keyPath);
993
+ element.textContent = value;
994
+ }
995
+ }
996
+
997
+ applyDocumentLanguage();