summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/i18n.js
blob: 25fbd774cded88b76ab10782e4609f0a1b3b4545 (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
/**
 * MeshBay i18n — lightweight string localization.
 *
 * Usage:
 *   import { t, setLocale, getLocale, LOCALES } from './i18n.js';
 *   t('nav.logout')              // "Logout"
 *   t('status.files', { n: 42 }) // "42 files"
 */

const LANG_KEY = 'mb_lang';

// ── English strings ─────────────────────────────────────────────────────────

const en = {
  // Nav
  'nav.toggle_menu': 'Toggle menu',
  'nav.light_mode': 'Light mode',
  'nav.dark_mode': 'Dark mode',
  'nav.logout': 'Logout',
  'nav.login': 'Login',

  // Sidebar
  'sidebar.my_groups': 'My Groups',
  'sidebar.no_groups': 'No groups yet',
  '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',

  // 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.success_title': 'Account created',
  'register.success_msg': 'You can now log in with your credentials.',
  'register.go_login': 'Go to login',
  'register.err_mismatch': 'Passwords do not match',
  'register.err_min_len': '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.',

  // 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.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',

  // Group page
  'group.default_name': 'Group',
  'group.tab_files': 'Files',
  'group.tab_chat': 'Chat',
  'group.tab_members': 'Members',
  'group.filter': 'Filter files...',
  '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.mkdir': 'New folder',
  'group.mkdir_prompt': 'Name of the new folder:',
  '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 via WebRTC...',
  'status.fetching': 'Fetching index...',
  'status.files': '{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',

  // Video player
  'video.loading': 'Loading {name}...',
  'video.buffering': 'Buffering...',
  'video.close': 'Close (Esc)',
  'preview.pdf_fallback': 'This browser will not display the PDF inline. Download it instead — it was decrypted here either way.',
  'group.upload_indexing': 'indexing…',
  'video.err_transport': 'Transport not connected',
  'video.err_mse': 'Codec not supported for streaming: {codec}',

  // 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',
  'settings.dl_choose': 'Choose folder',
  'settings.dl_change': 'Change',
  'settings.dl_forget': 'Forget',
  'settings.dl_path_note': 'A web page cannot be given a path, so there is nothing '
    + 'to type here: your browser grants access to the folder you pick, and MeshBay '
    + 'only ever writes inside it. You may be asked to confirm once per session.',
  'settings.dl_unsupported': 'This browser cannot write into a folder of your '
    + 'choosing (no File System Access API), so downloads go to its own download '
    + 'folder. Chrome and Edge support choosing one.',
  'settings.profile': 'Profile',
  'settings.username': 'Username',
  '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': '{n} pinned',
  'settings.node_pins_clear': 'Clear pinned identities',
  'settings.danger': 'Delete account',
  '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',
  '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_logs': 'Logs',
  'admin.tab_blocklist': 'Blocklist',

  // 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_details': 'Details',
  'admin.btn_delete': 'Delete',
  'admin.delete_confirm': 'Delete the account "{user}"? 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.visibility': 'Visibility',
  'create_group.private': 'Private',
  'create_group.public': 'Public',
  'create_group.join_policy': 'Join policy',
  'create_group.invite': 'Invite only',
  'create_group.open': 'Open (anyone can join)',
  '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...',

  // 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.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} — send it to them the way '
    + 'you normally talk. It works once, and it never passes through the hub.',
  'members.invite_code_hint': 'They enter it the first time they open this group. '
    + 'You do not need to be online then.',
  'transfers.title': 'Transfers',
  'transfers.clear': 'Clear finished',
  'transfers.cancel': 'Cancel',
  'transfers.done': 'Finished',
  'transfers.cancelled': 'Cancelled',
  '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': 'Delete {n} item(s)? {names}',
  'group.download_zip': '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.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',

  '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.title': 'Search Files',
  'search.placeholder': 'Search across all groups...',
  'search.no_results': 'No files match your search.',
  'search.synced': 'synced {ago}',
  'search.cache_note': 'Search reads what each group looked like the last time you '
    + 'opened it. Open a group to bring its files up to date here.',
  'search.col_group': 'Group',
  'search.result_count': '{n} files found',
  'search.hint': 'Search file names across all cached group indexes.',
};

// ── Locale registry ─────────────────────────────────────────────────────────

const _strings = { en };
let _locale = 'en';

export const LOCALES = [
  { code: 'en', name: 'English', flag: '\u{1F1EC}\u{1F1E7}' },
];

export function getLocale() { return _locale; }

export function setLocale(code) {
  if (_strings[code]) {
    _locale = code;
    localStorage.setItem(LANG_KEY, code);
    return true;
  }
  return false;
}

export function addLocale(code, strings) {
  _strings[code] = strings;
}

export function t(key, params) {
  let s = (_strings[_locale] && _strings[_locale][key])
    || _strings.en[key]
    || key;
  if (params) {
    for (const [k, v] of Object.entries(params)) {
      s = s.replace(`{${k}}`, v);
    }
  }
  return s;
}

// ── Init ────────────────────────────────────────────────────────────────────

function _init() {
  const stored = localStorage.getItem(LANG_KEY);
  if (stored && _strings[stored]) {
    _locale = stored;
    return;
  }
  const browserLang = (navigator.language || '').split('-')[0];
  if (_strings[browserLang]) {
    _locale = browserLang;
  }
}

_init();