aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
blob: 690a70bdf3d25aac57ef1790da606fda471b4ccc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
/**
 * English — the source catalogue.
 *
 * Every other locale is a translation of this file and carries the same keys.
 * A value may be a string, or an object of Intl.PluralRules categories
 * (`one`, `few`, `many`, `other`) when it is rendered with a count.
 *
 * Product vocabulary stays in English everywhere: `hub`, `node` and `GEK` name
 * the CLI (`meshbay-node`), node.toml and the docs, so translating them would
 * cut the interface off from everything an operator reads and types.
 */

export default {
  // Nav
  'nav.toggle_menu': 'Toggle menu',
  'nav.light_mode': 'Light mode',
  'nav.dark_mode': 'Dark mode',
  'nav.logout': 'Logout',
  'nav.login': 'Login',
  'nav.minimize_tray': 'Minimize to tray',
  'tray.show': 'Show MeshBay',
  'tray.quit': 'Quit',
  'tray.start_node': 'Start the node',
  'tray.stop_node': 'Stop the node',

  // Sidebar
  'sidebar.my_groups': 'My Groups',
  'sidebar.no_groups': 'No groups yet',
  'sidebar.legal': 'Legal information',
  'sidebar.discover': 'Discover',
  'sidebar.public_groups': 'Public groups',

  // Login
  'login.title': 'Login',
  'login.username': 'Username',
  'login.password': 'Password',
  'login.submit': 'Login',
  'login.loading': 'Logging in...',
  'login.no_account': 'No account?',
  'login.register_link': 'Register',
  'login.forgot': 'Forgot your passphrase?',
  'reset.title': 'Reset passphrase',
  'reset.request_intro': 'Enter your username and the email on the account. If they match, a reset code is sent to it.',
  'reset.send_code': 'Send code',
  'reset.form_intro': 'Enter the code from your email and choose a new passphrase. If you have your recovery key, add it to also restore access to your groups.',
  'reset.code': 'Reset code',
  'reset.recovery_key': 'Recovery key (optional)',
  'reset.recovery_key_hint': 'Leave blank if you do not have it. Without it your sign-in is restored, but each group must be rejoined.',
  'reset.new_pass': 'New passphrase',
  'reset.new_pass_repeat': 'Repeat new passphrase',
  'reset.submit': 'Reset passphrase',
  'reset.working': 'Restoring your group identities…',
  'reset.signin_restored': 'Your sign-in has been restored.',
  'reset.groups_restored': 'Your group identities were restored on every reachable node.',
  'reset.no_recovery': 'Your group identities were not restored. For each group, ask its operator to unpin you and send a new invitation code, then rejoin.',
  'reset.needs_operator': 'These groups still need action — ask each operator to unpin you and send a new code:',
  'reset.go_app': 'Go to the app',
  'reset.back_to_login': 'Back to sign in',
  'reset.err_unsupported': 'Passphrase reset needs the browser crypto module, which is not available here.',

  // Register
  'register.title': 'Register',
  'register.username': 'Username',
  'register.email': 'Email',
  'register.password': 'Passphrase (min 12 chars)',
  'register.confirm': 'Confirm password',
  'register.submit': 'Register',
  'register.loading': 'Creating account...',
  'register.has_account': 'Already have an account?',
  'register.login_link': 'Login',
  'register.recovery_title': 'Save your recovery key',
  'register.recovery_intro': 'This key restores your group identities if you ever forget your passphrase. Store it in a password manager or somewhere safe.',
  'register.recovery_copy': 'Copy',
  'register.recovery_copied': 'Copied',
  'register.recovery_warning': 'If you lose both your passphrase and this key, your files stay on their nodes but you rejoin each group with a new identity.',
  'register.recovery_saved': 'I have saved my recovery key somewhere safe.',
  'register.recovery_continue': 'Continue',
  'register.recovery_email_opt': 'Also email this recovery key to me as a backup',
  'register.recovery_emailed': 'A copy has also been sent to your email, in the same message as your verification code.',
  'register.recovery_not_emailed': 'This was not emailed. Save it now — this is the only time it is shown.',
  'register.success_title': 'Verify your email',
  'register.success_msg': 'A verification code has been sent to your email address. Enter it below to activate your account.',
  'register.go_login': 'Go to login',
  'register.code_placeholder': 'Verification code',
  'register.verify_btn': 'Verify',
  'register.verifying': 'Verifying...',
  'register.verified_title': 'Email verified',
  'register.verified_msg': 'Your account is active. You can now log in.',
  'register.resend': 'Resend code',
  'register.resend_sent': 'A new code has been sent.',
  'login.pending_verification': 'Your email is not yet verified. Please check your inbox for the verification code.',
  'login.verify_link': 'Verify email',
  'login.invalid': "Wrong username or passphrase.",
  'login.locked': "Too many wrong passphrases for this account. Try again in {minutes} min, or reset your passphrase.",
  // Sign-in page: what MeshBay is (browser only)
  'welcome.title': 'The Internet as it was meant to be.',
  'welcome.lead': 'MeshBay is open-source software that gives you remote access to your personal files, and runs applications on top of the storage on your own computer:',
  'welcome.app_chat': 'Chat',
  'welcome.app_photos': 'Photos',
  'welcome.app_media': 'Media center',
  'welcome.app_video': 'Video player',
  'welcome.app_music': 'Music player',
  'welcome.groups': 'Create groups to give the people close to you access to some of these applications.',
  'welcome.e2e': 'Your data is end-to-end encrypted and travels peer to peer, from each device to the computer that hosts it (your node). It never passes through the meshbay.org hub.',
  'welcome.uses_title': 'What you can do with it',
  'welcome.use_chat': 'Instant messaging within a group',
  'welcome.use_photos': 'Share photo albums with your family and friends',
  'welcome.use_media': 'Reach your home media center from anywhere, for the whole household (streaming, plus Chromecast from the desktop app)',
  'welcome.use_apps': 'Build your own applications on the node you host',
  'welcome.hub_title': 'What meshbay.org does',
  'welcome.hub_body': 'meshbay.org is only a signaling service: it signs you in and connects the parties to one another, plug-and-play, across home networks.',
  'welcome.hub_never': 'Your content never reaches it:',
  'welcome.never_transit': 'Nothing passes through it: no content, no indexing',
  'welcome.never_stored': 'None of your files or messages are stored on it',
  'welcome.never_e2e': 'Encryption runs end to end, from each device to your node',
  'welcome.hub_free': 'The service is free. There is no business model by design, no tracking and no ads. Enjoy!',
  'welcome.download': 'Download (beta)',
  'welcome.legal': 'Legal information',
  'register.err_mismatch': 'Passwords do not match',
  'register.err_username_len': 'Username must be at least {n} characters.',
  'register.err_min_len': {
    one: 'Use at least {n} character',
    other: 'Use at least {n} characters',
  },
  'register.err_too_weak': 'Too easy to guess. Your passphrase is what protects '
    + 'your keys where they are stored — a few unrelated words work well.',
  'register.strength': 'Strength: about {bits} bits. This protects the copy of '
    + 'your keys kept on the nodes you join, so it is worth getting right.',
  'captcha.required': 'Please complete the captcha.',
  'captcha.failed': 'Captcha verification failed — please try again.',

  // Home
  'home.welcome': 'Welcome to MeshBay',
  'home.no_groups': 'You are not a member of any group yet.',
  'home.browse_prefix': 'Browse ',
  'home.browse_link': 'public groups',
  'home.browse_suffix': ' or ask a group admin to invite you.',
  'home.invite_only': 'Ask a group admin to invite you.',
  'home.my_groups': 'My Groups',

  // Explore
  'explore.title': 'Public Groups',
  'explore.search': 'Search groups...',
  'explore.loading': 'Loading...',
  'explore.empty': 'No public groups available.',
  'explore.create_group': 'Create group',
  'explore.join': 'Join',
  'explore.member': 'Joined',
  'explore.disabled': "This hub does not have public groups.",

  // Group page
  'group.default_name': 'Group',
  'group.tab_files': 'Files',
  'group.tab_chat': 'Chat',
  'group.tab_video': 'Videos',
  'group.tab_music': 'Music',
  'group.tab_photos': 'Photos',
  'group.tab_helloworld': 'HelloWorld',
  'helloworld.greeting': 'Hello, World!',
  'helloworld.no_directory': 'No folder chosen yet. Pick one in Settings.',
  'helloworld.counted': '{n} file(s)',
  'settings_app.helloworld_directory_label': 'Folder',
  'settings_app.helloworld_directory_hint': 'The folder this reference app lists. It exists to show that adding an application needs nothing beyond its own two files.',
  'group.tab_members': 'Members',
  'group.tab_settings': "Settings",
  'members.danger_leave_hint': "You will lose access to this group's files and chat.",
  'members.uploads_title': "Uploads",
  'members.uploads_on': "Members can add files",
  'members.uploads_off': "Only you can add files",
  'members.uploads_disable': "Turn off",
  'members.uploads_enable': "Turn on",
  'members.uploads_hint': "Applies to everyone but you. The node refuses the upload itself, so this is not a matter of hiding a button.",
  'members.apps_title': "Applications",
  'members.apps_hint': "Which parts of this group members see. At least one must stay on.",
  'members.apps_need_one': "At least one application must stay enabled.",
  'members.danger_delete_hint': "This removes the group for every member. It cannot be undone.",
  'group.filter': 'Filter files...',
  'group.refresh_index': 'Refresh file list',
  'group.col_name': 'Name',
  'group.col_size': 'Size',
  'group.col_type': 'Type',
  'group.col_date': 'Date',
  'group.empty_filter': 'No files match the filter',
  'group.empty_dir': 'This directory is empty',
  'group.download': 'Download',
  'group.play': 'Play',
  'group.offline_title': 'No nodes are currently online for this group.',
  'group.offline_hint': 'Files will appear when a node hosting this group connects.',
  'group.upload': 'Upload',
  'group.drop_here': 'Drop to upload into {dir}',
  'group.drop_not_here': 'Files can only be dropped into a folder you can write to.',
  'group.drop_conflict': 'Upload cancelled — already in this folder: {names}. Rename or remove it first.',
  'group.drop_invalid': 'Upload cancelled — these names cannot be stored on the node: {names}',
  'group.drop_unreadable': 'Upload cancelled — the dropped items could not be read.',
  'group.drop_mkdir_failed': 'Upload cancelled — a folder could not be created ({detail}).',
  'group.mkdir': 'New folder',
  'group.mkdir_prompt': 'New folder name',
  'group.mkdir_offline': 'Not connected to the node.',
  'group.download_offline': 'Not connected to the node — the download cannot start. It reconnects on its own; try again in a moment.',
  'group.download_write_stalled': 'The file stopped being written to disk ({seconds}s with no progress). The download was stopped rather than left hanging; try it again.',
  'device.add_title': 'This browser is not linked to this node yet',
  'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.',
  'device.add_btn': 'Get a linking code',
  'device.add_show': 'Type this code into a device already linked to this node, within the hour:',
  'device.mine_title': 'Your devices on this node',
  'device.mine_hint': 'Each browser or client you use here holds its own key. They are separate from your devices on other nodes.',
  'device.mine_empty': 'No device is linked here yet.',
  'device.this_one': 'this one',
  'device.revoke': 'Remove',
  'device.revoke_confirm': 'Remove this device? It will lose access to this node until it is linked again.',
  'device.approve_hint': 'Linking a new device? Enter the code it is showing.',
  'device.approve_btn': 'Approve',
  'device.approved': 'Device linked.',
  'group.root_unavailable': '(unavailable — the drive is disconnected)',
  'group.root_ejected': '(ejected)',
  'group.root_writable': 'Read/Write',
  'group.root_eject': 'Eject',
  'group.root_plug': 'Plug in',
  'group.view': 'View',
  'group.delete': 'Delete',
  'group.delete_confirm': 'Delete {name}?',
  'group.delete_group': 'Delete group',
  'group.delete_group_confirm': 'Permanently delete "{name}" and remove all members? Files on the node are preserved.',
  'group.err_transport': 'Transport module not loaded',

  // Status
  'status.idle': 'Idle',
  'status.discovering': 'Finding nodes...',
  'status.connecting': 'Connecting...',
  'status.connecting_short': 'Connecting...',
  'status.fetching': 'Fetching index...',
  'status.files': {
    one: '{n} file',
    other: '{n} files',
  },
  'status.offline': 'No nodes online',
  'status.error': 'Connection failed',

  // Chat
  'chat.empty': 'No messages yet. Start the conversation!',
  'chat.placeholder': 'Type a message...',
  'chat.send': 'Send',
  'chat.attach': 'Attach file',
  'chat.attach_read_only': 'No writable shared directory — attachments are off',

  // Video player
  'video.loading': 'Loading {name}...',
  'video.buffering': 'Buffering...',
  'video.resumed_at': "Resumed at {time}",
  'video.from_start': "Start from the beginning",
  'video.audio_track': 'Audio track',
  'video.audio_track_n': 'Track {n}',
  'video.subtitles': 'Subtitles',
  'video.subtitles_off': 'Off',
  'video.subtitle_track_n': 'Track {n}',
  'video.subtitles_forced': 'forced (signs only)',
  'video.subtitles_sdh': 'for the deaf and hard of hearing',
  'video.err_subtitle': 'Could not load this subtitle track',
  'video.close': 'Close (Esc)',
  'preview.pdf_fallback': 'This browser will not display the PDF inline. Download it instead — it was decrypted here either way.',
  'preview.too_large': 'This file is {size}, more than this page can hold in memory ({limit}). Download it instead — a download is written straight to disk.',
  'download.too_large_for_memory': '"{name}" is {size}. This browser can only save a file that large by streaming it to disk, and it has no way to do that here — it would have to hold the whole file in memory. Reload the page and try again; if that does not help, use the desktop app.',
  'group.upload_indexing': 'indexing…',
  'video.err_transport': 'Transport not connected',
  'video.err_mse': 'Codec not supported for streaming: {codec}',
  'video.mode_poster': 'Posters',
  'video.mode_flat': 'Flat list',
  'video.filter_all': 'All',
  'video.filter_movies': 'Movies',
  'video.filter_series': 'Series',
  'pager.range': '{from}–{to} of {total}',
  'pager.previous': 'Previous page',
  'pager.next': 'Next page',
  'video.empty': 'No videos found.',
  'video.no_root_configured': 'No Videos root folder is set for this group yet — an operator can choose one in Settings.',
  'music.no_root_configured': 'No Music root folder is set for this group yet — an operator can choose one in Settings.',
  'video.director': 'Director',
  'video.specials': 'Specials',
  'video.season_n': 'Season {n}',
  'video.episode_n': 'Episode {n}',
  'video.n_episodes': {
    one: '{n} episode',
    other: '{n} episodes',
  },
  'video.fix_match': 'Fix match…',
  'video.search_title': 'Correct the TMDB match',
  'video.search_placeholder': 'Search TMDB…',
  'video.search_button': 'Search',
  'video.search_no_results': 'No matches found.',
  'video.search_apply_hint': 'Applies to every file currently grouped under this title.',
  'video.search_apply_hint_movie': 'Applies to this file only.',
  'video.source_file': 'File: {name}',
  'video.no_match': 'No confident TMDB match — showing the filename.',
  'video.rematch_one': 'Re-match',
  'video.read_more': 'Read more',
  'video.read_less': 'Show less',

  // Music
  'music.mode_grid': 'Albums',
  'music.mode_flat': 'Flat list',
  'music.empty': 'No music found.',
  'music.unknown_album': 'Unknown album',
  'music.various': 'Various',
  'music.play_all': 'Play all',
  'music.menu_more': 'More…',
  'music.menu_play': 'Play',
  'music.menu_play_next': 'Play next',
  'music.menu_enqueue': 'Add to queue',
  'music.n_tracks': {
    one: '{n} track',
    other: '{n} tracks',
  },
  'music.err_transport': 'Transport not connected',
  'music.err_playback': 'Playback error',
  'music.err_transcode': 'Format conversion failed',
  'music.player_play': 'Play',
  'music.player_pause': 'Pause',
  'music.player_prev': 'Previous',
  'music.player_next': 'Next',
  'music.player_shuffle': 'Shuffle',
  'music.player_repeat_off': 'Repeat off',
  'music.player_repeat_all': 'Repeat all',
  'music.player_repeat_one': 'Repeat one',
  'music.player_volume': 'Volume',
  'music.player_close': 'Close player',
  'playlists.menu': 'Playlists',
  'playlists.load': 'Load a playlist',
  'playlists.create': 'New playlist…',
  'playlists.delete': 'Delete a playlist',
  'playlists.remove_track': 'Remove a track',
  'playlists.sync_now': 'Sync now',
  'playlists.add_to': 'Add to playlist',
  'playlists.favorites': 'Favourites',
  'playlists.none_yet': 'No playlists yet',
  'playlists.empty_playlist': 'This playlist is empty',
  'playlists.added': '{n} added',
  'playlists.already_there': 'Already in that playlist',
  'playlists.name_placeholder': 'Playlist name',
  'playlists.err_duplicate': 'A playlist by that name already exists',
  'playlists.confirm_delete': 'Delete the playlist "{name}"? This cannot be undone.',
  'playlists.deleted': '"{name}" deleted',
  'playlists.synced': 'Playlists synced',
  'playlists.sync_failed': 'Could not sync playlists',
  'playlists.err_no_key': 'Sign out and sign in with your password once to sync playlists across devices.',
  'playlists.err_too_large': '"{name}" is too large to sync to your other devices. Split it into playlists of about {n} tracks.',
  'playlists.track_removed': 'Track removed',
  'playlists.save_queue': 'Save the queue as a playlist…',
  'playlists.cancel': 'Cancel',
  'playlists.save': 'Save',
  'music.player_queue': 'Current queue',
  'music.queue_title': 'Playing now',

  // Photos
  'photo.empty': 'No photos found.',
  'photo.no_roots_configured': 'No Photos root folders are set for this group yet — an operator can choose some in Settings.',
  'photo.root_album': '(top level)',
  'photo.n_photos': {
    one: '{n} photo',
    other: '{n} photos',
  },
  'photo.back': 'Back',
  'photo.prev': 'Previous (←)',
  'photo.next': 'Next (→)',
  'photo.zip_album': 'Download album as zip',
  'photo.zoom_in': 'Zoom in',
  'photo.zoom_out': 'Zoom out',
  'photo.zoom_fit_title': 'Fit to window',
  'photo.zoom_fit_label': 'Fit',
  'photo.zoom_100': 'Actual size (100%)',

  // LAN cast
  'cast.start': 'Cast to device',
  'cast.stop': 'Stop casting',
  'cast.copy_url': 'Copy stream URL',
  'cast.copied': 'URL copied',
  'cast.scanning': 'Scanning for devices…',
  'cast.no_devices': 'No devices found',

  // Settings
  'settings.title': 'Settings',
  'settings.coming_soon': 'Coming soon.',
  'settings.downloads': 'Downloads',
  'settings.dl_auto': 'Save automatically',
  'settings.dl_auto_hint': 'Files go straight into the folder you choose below, '
    + 'with no dialog. Downloading twenty files puts twenty files there.',
  'settings.dl_ask': 'Ask every time',
  'settings.dl_ask_hint': 'A Save As dialog for each file — one per file, '
    + 'including when you download a selection.',
  'settings.dl_folder': 'Folder: {name}',
  'settings.dl_no_folder': 'No folder chosen — downloads go wherever your browser '
    + 'puts them, and they cannot be paused',
  'settings.dl_choose': 'Choose folder',
  'settings.dl_change': 'Change',
  'settings.dl_forget': 'Forget',
  'settings.profile': 'Profile',
  'settings.username': 'Username',
  'settings.email': 'Email',
  'settings.email_save': 'Save',
  'settings.email_saved': 'Email updated',
  'settings.email_code_sent': 'A verification code has been sent to your new email address.',
  'settings.email_code_placeholder': 'Verification code',
  'settings.email_verify_btn': 'Verify',
  'settings.email_verifying': 'Verifying...',
  'settings.email_verified': 'Email address changed successfully.',
  'settings.notif_global_disable': 'Disable all notifications',
  'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
  'settings.defaults': 'Defaults',
  'settings.default_tab': 'Default tab',
  'settings.default_tab_hint': 'Which tab opens first when you enter a group.',
  'settings.media_page_size': 'Items per page in Videos and Music',
  'settings.media_page_size_hint': 'How many posters or albums are shown at once; the arrows in the toolbar move to the next batch.',
  'settings.music_keep_screen_on': 'Keep screen on during music playback',
  'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing.',
  'settings.node_pins': 'Node identities',
  'settings.node_pins_hint': "Each node's identity key is remembered the first time you connect. If it changes, the connection is refused — that is expected only when an operator reinstalls a node. Verify with them before clearing.",
  'settings.node_pins_count': {
    one: '{n} pinned',
    other: '{n} pinned',
  },
  'settings.node_pins_clear': 'Clear pinned identities',
  'settings.passphrase': 'Passphrase',
  'settings.passphrase_hint': "Change this account's passphrase. Your identity keys are re-wrapped on every group whose node is online; a group whose node is offline has to be rejoined afterwards.",
  'settings.passphrase_change': 'Change passphrase',
  'settings.passphrase_current': 'Current passphrase',
  'settings.passphrase_new': 'New passphrase',
  'settings.passphrase_new_repeat': 'Repeat new passphrase',
  'settings.continue': 'Continue',
  'settings.done': 'Done',
  'settings.passphrase_confirm_intro': 'Your identity keys will be re-wrapped now on every group whose node can be reached:',
  'settings.passphrase_reachable': 'Updated now',
  'settings.passphrase_unreachable': 'Cannot be reached',
  'settings.passphrase_fallback_note': 'For a group that cannot be reached, ask its operator to unpin you and send a fresh invitation code, then rejoin. Your files and messages are not lost.',
  'settings.passphrase_confirm_btn': 'Change it',
  'settings.passphrase_working': 'Re-wrapping your keys…',
  'settings.passphrase_done': 'Passphrase changed.',
  'settings.passphrase_needs_operator': 'These groups still need action — ask each operator to unpin you and send a new code:',
  'settings.pw_too_short': 'The new passphrase must be at least 12 characters.',
  'settings.pw_mismatch': 'The two new passphrases do not match.',
  'settings.pw_same': 'The new passphrase must differ from the current one.',
  'settings.pw_wrong_current': 'The current passphrase is not correct. Nothing was changed.',
  'settings.recovery': 'Recovery key',
  'settings.recovery_hint': 'Add a recovery-wrapped copy of your identity to every group. Do this once in each browser you use; without it, a lost passphrase means rejoining each group by hand.',
  'settings.recovery_loaded': 'Loaded in this browser — new groups are covered automatically.',
  'settings.recovery_open': 'Enter recovery key',
  'settings.recovery_input_ph': 'Paste your recovery key',
  'settings.recovery_submit': 'Add backup copies',
  'settings.recovery_working': 'Updating your groups…',
  'settings.recovery_done': 'Your groups now have a recovery copy.',
  'settings.recovery_partial': 'These groups could not be reached. Open them later, or run this again:',
  'settings.recovery_need_relogin': 'Please sign out and back in, then try again.',
  'settings.danger': 'Delete account',
  'settings.sessions_heading': 'Sessions',
  'settings.sign_out_everywhere': 'Sign out everywhere',
  'settings.sign_out_everywhere_hint': 'Ends every session of your account, including this one: useful if you stayed signed in on a computer that is not yours. A desktop application signs back in with its device key until you remove that device.',
  'settings.sign_out_everywhere_confirm': 'Sign out of every browser and application, including this one?',
  'settings.delete_hint': 'Removes your account, your group memberships and your '
    + 'notifications from the hub, and frees your username. It cannot reach what '
    + 'lives on nodes: files you uploaded stay where their operator keeps them, '
    + 'and each node keeps the identity it pinned for you until its operator '
    + 'unpins it. Connection logs are kept for a year, as the law requires.',
  'settings.delete_account': 'Delete my account',
  'settings.delete_confirm': 'Enter your passphrase to confirm. This cannot be undone.',
  'settings.delete_confirm_btn': 'Delete for good',
  'settings.cancel': 'Cancel',
  'settings.appearance': 'Appearance',
  'settings.theme': 'Theme',
  'settings.theme_light': 'Light',
  'settings.theme_dark': 'Dark',
  'settings.theme_system': 'System',
  'settings.language': 'Language',
  'settings.about': 'About',
  'firstrun.title': 'Which hub?',
  'firstrun.hint': 'A hub holds your account and introduces you to nodes. It never sees your files, your messages or your keys.',
  'firstrun.btn': 'Continue',
  'firstrun.checking': 'Checking…',
  'firstrun.note': 'There is no default on purpose: this application only trusts a hub for its API, never for the interface, which ships with the application itself.',
  'device.this_device': 'This device',
  'settings.keys_heading': 'Keys on this device',
  'settings.hub_heading': 'Hub',
  'settings.hub_current': 'Currently',
  'settings.hub_hint': 'Changing this signs you out and restarts the window.',
  'settings.hub_change': 'Change',
  'settings.keys_where': 'Protected by',
  'settings.keys_unprotected': 'No system keyring is running, so your keys are encrypted with a key that is not a secret. Anyone who can read this machine’s files can read them. Start a keyring, or treat this device as untrusted.',
  'settings.keys_unavailable': 'This system offers no key storage at all, so this device cannot be remembered — you will be asked for your passphrase each time. That is the safe outcome: nothing was stored unprotected.',
  'settings.version': 'Version',
  'settings.protocol': 'Protocol',
  'settings.role': 'Role',
  'settings.groups': 'Group notifications',
  'settings.notifications': 'Notifications',
  'settings.node_key': 'Link Node',
  'settings.node_key_desc': 'If you operate a MeshBay node, paste its Ed25519 public key here to link it to your account. You can find this key in your node\'s admin dashboard.',
  'settings.node_key_placeholder': 'Paste node Ed25519 public key (base64)',
  'settings.node_key_submit': 'Link Node',
  'settings.node_key_success': 'Node linked successfully',
  'settings.node_key_current': 'Linked node key',

  // Sidebar
  'sidebar.create_group': 'Create group',
  'sidebar.settings': 'Settings',
  'sidebar.admin': 'Admin',

  // User menu
  'usermenu.language': 'Language',
  'usermenu.settings': 'Settings',
  'usermenu.theme_light': 'Light mode',
  'usermenu.theme_dark': 'Dark mode',
  'usermenu.logout': 'Logout',

  // Admin panel
  'admin.title': 'Administration',
  'admin.tab_stats': 'Stats',
  'admin.tab_users': 'Users',
  'admin.tab_groups': 'Groups',
  'admin.tab_nodes': 'Nodes',
  'admin.tab_logs': 'Logs',
  'admin.col_observed_ip': 'Seen from',
  'admin.col_hint': 'Announced hint',
  'admin.col_last_seen': 'Last announce',
  'admin.node_online': 'online',
  'admin.node_offline': 'offline',
  'admin.no_nodes': 'No nodes registered',
  'admin.nodes_hint': '"Seen from" is the address the announcement arrived from, '
    + 'on a request signed with the node key — that is the one to answer a '
    + 'question with. The hint is what the node believes its own address to be, '
    + 'learned from a STUN server and sent to us; it is useful for reaching the '
    + 'node and is not evidence of anything.',
  'admin.tab_blocklist': 'Blocklist',
  'admin.tab_general': "General",
  'admin.general_groups_heading': "Groups",
  'admin.allow_public_groups_label': "Allow members to create public groups",
  'admin.allow_public_groups_hint': "When off, new groups can only be private and invite-only. Existing public groups are left as they are — suspend them one by one from the Groups tab.",
  'admin.mail_heading': "Outgoing mail",
  'admin.mail_hint': "What this hub will send, and how much. The two that matter are the hourly budget, which no number of accounts can raise, and the daily cap per recipient, which is what someone being flooded actually experiences.",
  'admin.mail_this_hour': "Sent this hour",
  'admin.mail_remaining': "{general} left for sign-ups and address changes, {recovery} for passphrase resets and invitations.",
  'admin.mail_hourly_budget': "Messages per hour (whole instance)",
  'admin.mail_hourly_reserved_for_recovery': "Of those, reserved for resets and invitations",
  'admin.mail_destination_daily_cap': "Messages per day to one recipient",
  'admin.mail_destination_cooldown_seconds': "Seconds between messages to one recipient",
  'admin.mail_verification_resend_cooldown': "Seconds between two sign-up codes",
  'admin.mail_reset_cooldown': "Seconds between two passphrase-reset codes",
  'admin.mail_email_change_cooldown': "Seconds before an account may propose another address",
  'admin.mail_save': "Save mail limits",
  'admin.mail_reset_defaults': "Restore defaults",
  'admin.login_heading': "Sign-in",
  'admin.login_hint': "After this many wrong passphrases for one username, signing in with a passphrase is refused for the set duration. Sessions already open and registered devices keep working, and a passphrase reset ends the lockout. 0 turns it off.",
  'admin.session_heading': 'Sessions',
  'admin.session_hint': 'A browser signs itself out after the first number of hours with no input and nothing playing; the desktop application does not. A session unused for the second number stops renewing, and none lasts past the third.',
  'admin.session_browser_idle_hours': 'Browser sign-out after inactivity (hours)',
  'admin.session_refresh_idle_hours': 'Session expires when unused for (hours)',
  'admin.session_max_hours': 'Maximum session length (hours)',
  'admin.session_save': 'Save session settings',
  'admin.login_max_failures': "Wrong passphrases before a lockout",
  'admin.login_lockout_minutes': "Lockout duration (minutes)",
  'admin.login_save': "Save sign-in limits",
  'admin.mail_state_is_in_stats': "The current hour's usage is shown under Statistics.",
  'admin.mail_state_hint': "Resets and invitations may spend the whole budget; sign-ups and address changes may not spend the share reserved for them. Administrators are notified once per hour when either ceiling is reached.",
  'admin.mail_left_signups': "Left for sign-ups",
  'admin.mail_left_recovery': "Left for resets and invitations",
  'admin.mail_recipients': "Recipients counted today",
  'admin.mail_alert_all': "This hub has stopped sending mail for this hour. Passphrase resets and invitations are refused too.",
  'admin.mail_alert_general': "The share for sign-ups and address changes is spent for this hour. Passphrase resets and invitations still go out.",
  'admin.settings_readonly': "Only an admin can change these settings.",

  // Admin stats
  'admin.stat_users': 'Users',
  'admin.stat_groups': 'Groups',
  'admin.stat_nodes': 'Nodes',
  'admin.stat_online': 'Online Nodes',

  // Admin users
  'admin.users_search': 'Search users...',
  'admin.col_username': 'Username',
  'admin.col_role': 'Role',
  'admin.col_status': 'Status',
  'admin.col_created': 'Created',
  'admin.col_actions': 'Actions',
  'admin.col_email': 'Email',
  'admin.col_groups': 'Groups',
  'admin.no_users': 'No users found',
  'admin.btn_suspend': 'Suspend',
  'admin.btn_unsuspend': 'Unsuspend',
  'admin.btn_revoke': "Revoke",
  'admin.revoke_group_confirm': "Revoke the group \"{group}\"? This pushes a signed revocation to every node hosting it and cannot be undone from here. Suspend it instead if you only need to pause it.",
  'admin.btn_details': 'Details',
  'admin.btn_delete': 'Delete',
  'admin.delete_confirm': 'Delete the account "{user}" and every group it owns? This cannot be undone. '
    + 'Files they uploaded stay on the nodes that host them, and each node keeps '
    + 'the identity it pinned until its operator unpins it. Suspending is the '
    + 'reversible option.',
  'admin.user_detail': 'User details',
  'admin.btn_close': 'Close',
  'admin.self_note': '(you)',

  // Admin groups
  'admin.col_name': 'Name',
  'admin.col_visibility': 'Visibility',
  'admin.col_members': 'Members',
  'admin.no_groups': 'No groups found',

  // Admin logs
  'admin.col_time': 'Time',
  'admin.col_event': 'Event',
  'admin.col_user': 'User',
  'admin.col_ip': 'IP',
  'admin.col_detail': 'Detail',
  'admin.filter_all': 'All events',
  'admin.no_logs': 'No logs found',
  'admin.btn_load_more': 'Load more',

  // Admin blocklist
  'admin.col_hash': 'Hash',
  'admin.col_reason': 'Reason',
  'admin.col_date': 'Date',
  'admin.col_added_by': 'Added by',
  'admin.no_blocked': 'No blocked hashes',
  'admin.add_hash': 'Add hash to blocklist',
  'admin.hash_placeholder': 'blake3 hash (64 hex chars)',
  'admin.reason_placeholder': 'Reason',
  'admin.btn_block': 'Block',
  'admin.btn_unblock': 'Unblock',

  // Notifications
  // Create group
  'create_group.title': 'Create Group',
  'create_group.name': 'Group name',
  'create_group.name_placeholder': 'e.g. Family Photos, Project Files...',
  'create_group.join_policy': 'Join policy',
  'create_group.invite': 'Invite only',
  'create_group.invite_desc': "Members must be invited by an admin. The group is not listed publicly.",
  'create_group.open': 'Open (anyone can join)',
  'create_group.open_desc': "Listed in public groups — anyone can find it and join without approval.",
  'create_group.description': 'Description',
  'create_group.description_hint': 'What is this group about? (optional)',
  'create_group.submit': 'Create',
  'create_group.hint': 'A group needs a node to host files. You can create the group now and connect a node later.',
  'create_group.creating': 'Creating...',

  // Create Group Wizard (Electron-only)
  'wizard.title': 'Create Group',
  'wizard.detecting': 'Detecting local node...',
  'wizard.node_not_found': 'No local node detected. Make sure meshbay-node is running.',
  'wizard.retry': 'Retry',
  'wizard.wrong_account': 'The node is running but its configured username does not match your account. Check hub.username in node.toml.',
  'wizard.skip_node': 'Continue without node',
  'wizard.node_offline_warning': 'Node is not running. The group will be created on the hub only. You can connect the node later from the Node page.',
  'wizard.directories': 'Shared directories',
  'wizard.directories_hint': 'Choose the directories this group will share. At least one is required.',
  'wizard.add_directory': 'Add directory',
  'wizard.upload_target': 'upload target',
  'wizard.set_upload': 'Set as upload target',
  'wizard.remove': 'Remove',
  'wizard.create_and_setup': 'Create and set up',
  'wizard.create_hub_only': 'Create on hub only',
  'wizard.setting_up': 'Setting up your group...',
  'wizard.step_create_hub': 'Creating group on hub',
  'wizard.step_attach': 'Attaching to node',
  'wizard.step_apps': 'Choosing applications',
  'wizard.step_add_roots': 'Adding directories',
  'wizard.step_gek': 'Initializing encryption key',
  'wizard.step_pair': 'Setting up pairing',
  'wizard.step_index': 'Indexing files',
  'wizard.indexing_progress': 'Indexing… {pct}%',
  'wizard.indexing_current_dir': 'Scanning: {dir}',
  'wizard.finish_later': 'Finish setup later',
  'wizard.done_title': 'Group created',
  'wizard.done_message': 'Your group is ready — your node is hosting it and encryption is set up. Now invite people to it from the Settings tab.',
  'wizard.go_to_group': 'Go to the group',

  // First-run setup wizard (Electron-only)
  'setup.welcome_title': 'Welcome to MeshBay',
  'setup.welcome_message': 'Your node hosts your files — encrypted, on your machine. Let’s get it running and create your first group.',
  'setup.node_detected': 'Local node detected and running.',
  'setup.node_not_running': 'Your node is installed but not running.',
  'setup.node_start': 'Start node',
  'setup.node_starting': 'Starting node…',
  'setup.node_started': 'Node started.',
  'setup.node_not_installed': 'meshbay-node is not installed on this machine.',
  'setup.node_install_hint': 'Install it with your package manager, then come back here.',
  'setup.node_startup_hint': 'Once your node is running, visit “Node” in the sidebar any time to have it start automatically at sign-in, or run as a background service.',
  'setup.skip': 'Skip — create a group without a local node',
  'setup.create_group': 'Create your first group',
  'setup.dismiss': 'Skip setup',

  // Unified Group Settings (node sections)
  'settings_node.roots': 'Shared directories',
  'settings_node.detach_failed_continue': 'Could not detach the group from the node. Delete on hub anyway?',
  'settings_node.scan_title': 'Scanning',
  'settings_node.scan_hint': 'How often the node re-checks its directories for changes it may have missed, and how long it waits after a file changes before indexing it.',
  'settings_node.search_listed_title': 'Search',
  'settings_node.search_listed_label': 'List this group in Search',
  'settings_node.search_listed_hint': 'When off, this group\'s files do not appear in members\' cross-group Search. Members still see everything by opening the group: this keeps it out of the global listing and hides nothing from anyone.',
  'settings_node.scan_reconcile_label': 'Re-check interval (minutes)',
  'settings_node.scan_debounce_label': 'Wait after a change (seconds)',
  'settings_node.scan_save': 'Save',
  'settings_node.scan_saving': 'Saving…',
  'settings_node.scan_saved': 'Saved.',
  'settings_node.tmdb_title': 'TMDB metadata',
  'settings_node.tmdb_hint': 'Lets the Videos app show posters, overviews and cast from TMDB. Off means thumbnail-only browsing, with no request to a third party.',
  'settings_node.tmdb_enabled': 'Enabled',
  'settings_node.tmdb_disabled': 'Disabled',
  'settings_node.tmdb_token_label': 'Custom API token (optional)',
  'settings_node.tmdb_token_placeholder': 'Leave blank to use the shipped default',
  'settings_node.tmdb_token_customized': 'A custom token is set.',
  'settings_node.tmdb_token_default': 'Using the shipped default token.',
  'settings_node.tmdb_save': 'Save',
  'settings_node.tmdb_language_label': 'Language',
  'settings_node.tmdb_language_hint': 'Applies to everyone — one shared cache, not a per-viewer request.',
  'settings_node.video_root_title': 'Videos root folder',
  'settings_node.video_root_hint': 'Which folder (or subfolder) the Videos app treats as its entry point for this group. Nothing shows in Videos, and no TMDB lookups run, until one is chosen.',
  'settings_node.video_root_none': '— none chosen —',
  'settings_node.musicbrainz_title': 'MusicBrainz metadata',
  'settings_node.musicbrainz_hint': 'Lets the Music app show cover art and canonical naming from MusicBrainz when a track has no usable embedded cover. Off means tag/filename-only browsing, with no request to a third party.',
  'settings_node.musicbrainz_enabled': 'Enabled',
  'settings_node.musicbrainz_disabled': 'Disabled',
  'settings_node.video_root_save': 'Save',
  'settings_node.video_root_change_confirm': 'Changing the Videos root replaces what every member sees in the Videos tab. Continue?',

  'settings_node.audio_root_title': 'Music root folder',
  'settings_node.audio_root_hint': 'Which folder (or subfolder) the Music app treats as its entry point for this group. Nothing shows in Music, and no tag/cover reading runs, until one is chosen.',
  'settings_node.audio_root_none': '— none chosen —',
  'settings_node.audio_root_save': 'Save',
  'settings_node.audio_root_change_confirm': 'Changing the Music root replaces what every member sees in the Music tab. Continue?',
  'settings_node.photo_roots_title': 'Photos root folders',
  'settings_node.photo_roots_hint': 'Which folder(s) the Photos app treats as entry points for this group — a photo library is often scattered across several folders, so more than one may be chosen. Nothing shows in Photos until at least one is added.',
  'settings_node.photo_roots_none': '— none chosen —',
  'settings_node.photo_roots_add_placeholder': 'Add a folder…',
  'settings_node.photo_roots_add': 'Add',
  'settings_node.photo_roots_remove': 'Remove',
  'settings_node.photo_roots_save': 'Save',
  'settings_node.shared_directories_title': 'Shared directories',
  'settings_node.shared_directories_hint': 'Folders shared with this group. Toggle read-write to allow uploads, mark as removable for external drives.',
  'folder_tree.title_single': 'Choose a folder',
  'folder_tree.title_multi': 'Choose folders',
  'folder_tree.choose': 'Choose…',
  'folder_tree.remove': 'Remove',
  'folder_tree.add': 'Add a folder…',
  'folder_tree.confirm': 'Use these',
  'folder_tree.expand': 'Expand',
  'folder_tree.collapse': 'Collapse',
  'folder_tree.nothing_selected': 'Nothing selected',
  'folder_tree.empty': 'This group has no shared directories yet.',
  'folder_tree.writable_only': 'Only read-write directories can be chosen here — files are written to this one.',
  'folder_tree.no_writable_root': 'This group has no read-write directory. Make one read-write in Shared directories first.',
  'folder_tree.read_only_blocked': 'Read-only — cannot be written to',
  'settings_app.save': 'Save',
  'settings_app.saving': 'Saving…',
  'settings_app.disabled_hint': 'Turn this app on to configure it.',
  'settings_app.video_directories_label': 'Video folders',
  'settings_app.video_directories_hint': 'Where this group\'s films and shows live. Nothing outside them appears in the Videos tab.',
  'settings_app.music_directories_label': 'Music folders',
  'settings_app.music_directories_hint': 'Where this group\'s albums live. Nothing outside them appears in the Music tab.',
  'settings_app.photo_directories_label': 'Photo folders',
  'settings_app.photo_directories_hint': 'Where this group\'s albums live. Nothing outside them appears in the Photos tab.',
  'settings_app.chat_directory_label': 'Attachment folder',
  'settings_app.chat_directory_hint': 'Where files sent in chat are written. Must be a read-write directory.',
  'settings_app.chat_no_writable_root': 'This group has no read-write directory, so attachments are off.',
  'settings_app.chat_link_preview_label': 'Link previews',
  'settings_app.chat_link_preview_hint': 'When a member posts a link, the node fetches the page\'s title and image. That is a request from your machine to a site somebody else chose.',
  'settings_app.chat_encrypted_always': 'Chat in this group is always encrypted. It cannot be turned off.',
  'settings_app.chat_rotate_epoch': 'Move the chat key on',
  'settings_app.chat_rotate_epoch_hint': 'Members keep reading the whole history. Anyone removed from the group cannot read what is written from now on. Removing a member or a device already does this by itself.',
  'settings_app.tmdb_token_prompt': 'Sign up on TMDB to generate your own API key.',
  'settings_app.tmdb_token_link': 'Get a key',
  'settings_node.roots_offline_hint': 'Not connected to the node — changes go through the local node instead, and take effect on its next reload.',
  'settings_node.directories_title': 'App directories',
  'settings_node.directories_hint': 'Which shared folders the Videos, Music and Photos apps use as their entry point(s).',

  // Members
  'members.col_role': 'Role',
  'members.group_role': 'Group role',
  'members.admin': 'Admin',
  'members.group_admin': 'Group admin',
  'members.owner': 'Owner',
  'members.member': 'Member',
  'members.invite_title': 'Invite member',
  'members.username_placeholder': 'Username',
  'members.invite_btn': 'Invite',
  'members.invite_needs_pairing': 'Invitations are signed with the key this node '
    + 'pinned for your browser, so pair it below before inviting anyone.',
  'members.invite_ask_operator': 'Only the operator of the node hosting this group can '
    + 'invite, from a browser paired with it.',
  'members.remove': 'Remove',
  'members.remove_confirm': 'Remove {user} from this group? Their account, their '
    + 'other groups and the files they uploaded are not affected.',
  'members.remove_hint': 'Removing someone takes away their membership and stops '
    + 'the node serving them the group key. It does not delete their account, and '
    + 'it does not take back the key they already hold — rotate it on the node '
    + '(meshbay-node gek-init) if that matters.',
  'members.pair_title': 'Pair this browser with your node',
  'members.pair_hint': 'Your node only accepts operator actions — invites, file '
    + 'deletion — from a browser it has been paired with. Run '
    + '`meshbay-node operator pair` on the node and type the code here. The code '
    + 'never passes through the hub.',
  'members.pair_btn': 'Pair',
  'members.pair_success': 'This browser is now paired with the node.',
  'members.invite_code_ready': 'Invitation code for {user}:',
  'members.invite_email_sent': 'An email with the code has been sent to this member.',
  'members.invite_email_failed': 'Could not send the email — share the code manually.',
  'members.invite_code_hint': 'You can also share this code via another channel (e.g. SMS). '
    + 'They enter it the first time they open this group.',
  'transfers.title': 'Transfers',
  'transfers.clear': 'Clear finished',
  'transfers.cancel': 'Cancel',
  'transfers.open': 'Open',
  'transfers.done': 'Finished',
  'transfers.cancelled': 'Cancelled',
  'transfers.preparing': 'Preparing…',
  'transfers.waiting_own_slots': 'Waiting — your slots are busy',
  'transfers.waiting_node': 'Waiting — {n} ahead',
  'transfers.summary': '{running} running · {waiting} waiting',
  'transfers.group_running': 'Running',
  'transfers.group_waiting': 'Waiting',
  'transfers.group_paused': 'Paused',
  'transfers.group_finished': 'Finished',
  'transfers.cancel_one': 'Cancel {name}',
  'transfers.pause': 'Pause',
  'transfers.resume': 'Resume',
  'transfers.paused': 'Paused',
  'transfers.not_pausable': 'Cannot be paused — choose a download folder in Settings to enable it',
  'transfers.pause_one': 'Pause {name}',
  'transfers.resume_one': 'Resume {name}',
  'transfers.eta_seconds': '{n}s left',
  'transfers.eta_minutes': '{n} min left',
  'transfers.eta_hours': '{n} h left',
  'transfers.failed': 'Failed',
  'group.select': 'Select',
  'group.select_done': 'Done',
  'group.actions': 'Actions ({n})',
  'group.download_n': 'Download ({n})',
  'group.download_zip_n': 'Download folders as zip ({n})',
  'group.delete_n': 'Delete ({n})',
  'group.delete_n_confirm': {
    one: 'Delete {n} item? {names}',
    other: 'Delete {n} items? {names}',
  },
  'group.download_zip': {
    one: 'Download as zip ({n} file)',
    other: 'Download as zip ({n} files)',
  },
  'group.zip_empty': 'That folder has nothing in it to download.',
  'group.zip_no_stream': 'This browser cannot write a download straight to disk, '
    + 'so {name} ({size}) has to be built in memory first. On a large archive '
    + 'that may fail. Continue?',
  'group.zip_too_large': '"{name}" is {size}, and a folder can only be downloaded as a zip up to '
    + '{limit}. Take a subfolder at a time, or the files individually.',
  'group.rmdir': 'Delete folder',
  'group.rmdir_confirm': 'Delete the folder "{name}"? It must be empty.',
  'group.rmdir_not_empty': 'That folder is not empty. Delete what is in it first — '
    + 'nothing here removes files you cannot see.',
  'group.desc_edit': 'Edit description',
  'group.desc_add': 'Add a description',
  'group.desc_save': 'Save',
  'group.desc_cancel': 'Cancel',
  'group.desc_placeholder': 'What this group is for — members see it on their home page.',
  'group.join_code_title': 'This node needs to recognise you',
  'group.join_code_hint': 'Ask whoever invited you for the one-time code, and enter '
    + 'it here. After that this browser is recognised and you will not be asked again.',
  'group.join_code_btn': 'Join',
  'group.pass_title': 'Enter your passphrase',
  'group.pass_hint': 'This browser has not unlocked your keys yet. Enter your passphrase once to open your groups here.',
  'group.pass_btn': 'Unlock',

  'notif.purge': 'Clear all',
  'notif.title': 'Notifications',
  'notif.empty': 'No notifications',
  'notif.mark_all_read': 'Mark all read',

  // Sidebar
  'sidebar.search': 'Search files',

  // Status

  // Search
  'search.placeholder': 'Search across all groups...',
  'search.no_results': 'No files match your search.',
  'search.col_group': 'Group',
  'search.result_count': { one: '{n} file found', other: '{n} files found' },
  'search.hint': 'Type to search file names across all your groups.',
  'search.indexing': 'Indexing {done} / {total} groups...',
  'search.unreachable': { one: '{n} group unreachable', other: '{n} groups unreachable' },
  'search.n_sources': { one: '{n} source', other: '{n} sources' },
  'search.view_files': 'Files',
  'search.view_videos': 'Videos',
  'search.view_music': 'Music',
  'search.view_photos': 'Photos',
  'search.connecting': 'Connecting to groups...',

  // Presence, chat paging, leaving a group, the profile page
  'presence.online': 'A node serving this group is online',
  'presence.offline': 'No node is reachable for this group',
  'presence.unknown': 'Not checked yet',
  'presence.indexing': 'Indexing — {pct}% done',
  'indexdock.kind_scan': "Indexing",
  'indexdock.kind_rescan': "Re-reading",
  'indexdock.kind_reconcile': "Catching up",
  'indexdock.kind_watch': "Indexing new files",
  'indexdock.waiting': "Waiting to index",
  'indexdock.done': "Indexing finished",
  'indexdock.listing': "Listing files…",
  'indexdock.percent': "{pct}%",
  'indexdock.files': "{done} / {total} files",
  'indexdock.next': "Next: {roots}",
  'indexdock.next_count': "{count} more waiting",
  'indexdock.open': "Open this group's settings",
  'indexdock.hide': "Hide until the next indexing",
  'chat.load_older': 'Load {n} older messages',
  'chat.start_of_history': 'Beginning of the conversation',
  'chat.today': 'Today',
  'chat.yesterday': 'Yesterday',
  'chat.unread': 'New messages',
  'chat.jump_latest': 'Jump to latest',
  'chat.jump_new': 'New messages',
  // A message this client could not open. Each one names what actually went
  // wrong, because "could not read this message" sends an operator nowhere:
  // a signature failure is somebody impersonating a member, a missing epoch key
  // is a device that joined after the message was written, and a decryption
  // failure is neither.
  'chat.unreadable': 'This message could not be read',
  'chat.unreadable_signature': 'Unverified message — the signature does not match the sender',
  'chat.unreadable_epoch': 'Written before this device could read this conversation',
  'chat.unreadable_keys': 'Chat keys unavailable — reconnect to read this',
  'chat.unreadable_decrypt': 'This message could not be decrypted',
  'chat.unreadable_envelope': 'This message arrived incomplete',
  'chat.unreadable_format': 'This message needs a newer version of MeshBay',
  'chat.key_changed': 'This account is using a key you have not seen before',
  'chat.key_changed_hint': 'The node says this message came from this account, but its device key is not one you have accepted before and nothing signed it into place. It may be a new device added elsewhere — or the node naming the wrong person.',
  'chat.encrypted_needs_newer': 'This conversation is encrypted and this client cannot read it — update MeshBay',
  'chat.encrypted_cannot_send': 'This conversation is encrypted and this device cannot post to it yet',
  'group.leave': 'Leave group',
  'group.leave_confirm': 'Leave “{name}”? You will lose access to its files and chat. Files you uploaded stay on the node, and the node keeps the identity it pinned for you until its operator removes it.',
  'group.mute': 'Mute notifications',
  'group.unmute': 'Unmute notifications',
  'group.retry': 'Retry',
  'profile.title': 'Profile',
  'usermenu.profile': 'Profile',

  // Node management (D5)
  'sidebar.node': 'Node',
  'node.title': 'Node',
  'node.service_checking': 'Checking node service…',
  'node.service_not_installed': 'Not installed as a service',
  'node.service_state_active': 'Running',
  'node.service_state_inactive': 'Stopped',
  'node.service_state_failed': 'Failed',
  'node.service_state_activating': 'Starting…',
  'node.service_state_deactivating': 'Stopping…',
  'node.service_state_unknown': 'Unknown',
  'node.service_start': 'Start',
  'node.service_starting': 'Starting…',
  'node.service_stop': 'Stop',
  'node.service_stopping': 'Stopping…',
  'node.service_restart': 'Restart',
  'node.service_restarting': 'Restarting…',
  'node.service_mode_hint': 'Running as a background service — it starts at boot, before sign-in.',
  'node.startup_mode_label': 'Start automatically:',
  'node.startup_mode_off': 'Off (start manually)',
  'node.startup_mode_signin': 'At sign-in',
  'node.startup_mode_service': 'As a background service (starts at boot)',
  'node.startup_mode_updating': 'Switching mode — check for an administrator prompt…',
  'node.startup_mode_service_unavailable_hint': 'Background service mode needs an installed build. For local testing, run "meshbay-node service install" from an elevated PowerShell.',
  'node.offline': 'Node is offline',
  'node.no_groups': 'No groups configured on this node.',
  'node.retry': 'Retry',
  'node.files': { one: '1 file', other: '{n} files' },
  'node.peers': { one: '1 peer', other: '{n} peers' },
  'node.no_gek': 'No group key',
  'node.roots': 'Directories',
  'node.directory': 'Directory',
  'node.root_rw': 'Writable',
  'node.root_ro': 'read-only',
  'node.removable': 'Removable',
  'node.unavailable': 'unavailable',
  'node.add_root': 'Add directory',
  'node.remove_root': 'Remove',
  'node.root_no_signing_key': 'No signing key available — pair this device with the node first',
  'node.root_no_route': 'No route to the node — connect to it, or use the app on the machine hosting it',
  'node.root_remove_last': 'A group needs at least one directory',
  'node.root_path_hint': 'The path as the node sees it, on the machine hosting this group.',
  'node.root_path_placeholder': '/home/user/Media',
  'node.root_path': 'Path',
  'node.root_added': 'Directory added.',
  'node.root_removed': 'Directory removed. Restart recommended to update the index.',
  'node.root_remove_confirm': 'Remove "{name}" from this group?',
  'node.not_operator': 'Could not reach your node. Make sure it is running.',
  'node.gek': 'Group key',
  'node.gek_rotate': 'Rotate group key',
  'node.gek_rotate_confirm': 'Rotate the group key? Connected members will receive the new key automatically. Content already downloaded is unaffected.',
  'node.gek_rotated': 'Group key rotated.',
  'node.gek_init_hint': 'No group key yet. Run GEK init from the CLI to set one up.',
  'node.gek_public_hint': 'Key rotation is not available for public groups.',
  'node.roster': 'Roster',
  'node.roster_load': 'Load roster',
  'node.roster_empty': 'No members pinned yet.',
  'node.roster_user': 'User',
  'node.roster_role': 'Role',
  'node.roster_status': 'Status',
  'node.roster_via': 'Pinned via',
  'node.roster_invites': { one: '1 pending invite', other: '{n} pending invites' },
  'node.unpin': 'Unpin',
  'node.unpin_confirm': 'Unpin "{name}"? They will need to pair again.',
  'node.unpin_done': 'Identity unpinned.',
  'node.denylist': 'Denylist',
  'node.denylist_load': 'Load denylist',
  'node.denylist_empty': 'Denylist is empty.',
  'node.denylist_count': { one: '1 entry', other: '{n} entries' },
  'node.denylist_remove': 'Remove',
  'node.denylist_clear_all': 'Clear all',
  'node.denylist_clear_confirm': 'Remove "{subject}" from the denylist?',
  'node.denylist_cleared': 'Denylist entry removed.',
  'node.pair_needed': 'No operator paired. Enter a pairing code to enable admin operations (invites, file deletion).',
  'node.pair_code_placeholder': 'Pairing code',
  'node.pair_button': 'Pair this browser',
  'node.pair_success': 'Paired successfully.',
  'node.reload': 'Reload config',
  'node.reloaded': 'Config reloaded. Root changes on existing groups are now active.',
  'node.attach_group': 'Add group',
  'node.attach_pick': 'Group to host',
  'node.attach_dir': 'Shared directory',
  'node.attach_upload_label': 'Upload directory (for file uploads and chat attachments)',
  'node.attach_upload_dir': 'Upload directory',
  'node.attached': 'Group added and loaded. Run meshbay-node gek-init --group {name} if not done yet.',
  'node.restart_needed': 'After adding: initialise the group key (meshbay-node gek-init --group <name>).',
  'node.detach_group': 'Remove group',
  'node.detach_confirm': 'Remove "{name}" from this node?',
  'node.detached': 'Group removed.',
  'node.stale': 'not on hub',
  'node.settings': 'Node settings',
  'node.settings_edit_hint': 'Changes are applied immediately and saved to node.toml.',
  'node.settings_save': 'Save',
  'node.settings_saving': 'Saving…',
  'node.settings_saved': 'Settings saved.',
  'node.setting_invite_ttl': 'Invitation TTL',
  'node.setting_pair_ttl': 'Pairing code TTL',
  'node.setting_device_ttl': 'Device request TTL',
  'node.setting_max_streams': 'Max concurrent streams',
  'node.setting_max_downloads': 'Max concurrent downloads',
  'node.setting_max_uploads': 'Max concurrent uploads',
  'node.setting_transcode': 'Transcode incompatible video',
  'node.setting_max_upload': 'Largest single upload',
  'node.setting_unit_gb': 'GB',
  'node.setting_unit_hours': 'hours',
  'node.setting_unit_minutes': 'min',
  'node.setting_on': 'On',
  'node.setting_off': 'Off',
  'node.stun_servers': 'STUN Servers',
  'node.stun_hint': 'Servers used for WebRTC NAT traversal (ICE candidate gathering).',
  'node.stun_add': 'Add',
  'node.stun_add_placeholder': 'stun:host:port',
  'node.stun_remove': 'Remove',
  'node.stun_move_up': 'Move up',
  'node.stun_move_down': 'Move down',
  'node.stun_save': 'Save',
  'node.stun_saving': 'Saving…',
  'node.stun_saved': 'STUN servers saved.',
  'node.stun_reset': 'Reset to defaults',
  'node.stun_empty': 'No STUN servers configured. NAT traversal will not work.',
  'node.stun_duplicate': 'This server is already in the list.',
  'node.stun_invalid': 'STUN URL must start with stun:',
  'node.ice_interfaces': 'ICE Interfaces',
  'node.ice_auto_hint': 'Auto mode — virtual and VPN interfaces (Tailscale, Docker, libvirt, etc.) are excluded automatically.',
  'node.ice_manual_hint': 'Manual mode — only the listed interfaces are used for ICE candidate gathering.',
  'node.ice_add': 'Add',
  'node.ice_add_placeholder': 'e.g. eth0, wlp0s20f3',
  'node.ice_remove': 'Remove',
  'node.ice_save': 'Save',
  'node.ice_saving': 'Saving…',
  'node.ice_saved': 'ICE interfaces saved.',
  'node.ice_reset_auto': 'Switch to auto',
  'node.ice_duplicate': 'This interface is already in the list.',
  'node.overview': 'Overview',
  'node.overview_version': 'Version',
  'node.overview_node_id': 'Node ID',
  'node.overview_quic_port': 'QUIC port',
  'node.overview_hub': 'Hub',
  'node.node_roster': 'Node roster',
  'node.node_roster_hint': 'Every identity this node recognises, across all groups.',
  'node.roster_scope': 'Scope',
  'node.roster_scope_node': 'node-wide',
  'node.peers_title': 'Connected peers',
  'node.peers_empty': 'No peers connected.',
  'node.peers_user': 'User',
  'node.peers_ip': 'IP',
  'node.peers_group': 'Group',
  'node.peers_state': 'State',
  'node.audit_title': 'Audit log',
  'node.audit_all_events': 'All events',
  'node.audit_empty': 'No audit entries.',
  'node.audit_time': 'Time',
  'node.audit_event': 'Event',
  'node.audit_user': 'User',
  'node.audit_ip': 'IP',
  'node.audit_group': 'Group',
  'node.audit_detail': 'Detail',
  'node.maintenance': 'Maintenance',
  'node.maintenance_cache': 'Index cache: {n} path(s) remembered.',
  'node.maintenance_prune': 'Prune stale entries',
  'node.maintenance_pruned': 'Pruned {n} stale cache entries.',
  'node.loading': 'Loading…',
  'node.audit_export': 'Export CSV',
  'node.audit_exporting': 'Exporting…',
  'node.audit_prev': 'Previous',
  'node.audit_next': 'Next',
  'node.audit_page': 'Page {n}',
  'node.audit_exported': 'Exported {n} entries.',
  'node.unlink_title': 'Unlink from hub',
  'node.unlink_hint': 'Stops the hub from brokering connections to this node and listing it for group members. Your groups, keys, roster and files are untouched — the node keeps serving anyone who reaches it directly. Re-link any time from the group wizard or your profile.',
  'node.unlink_button': 'Unlink this node',
  'node.unlink_confirm': 'Unlink this node from the hub? The hub will stop routing members to it. Group data on disk is not affected.',
  'node.unlink_done': 'Node unlinked from the hub. It may take a moment for the hub connection to drop.',
  'node.tab_overview': 'Overview',
  'node.tab_groups': 'Groups',
  'node.tab_roster': 'Roster',
  'node.tab_peers': 'Peers',
  'node.tab_audit': 'Audit',
  'node.tab_settings': 'Settings',
};