Skip to content
Clone

feat(web/app): add shared libs, local-first cache and i18n

Carry over the local-first data hook, cache key conventions, theme/palette
store and the message catalogue. useAsync gains reload/reloadSilently/mutate so
event-driven updates can refresh data in place without a loading state.
ZhenYi committed
parent 0cf4fcc commit d3d60a5 Browse files

49 files changed

+7128 −0

File tree 49
Added view/app/src/i18n/messages/en/auth.ts View view/app/src/i18n/messages/en/auth.ts
@@ -0,0 +1,91 @@
1/** Authentication flow: shell chrome plus the six auth screens. */
2export const auth = {
3 'auth.shell.breadcrumb': 'Breadcrumb',
4 'auth.shell.switchToLight': 'Switch to light theme',
5 'auth.shell.switchToDark': 'Switch to dark theme',
6 'auth.shell.region': 'Region {region}',
7 'auth.login.crumb': 'Sign in',
8 'auth.login.hint': 'Up to 5 attempts per account every 5 minutes',
9 'auth.login.heading': 'Sign in to GitDataAI',
10 'auth.login.subheading': 'Repositories, boards and team chat in one place.',
11 'auth.login.identifier': 'Email or username',
12 'auth.login.passphrase': 'Password',
13 'auth.login.forgot': 'Forgot password?',
14 'auth.login.keepSignedIn': 'Keep me signed in',
15 'auth.login.keepSignedInAria': 'Keep me signed in',
16 'auth.login.submit': 'Sign in',
17 'auth.login.losingAccess': 'Trouble signing in?',
18 'auth.login.resendVerify': 'Resend verification email',
19 'auth.login.noAccount': 'No account yet?',
20 'auth.login.requestAccess': 'Create account',
21 'auth.forgot.crumb': 'Recover access',
22 'auth.forgot.hint': 'Reset links expire after 30 minutes',
23 'auth.forgot.heading': 'Forgot password?',
24 'auth.forgot.subheading':
25 'Enter your account email and we will send a single-use reset link. It expires after 30 minutes.',
26 'auth.forgot.email': 'Account email',
27 'auth.forgot.privacyNote':
28 'If the address is registered you will receive a reset email. For security we never reveal whether an account exists.',
29 'auth.forgot.submit': 'Send reset link',
30 'auth.forgot.remembered': 'Remembered it?',
31 'auth.forgot.backToSignIn': 'Back to sign in',
32 'auth.reset.crumb': 'Reset password',
33 'auth.reset.hint': 'Every signed-in device must sign in again',
34 'auth.reset.heading': 'Set a new password',
35 'auth.reset.subheading': 'Link verified. Choose your new password.',
36 'auth.reset.subheadingInvalid': 'Open the reset link from your email to continue.',
37 'auth.reset.noAddress': 'no address',
38 'auth.reset.tokenValid': 'Link valid',
39 'auth.reset.noToken': 'Link invalid',
40 'auth.reset.newPassword': 'New password',
41 'auth.reset.confirmPassword': 'Confirm new password',
42 'auth.reset.strength.strong': 'Strong',
43 'auth.reset.strength.fair': 'Fair',
44 'auth.reset.strength.weak': 'Weak',
45 'auth.reset.error.mismatch': 'The two passwords do not match.',
46 'auth.reset.noTokenNote': 'Open the reset link from your email, or',
47 'auth.reset.requestNewLink': 'request a new link',
48 'auth.reset.done': 'Password updated — returning to sign in…',
49 'auth.reset.submit': 'Update password',
50 'auth.verify.crumb': 'Verify email',
51 'auth.verify.hint': 'No email yet? Check your spam folder',
52 'auth.verify.heading': 'Check your inbox',
53 'auth.verify.subheading': 'Verification email sent to',
54 'auth.verify.resetSubheading': 'Reset email sent to',
55 'auth.verify.purpose': 'purpose · {purpose}',
56 'auth.verify.purpose.signup': 'sign-up',
57 'auth.verify.purpose.password_reset': 'password reset',
58 'auth.verify.purpose.add_email': 'add email',
59 'auth.verify.purpose.reclaim': 'account recovery',
60 'auth.verify.linkOpened': 'Link opened. Confirm below to continue.',
61 'auth.verify.linkExpiry': 'The code and link in the email expire in 10 minutes.',
62 'auth.verify.resetBody': 'Set a new password through the link in the email within 30 minutes.',
63 'auth.verify.verified': 'Email verified — returning to sign in…',
64 'auth.verify.resent': 'A new verification email was sent to {email}.',
65 'auth.verify.resetResent': 'A new reset email was sent to {email}.',
66 'auth.verify.waiting': 'Open the verification link in the email to continue.',
67 'auth.verify.confirm': 'Confirm verification',
68 'auth.verify.divider': 'Did not arrive?',
69 'auth.verify.resend': 'Resend verification email',
70 'auth.verify.resendCountdown': 'Resend verification email ({time})',
71 'auth.verify.resetResend': 'Resend reset email',
72 'auth.verify.resetResendCountdown': 'Resend reset email ({time})',
73 'auth.verify.wrongAddress': 'Wrong address?',
74 'auth.verify.changeEmail': 'Change it',
75 'auth.twofa.crumb': 'Two-factor',
76 'auth.twofa.hint': 'Code valid for 5 min · 5 attempts max',
77 'auth.twofa.heading': 'Two-factor authentication',
78 'auth.twofa.subheading': 'Enter the 6-digit code from your authenticator app.',
79 'auth.twofa.recoveryCode': 'Recovery code',
80 'auth.twofa.authCode': 'Authentication code',
81 'auth.twofa.expiresIn': 'Expires in {time}',
82 'auth.twofa.noChallengeError': 'The sign-in request expired. Start again from the sign-in screen.',
83 'auth.twofa.noChallengeNote': 'Sign-in request expired —',
84 'auth.twofa.signIn': 'sign in again',
85 'auth.twofa.submit': 'Verify',
86 'auth.twofa.trouble': 'Trouble?',
87 'auth.twofa.useAuthenticator': 'Use authenticator code',
88 'auth.twofa.useRecovery': 'Use a recovery code',
89 'auth.error.network': 'Network connection failed. Check your connection and retry.',
90 'auth.error.unknown': 'Something went wrong. Please try again.',
91} as const
Added view/app/src/i18n/messages/en/chat.ts View view/app/src/i18n/messages/en/chat.ts
@@ -0,0 +1,357 @@
1/** Chat: sidebar lists, composer, transcript, threads and voice. */
2export const chat = {
3 'chat.rooms.title': 'Channels',
4 'chat.rooms.empty': 'No channels yet',
5 'chat.rooms.create': 'New channel',
6 'chat.rooms.name': 'Channel name',
7 'chat.rooms.topic': 'Topic',
8 'chat.rooms.private': 'Private channel',
9 'chat.rooms.created': 'Channel created.',
10 'chat.rooms.updated': 'Channel updated.',
11 'chat.rooms.deleted': 'Channel deleted.',
12 'chat.dms.title': 'Direct messages',
13 'chat.dms.empty': 'No direct messages yet',
14 'chat.upload.failedStatus': 'Upload failed ({status})',
15 'chat.composer.placeholder': 'Message #{room}',
16 'chat.composer.send': 'Send',
17 'chat.composer.attach': 'Add attachment',
18 'chat.composer.emoji': 'Emoji',
19 'chat.composer.tooLong': 'Message is too long (maximum {max} characters).',
20 'chat.composer.empty': 'Message cannot be empty.',
21 'chat.composer.sent': 'Message sent.',
22 'chat.composer.failed': 'Sending failed. Please try again.',
23 'chat.composer.unauthorized': 'Your session expired — sign in again',
24 'chat.composer.forbidden': 'You cannot post in this channel',
25 'chat.composer.notFound': 'That channel or message no longer exists',
26 'chat.composer.rateLimited': 'Sending too fast — try again shortly',
27 'chat.composer.unavailable': 'No response from the service; the message was not sent',
28 'chat.message.edited': 'edited',
29 'chat.message.deleted': 'Message deleted',
30 'chat.message.edit': 'Edit message',
31 'chat.message.delete': 'Delete message',
32 'chat.message.reply': 'Reply',
33 'chat.message.thread': 'Thread',
34 'chat.message.save': 'Save',
35 'chat.message.unsave': 'Unsave',
36 'chat.message.pin': 'Pin',
37 'chat.message.unpin': 'Unpin',
38 'chat.message.copyLink': 'Copy link',
39 'chat.message.saved': 'Message saved.',
40 'chat.message.unsaved': 'Message removed from saved.',
41 'chat.message.deletedNotice': 'Message deleted.',
42 'chat.message.editedNotice': 'Message updated.',
43 'chat.message.deleteConfirm': 'Delete this message? This cannot be undone.',
44 'chat.day.today': 'Today',
45 'chat.day.yesterday': 'Yesterday',
46 'chat.day.weekday': '{weekday}',
47 'chat.thread.title': 'Threads',
48 'chat.thread.empty': 'No threads yet',
49 'chat.thread.create': 'New thread',
50 'chat.thread.replies': '{count} replies',
51 'chat.thread.archived': 'Archived',
52 'chat.thread.archivedNotice': 'Thread archived.',
53 'chat.attachment.image': 'Image',
54 'chat.attachment.video': 'Video',
55 'chat.attachment.audio': 'Audio',
56 'chat.attachment.pdf': 'PDF',
57 'chat.attachment.text': 'Text',
58 'chat.attachment.file': 'File',
59 'chat.attachment.download': 'Download attachment',
60 'chat.attachment.tooLarge': 'The attachment exceeds the size limit.',
61 'chat.attachment.uploadFailed': 'Attachment upload failed. Please retry.',
62 'chat.voice.join': 'Join voice',
63 'chat.voice.leave': 'Leave voice',
64 'chat.voice.mute': 'Mute',
65 'chat.voice.unmute': 'Unmute',
66 'chat.voice.stage': 'Stage',
67 'chat.voice.connected': 'Connected to the voice channel',
68 'chat.voice.disconnected': 'Left the voice channel',
69 'chat.voice.restore': 'Restore sound',
70 'chat.voice.failed': 'Could not join the voice channel. Please retry.',
71 'chat.voice.members': 'Members online',
72 'chat.voice.empty': 'Nobody else is in this channel yet',
73 'chat.voice.membersCount': '{count} members are already here',
74 'chat.member.list': 'Members',
75 'chat.member.invite': 'Invite members',
76 'chat.member.online': 'Online',
77 'chat.mention.everyone': 'Mention everyone',
78 'chat.notification.newMessage': '{author} mentioned you in #{room}',
79 'chat.notification.reply': '{author} replied to your message',
80 'chat.notification.viewChannel': 'View channel',
81 // Room header, connection banner and room lifecycle.
82 'chat.room.slowModeTitle': 'Slow mode — one message every {seconds}s',
83 'chat.room.dmTopicPlaceholder': 'Every conversation starts with a hello',
84 'chat.room.topicPlaceholder': 'Share what you are thinking and get things done together',
85 'chat.room.transcriptPane': 'Transcript',
86 'chat.room.callPane': 'Call view',
87 'chat.room.pins': 'Pinned messages',
88 'chat.room.threads': 'Threads',
89 'chat.room.members': 'Channel members',
90 'chat.room.membersWithCount': 'Channel members · {count}',
91 'chat.room.options': 'Channel options',
92 'chat.room.mute': 'Mute this channel',
93 'chat.room.unmute': 'Unmute',
94 'chat.room.favorite': 'Favourite this channel',
95 'chat.room.unfavorite': 'Remove from favourites',
96 'chat.room.clearDraft': 'Clear draft',
97 'chat.room.closeDm': 'Close this conversation',
98 'chat.room.settings': 'Channel settings',
99 'chat.room.info': 'Channel info',
100 'chat.room.dmSettings': 'Conversation settings',
101 'chat.room.ownedByViewer': 'You own this channel',
102 'chat.room.leave': 'Leave channel',
103 'chat.room.details': 'Channel details',
104 'chat.room.accessDenied': 'You cannot view this channel',
105 'chat.room.gone': 'This channel no longer exists or was deleted',
106 'chat.room.goneHint':
107 'The link may have expired, or the owner deleted the channel. Pick another one from the list on the left.',
108 'chat.room.settingsNoteDm':
109 'A direct message only exposes a few settings beyond its name; closing it removes it from the list.',
110 'chat.room.settingsNoteManage': 'These fields are written to the room itself, so every member sees the change.',
111 'chat.room.settingsNoteView': 'You have read access only — the settings below are read-only.',
112 'chat.room.nameHint': 'Lowercase letters, digits and hyphens are the safest choice',
113 'chat.room.topicHint': 'Shown in the title bar; up to 1024 characters',
114 'chat.room.private': 'Private channel (members only)',
115 'chat.room.rateLimit': 'Slow mode (seconds)',
116 'chat.room.rateLimitHint': '0 disables it; the shortest gap between two messages from one member',
117 'chat.room.deleteNote': 'The channel stops being visible and its messages will not come back.',
118 'chat.room.deleteConfirm': 'Delete the channel',
119 'chat.room.deleteAction': 'Delete this channel',
120 'chat.room.saved': 'Saved',
121 'chat.room.slowModeShort': 'Slow mode {seconds}s',
122
123 // Realtime connection banner.
124 'chat.connection.authRequired': 'The realtime connection needs a fresh sign-in',
125 'chat.connection.stopped': 'Realtime stopped; messages will not update automatically',
126 'chat.connection.connecting': 'Connecting to the realtime channel…',
127 'chat.connection.closed': 'Realtime disconnected; messages will not update automatically',
128 'chat.connection.retry': 'Reconnect',
129
130 // Transcript.
131 'chat.transcript.aria': 'Chat messages',
132 'chat.transcript.accessDenied': 'You no longer have access to this channel',
133 'chat.transcript.accessDeniedHint': 'The server revoked read access here. Pick another channel from the left.',
134 'chat.transcript.loadingOlder': 'Loading earlier messages…',
135 'chat.transcript.threadStart': 'The thread starts here',
136 'chat.transcript.start': 'Every good conversation starts somewhere',
137 'chat.transcript.loadFailed': 'Messages failed to load',
138 'chat.transcript.seedFailed': 'The latest messages could not be loaded',
139 'chat.transcript.threadEmptyTitle': 'Keep the discussion going',
140 'chat.transcript.threadEmptyBody': 'Share your take and be the first to reply.',
141 'chat.transcript.roomEmptyTitle': 'Welcome to {room}',
142 'chat.transcript.roomEmptyBody': 'A new topic, a fresh idea, or just a hello. Let us start here.',
143 'chat.transcript.newMessages': '{count} new messages',
144 'chat.transcript.newMessages_one': '{count} new message',
145 'chat.transcript.newMessages_other': '{count} new messages',
146 'chat.transcript.backToLatest': 'Back to the latest',
147 'chat.transcript.pinned': 'Pinned',
148 'chat.transcript.replyDeleted': 'Replying to a deleted message',
149 'chat.transcript.thisDeleted': '(this message was deleted)',
150 'chat.transcript.deletedShort': '(deleted)',
151 'chat.transcript.jumpToOriginal': 'Jump to the original message',
152 'chat.transcript.actions': 'Message actions',
153 'chat.transcript.typingOne': '{name} is typing…',
154 'chat.transcript.typingTwo': '{first} and {second} are typing…',
155 'chat.transcript.typingMany': '{names} are typing…',
156 'chat.transcript.mine': 'You',
157 'chat.transcript.unsent': 'Not sent',
158 'chat.transcript.sending': 'Sending',
159 'chat.transcript.sendFailed': 'Sending failed',
160 'chat.transcript.editHint': 'Enter to save · Shift+Enter for a new line · Esc to cancel',
161 'chat.transcript.react': 'Add a reaction',
162 'chat.transcript.replyInThread': 'Reply in a thread',
163 'chat.transcript.quoteReply': 'Quote reply',
164 'chat.transcript.copied': 'Copied',
165 'chat.transcript.copyText': 'Copy text',
166 'chat.transcript.more': 'More actions',
167 'chat.transcript.lastReply': 'Last reply {time}',
168 'chat.transcript.locked': 'Locked',
169
170 // Reactions and unread markers.
171 'chat.reaction.aria': '{emoji}, {count} reacted',
172 'chat.reaction.ariaMine': '{emoji}, {count} reacted, including you',
173 'chat.reaction.title': '{count} reacted',
174 'chat.unread.count': '{count} unread',
175 'chat.unread.label': 'Unread',
176
177 // System lines written by the backend into a room.
178 'chat.system.integration': 'Integration',
179 'chat.system.postWithContent': '{author}: {content}',
180 'chat.system.post': '{author} posted a message',
181 'chat.system.threadCreated': '{author} started a thread',
182 'chat.system.joined': '{author} joined this channel',
183 'chat.system.left': '{author} left this channel',
184 'chat.system.topicChanged': '{author} changed the topic to "{topic}"',
185 'chat.system.topicUpdated': '{author} updated the channel topic',
186 'chat.system.pinned': '{author} pinned a message',
187 'chat.system.reminder': '{author} set a reminder: {content}',
188 'chat.system.reminderSet': '{author} set a reminder',
189 'chat.system.message': 'System message',
190 'chat.system.sent': '{author} sent a message',
191
192 // Thread panels.
193 'chat.panel.close': 'Close panel',
194 'chat.panel.resize': 'Resize side panel',
195 'chat.panel.clearFocus': 'Restore chat clarity',
196 'chat.panel.loadedMembers': 'Loading space members…',
197 'chat.panel.searching': 'Searching…',
198 'chat.thread.lockedNotice': 'This thread is locked and cannot be replied to',
199 'chat.thread.archivedReadOnly': 'This thread is archived and read-only',
200 'chat.thread.byOwner': "{name}'s thread",
201 'chat.thread.fromOwner': 'Thread from {name}',
202 'chat.thread.summary': '{replies} replies · {participants} participants',
203 'chat.thread.participants': '{count} people',
204 'chat.thread.participants_one': '{count} participant',
205 'chat.thread.participants_other': '{count} participants',
206 'chat.thread.count': '{count} threads',
207 'chat.thread.follow': 'Turn on alerts',
208 'chat.thread.followTitle': 'Notify me about every new reply in this thread',
209 'chat.thread.following': 'Alerts on',
210 'chat.thread.followingTitle': 'New replies will notify you; click to turn alerts off',
211 'chat.thread.unfollow': 'Turn off alerts',
212 'chat.thread.unfollowTitle': 'Stop notifying me about this thread',
213 'chat.thread.lock': 'Lock the thread (no new replies)',
214 'chat.thread.unlock': 'Unlock the thread',
215 'chat.thread.locked': 'Locked',
216 'chat.thread.missing': 'Not found',
217 'chat.thread.missingHint':
218 'This thread may have been deleted, or it is still loading. Close this and reopen it from the thread list.',
219 'chat.thread.emptyHint':
220 'No threads yet. Choose "Reply in a thread" from a message toolbar to keep the discussion organised.',
221
222 // Pin list.
223 'chat.pins.count': '{count} pinned',
224 'chat.pins.empty': 'Nothing is pinned yet. Hover a message and pin it from "More actions".',
225 'chat.pins.jump': 'Jump to this message',
226 'chat.pins.jumpShort': 'Jump',
227
228 // Member roster.
229 'chat.member.count': '{count} people',
230 'chat.member.count_one': '{count} person',
231 'chat.member.count_other': '{count} people',
232 'chat.member.owner': 'Owner',
233 'chat.member.forbidden': 'This is a private channel; its member list is not shared with you.',
234 'chat.member.loadFailed': 'The member list failed to load.',
235 'chat.member.self': 'This is you',
236 'chat.member.dmTitle': 'Direct message @{username}',
237 'chat.member.addCount': 'Add ({count})',
238 'chat.member.allInRoom': 'Everyone in this space is already in this channel.',
239 'chat.member.listFailed': 'Space members could not be read',
240
241 // Search panel.
242
243 // Attachments.
244 'chat.attachment.unknownType': 'Unknown type',
245 'chat.attachment.preview': 'Preview',
246 'chat.attachment.closePreview': 'Close preview',
247
248 // Emoji picker.
249 'chat.emoji.common': 'Frequent',
250 'chat.emoji.gestures': 'Gestures',
251 'chat.emoji.symbols': 'Symbols',
252 'chat.emoji.custom': 'Space',
253 'chat.emoji.search': 'Search emoji',
254 'chat.emoji.searchClear': 'Clear search',
255
256 // Voice surface.
257 'chat.voice.channelFallback': 'Voice channel',
258 'chat.voice.callAria': 'Call',
259 'chat.voice.barDragHint': 'Drag the empty area to move it; double-click to reset',
260 'chat.voice.mic': 'Microphone',
261 'chat.voice.camera': 'Camera',
262 'chat.voice.screenShare': 'Share screen',
263 'chat.voice.noPublish': 'You cannot speak right now',
264 'chat.voice.return': 'Back to the voice channel',
265 'chat.voice.hangUp': 'Leave the call',
266 'chat.voice.disabled': 'Voice is not enabled',
267 'chat.voice.joinFailed': 'Could not join the voice channel',
268 'chat.voice.handFailed': 'Raising your hand failed',
269 'chat.voice.handCancelFailed': 'Withdrawing your hand failed',
270 'chat.voice.handDecideFailed': 'The approval failed',
271 'chat.voice.sharing': '{name} is sharing their screen',
272 'chat.voice.stageHint':
273 'This is a stage channel: join to listen, raise your hand to speak, and the microphone opens once a host approves.',
274 'chat.voice.joinHint': 'Joining turns on your microphone; you can mute it or turn the camera off at any time.',
275 'chat.voice.handPending': 'Hand raised — waiting for a host to approve',
276 'chat.voice.cancelHand': 'Withdraw your hand',
277 'chat.voice.joining': 'Joining…',
278 'chat.voice.tilePlaced': 'Drag to park it, resize from the bottom-right, double-click to return to the grid',
279 'chat.voice.tileHint': 'Press and drag to any free spot',
280 'chat.voice.forceUnmute': 'Unmute',
281 'chat.voice.forceMute': 'Mute',
282 'chat.voice.kick': 'Remove from the call',
283 'chat.voice.tileSpeaking': '{name} is speaking',
284 'chat.voice.solo': 'Nobody is here with you yet — invite members to join.',
285 'chat.voice.hands': 'Speaking requests',
286 'chat.voice.canSpeak': 'You may speak now',
287 'chat.voice.raiseHand': 'Raise your hand to speak',
288 'chat.voice.pendingCount': '{count} pending',
289 'chat.voice.noHands': 'No pending requests',
290 'chat.voice.revoke': 'Revoke',
291 'chat.voice.speaking': '{name} · speaking',
292 'chat.voice.notice.micDenied': 'Microphone access was denied; joining in listen-only mode',
293 'chat.voice.notice.connectFailed': 'The voice connection failed',
294 'chat.voice.notice.kicked': 'A moderator removed you from the call',
295 'chat.voice.notice.duplicate': 'This account joined the call somewhere else',
296 'chat.voice.notice.dropped': 'The voice connection dropped — click to rejoin',
297 'chat.voice.notice.micUnavailable': 'The microphone is unavailable',
298 'chat.voice.notice.cameraUnavailable': 'The camera is unavailable',
299 'chat.voice.notice.screenUnavailable': 'Screen sharing is unavailable',
300 'chat.voice.notice.audioBlocked':
301 'The browser still blocks audio playback — check the sound permission for this site',
302 'chat.voice.notice.mediaUnreachable':
303 'The voice channel could not be established: the browser cannot reach the voice service. Rejoin; contact an administrator if it keeps failing.',
304 'chat.voice.notice.permissionDenied':
305 'The browser denied microphone/camera permission — restore it from the address bar and retry',
306 'chat.voice.notice.tokenInvalid': 'The voice token is invalid or expired — join again',
307 'chat.voice.notice.deviceMissing': 'No usable device found - check that a microphone/camera is connected.',
308 'chat.voice.notice.deviceInUse': 'The device is in use by another application.',
309 'chat.voice.notice.browserUnsupported': 'This browser cannot do voice calls - use Chrome or Edge.',
310 'chat.voice.mutedByHost': 'The host muted your microphone.',
311 'chat.voice.unmutedByHost': 'The host gave you the microphone back.',
312 'chat.voice.reconnecting': 'Voice connection dropped - reconnecting automatically...',
313 'chat.voice.reconnectFailed': 'Automatic reconnection failed. Select rejoin to try again.',
314 'chat.voice.rejoin': 'Rejoin',
315 'chat.voice.deviceSystemDefault': 'System default',
316 'chat.voice.deviceOff': 'Off',
317 'chat.voice.joinWithMic': 'Open the microphone when joining',
318 'chat.voice.deviceNoMic': 'Allow microphone access to list devices',
319 'chat.voice.deviceNoOutput': 'System default output',
320 'chat.voice.deviceNoCamera': 'Allow camera access to list devices',
321 'chat.voice.debugDetails': 'Connection details',
322 'chat.voice.deviceTitle': 'Devices & preview',
323 'chat.voice.deviceHint': 'Pick your devices, then join. The microphone follows your choice.',
324 'chat.voice.deviceJoin': 'Join channel',
325 'chat.voice.speaker': 'Speaker',
326 'chat.voice.micTest': 'Say something - the level bar below will move',
327 'chat.voice.micLevel': 'Microphone level',
328 'chat.voice.volume': 'Volume for {name}',
329 'chat.voice.focus': 'Focus {name}',
330 'chat.voice.clearFocus': 'Clear focus',
331 'chat.voice.shareAudio': 'Also share computer sound',
332 'chat.voice.shareCancelled': 'Screen share cancelled',
333 'chat.voice.autoLayout': 'Reset to automatic layout',
334 'chat.voice.prevPage': 'Previous page',
335 'chat.voice.nextPage': 'Next page',
336 'chat.voice.aria.joined': '{name} joined the call',
337 'chat.voice.aria.left': '{name} left the call',
338 'chat.voice.tileMenu': 'Actions for {name}',
339 'chat.voice.qualityHigh': 'Good connection',
340 'chat.voice.qualityMedium': 'Fair connection',
341 'chat.voice.qualityLow': 'Poor connection',
342 'chat.voice.qualityUnknown': 'Connection quality unknown',
343
344 // Bullet comments (danmaku).
345 'chat.danmaku.title': 'Bullet comments',
346 'chat.danmaku.count': '{count} comments',
347 'chat.danmaku.loadOlder': 'Load earlier',
348 'chat.danmaku.empty': 'No bullet comments yet',
349 'chat.danmaku.flying': 'Fly across the screen',
350 'chat.danmaku.placeholder': 'Fire a bullet comment…',
351 'chat.danmaku.label': 'Bullet comment',
352
353 // Composer leftovers.
354 'chat.composer.attachmentLimit': 'A message takes at most {max} attachments; the extra ones were not added',
355 'chat.composer.blockedUploading': 'An attachment is uploading; sending unlocks automatically when it finishes',
356 'chat.composer.blockedFailed': 'An attachment failed to upload — retry or remove it before sending',
357} as const
Added view/app/src/i18n/messages/en/chatComposer.ts View view/app/src/i18n/messages/en/chatComposer.ts
@@ -0,0 +1,30 @@
1/** Chat composer: entry hints, mention list, attachment strip, send states. */
2export const chatComposer = {
3 'chat.composer.editLabel': 'Edit message',
4 'chat.composer.replyLabel': 'Reply to {author}',
5 'chat.composer.someMessage': 'a message',
6 'chat.composer.escCancel': 'Esc to cancel',
7 'chat.composer.uploadFailedShort': 'Failed',
8 'chat.composer.uploading': 'Uploading…',
9 'chat.composer.retryUpload': 'Retry upload',
10 'chat.composer.removeFile': 'Remove {name}',
11 'chat.composer.entryHint': 'Message {room} — share what you are thinking…',
12 'chat.composer.inputAria': 'Message input',
13 'chat.composer.mentionMembers': 'Mention members',
14 'chat.composer.addAttachmentHint': 'Add an attachment (or drop and paste a file)',
15 'chat.composer.addAttachment': 'Add attachment',
16 'chat.composer.insertEmoji': 'Insert emoji',
17 'chat.composer.enterSends': 'send',
18 'chat.composer.shiftEnterNewline': 'new line',
19 'chat.composer.sendAria': 'Send',
20 'chat.composer.slowModeWait': 'Slow mode — send again in {seconds}s',
21 'chat.composer.tooLongBy': '{count} characters over',
22 'chat.composer.sendWithEnter': 'Send (Enter)',
23 'chat.composer.save': 'Save',
24 'chat.composer.tooLongDetail': '{over} characters over the {max} limit',
25 'chat.composer.slowModeDetail': 'Slow mode is on in this channel: one message every {seconds}s',
26 'chat.composer.attachmentPlaceholder': '[attachment]',
27 'chat.composer.cancel': 'Cancel',
28 'chat.composer.draftSaveFailed': 'Draft could not be saved (the message itself is unaffected)',
29 'chat.composer.overLimit': 'Over the {mb} MB limit',
30} as const
Added view/app/src/i18n/messages/en/common.ts View view/app/src/i18n/messages/en/common.ts
@@ -0,0 +1,51 @@
1/** Brand and language-neutral copy shared by every screen. */
2export const common = {
3 'common.app.name': 'GitDataAI',
4 'common.action.save': 'Save',
5 'common.action.cancel': 'Cancel',
6 'common.action.close': 'Close',
7 'common.action.confirm': 'Confirm',
8 'common.action.create': 'Create',
9 'common.action.delete': 'Delete',
10 'common.action.edit': 'Edit',
11 'common.action.remove': 'Remove',
12 'common.action.retry': 'Retry',
13 'common.action.copy': 'Copy',
14 'common.action.copied': 'Copied',
15 'common.action.refresh': 'Refresh',
16 'common.action.loading': 'Loading…',
17 'common.action.working': 'Working…',
18 'common.action.back': 'Back',
19 'common.action.next': 'Next',
20 'common.action.search': 'Search',
21 'common.action.upload': 'Upload',
22 'common.action.download': 'Download',
23 'common.action.enable': 'Enable',
24 'common.action.disable': 'Disable',
25 'common.state.none': 'None',
26 'common.state.all': 'All',
27 'common.state.empty': 'Nothing here yet',
28 'common.state.unknown': 'Unknown',
29 'common.state.enabled': 'Enabled',
30 'common.state.disabled': 'Disabled',
31 'common.state.optional': 'Optional',
32 'common.state.required': 'Required',
33 'common.state.busy': 'Working…',
34 'common.time.justNow': 'Just now',
35 'common.time.expiresIn': 'Expires in {time}',
36 'common.theme.light': 'Light theme',
37 'common.theme.dark': 'Dark theme',
38 'common.language.label': 'Interface language',
39 'common.language.system': 'Follow system',
40 'common.language.saved': 'Interface language updated.',
41 'common.confirm.unsaved': 'You have unsaved changes. Leave anyway?',
42 'common.copy.failed': 'Copy failed — select the text and copy manually.',
43 'common.state.unknownUser': 'Unknown user',
44 'common.state.noSelection': 'No options available',
45 'common.action.dismiss': 'Discard',
46 'common.action.ignore': 'Dismiss',
47 'common.action.add': 'Add',
48 'common.action.clear': 'Clear',
49 'common.action.approve': 'Approve',
50 'common.action.reject': 'Reject',
51} as const
Added view/app/src/i18n/messages/en/errors.ts View view/app/src/i18n/messages/en/errors.ts
@@ -0,0 +1,45 @@
1/** Shared error surface plus HTTP/status fallbacks for localized API errors. */
2export const errors = {
3 'error.status.400': 'The request was invalid. Check the values and try again.',
4 'error.status.401': 'Your session has expired. Please sign in again.',
5 'error.status.403': 'You do not have permission to do that.',
6 'error.status.404': 'That resource does not exist or has been removed.',
7 'error.status.409': 'That action conflicts with the current state. Refresh and retry.',
8 'error.status.429': 'Too many requests — please slow down and retry shortly.',
9 'error.status.500': 'The server hit an error. Please try again later.',
10 'error.status.502': 'The service is temporarily unavailable. Please try again later.',
11 'error.status.503': 'The service is temporarily unavailable. Please try again later.',
12 'error.status.504': 'The service timed out. Please try again later.',
13 'error.status.0': 'Network error — no response.',
14 'error.status.unknown': 'The request failed. Please try again later.',
15 'error.code.bad_request': 'The request was invalid. Check the values and try again.',
16 'error.code.unauthorized': 'Your session has expired. Please sign in again.',
17 'error.code.token_expired': 'Your session has expired. Refreshing it now.',
18 'error.code.session_revoked': 'This session has been revoked. Please sign in again.',
19 'error.code.forbidden': 'You do not have permission to do that.',
20 'error.code.not_found': 'That resource does not exist or has been removed.',
21 'error.code.conflict': 'That action conflicts with the current state. Refresh and retry.',
22 'error.code.rate_limited': 'Too many requests — please slow down and retry shortly.',
23 'error.code.invalid_credentials': 'Incorrect account or password.',
24 'error.code.pow_failed': 'Proof-of-work verification failed. Please retry.',
25 'error.code.invalid_request_body': 'The request body is malformed.',
26 'error.code.merge_blocked': 'Branch protection rules blocked this merge.',
27 'error.code.import_disabled': 'Repository import is currently disabled.',
28 'error.code.import_upstream_failed': 'Could not reach the upstream repository. Check the URL or credentials.',
29 'error.code.password_change_required': 'You must change your password before continuing.',
30 'error.gate.title': 'Service unavailable',
31 'error.gate.body':
32 'API calls keep failing ({count} consecutive requests without a success), so rendering is paused. Check that the backend is running.',
33 'error.gate.lastFailure': 'Last failure: {reason}{path} | window failure rate {percent}% ({failures}/{samples})',
34 'error.gate.noResponse': 'No response',
35 'error.gate.networkError': 'Network error (no response)',
36 'error.gate.serverError': 'HTTP {status} · server error',
37 'error.gate.retry': 'Retry connection',
38 'error.gate.connecting': 'Connecting…',
39 'error.gate.probeInterval': 'Probing every {seconds}s',
40 'error.gate.lastProbe': 'last {time}',
41 'error.gate.imageAlt': 'Service unavailable',
42 'error.notFound.title': 'Page not found',
43 'error.notFound.body': 'The page you requested does not exist or has moved.',
44 'error.notFound.back': 'Back to workspace',
45} as const
Added view/app/src/i18n/messages/en/index.ts View view/app/src/i18n/messages/en/index.ts
@@ -0,0 +1,42 @@
1import type { MessageKey } from '../zh-CN'
2import { auth } from './auth'
3import { chat } from './chat'
4import { chatComposer } from './chatComposer'
5import { common } from './common'
6import { errors } from './errors'
7import { issueEvents, issues } from './issues'
8import { labels } from './labels'
9import { livekit } from './livekit'
10import { misc } from './misc'
11import { nav } from './nav'
12import { repo } from './repo'
13import { settings } from './settings'
14import { shell } from './shell'
15import { space } from './space'
16import { ui } from './ui'
17import { userSettings } from './userSettings'
18
19/**
20 * The English dictionary, typed as a total map over the reference key set. A
21 * key that exists in zh-CN but not here — or a typo in a key name — is a
22 * compile error rather than a silent runtime fallback.
23 */
24export const en: Record<MessageKey, string> = {
25 ...common,
26 ...auth,
27 ...errors,
28 ...shell,
29 ...labels,
30 ...livekit,
31 ...settings,
32 ...chat,
33 ...chatComposer,
34 ...repo,
35 ...issues,
36 ...issueEvents,
37 ...space,
38 ...misc,
39 ...nav,
40 ...ui,
41 ...userSettings,
42}
Added view/app/src/i18n/messages/en/issues.ts View view/app/src/i18n/messages/en/issues.ts
@@ -0,0 +1,571 @@
1/** Issue board and pull requests. */
2export const issues = {
3 'issue.board.new': 'New issue',
4 'issue.board.manage': 'Manage board',
5 'issue.board.empty': 'No issues yet',
6 'issue.board.emptyNote': 'Create an issue to start tracking work.',
7 'issue.board.viewBoard': 'Board',
8 'issue.board.viewList': 'List',
9 'issue.board.filter.state': 'State',
10 'issue.board.filter.priority': 'Priority',
11 'issue.board.filter.assignee': 'Assignee',
12 'issue.board.filter.label': 'Label',
13 'issue.board.filter.milestone': 'Milestone',
14 'issue.board.filter.sort': 'Sort',
15 'issue.board.filter.search': 'Search issues',
16 'issue.board.filter.reset': 'Reset filters',
17 'issue.detail.title': 'Title',
18 'issue.detail.description': 'Description',
19 'issue.detail.assignee': 'Assignee',
20 'issue.detail.labels': 'Labels',
21 'issue.detail.milestone': 'Milestone',
22 'issue.detail.dueDate': 'Due date',
23 'issue.detail.comments': 'Comments',
24 'issue.detail.commentPlaceholder': 'Write a comment…',
25 'issue.detail.commentSubmit': 'Comment',
26 'issue.detail.noComments': 'No comments yet',
27 'issue.detail.activity': 'Activity',
28 'issue.detail.close': 'Close issue',
29 'issue.detail.reopen': 'Reopen',
30 'issue.detail.closed': 'Issue closed.',
31 'issue.detail.reopened': 'Issue reopened.',
32 'issue.detail.created': 'Issue created.',
33 'issue.detail.updated': 'Issue updated.',
34 'issue.detail.deleted': 'Issue deleted.',
35 'issue.detail.deleteConfirm': 'Delete this issue?',
36 'issue.board.name': 'Board name',
37 'issue.board.created': 'Board created.',
38 'issue.board.updated': 'Board updated.',
39 'issue.board.deleted': 'Board deleted.',
40 'issue.label.create': 'New label',
41 'issue.label.color': 'Color',
42 'issue.label.name': 'Label name',
43 'issue.milestone.create': 'New milestone',
44 'issue.milestone.title': 'Milestone title',
45 'issue.milestone.due': 'Due date',
46 'issue.watch.watching': 'Watching',
47 'issue.watch.unwatched': 'Default',
48 'issue.watch.ignore': 'Ignoring',
49 'pull.new': 'New pull request',
50 'pull.title': 'Title',
51 'pull.description': 'Description',
52 'pull.base': 'Base branch',
53 'pull.head': 'Compare branch',
54 'pull.created': 'Pull request created.',
55 'pull.updated': 'Pull request updated.',
56 'pull.empty': 'No pull requests yet',
57 'pull.tabs.conversation': 'Conversation',
58 'pull.tabs.files': 'Files changed',
59 'pull.tabs.commits': 'Commits',
60 'pull.state.open': 'Open',
61 'pull.state.merged': 'Merged',
62 'pull.state.closed': 'Closed',
63 'pull.merge': 'Merge',
64 'pull.mergeMethod': 'Merge method',
65 'pull.mergeMethod.merge': 'Create a merge commit',
66 'pull.mergeMethod.squash': 'Squash and merge',
67 'pull.mergeMethod.rebase': 'Rebase and merge',
68 'pull.merged': 'Pull request merged.',
69 'pull.closedNotice': 'Pull request closed.',
70 'pull.reopened': 'Pull request reopened.',
71 'pull.draft': 'Draft',
72 'pull.readyForReview': 'Ready for review',
73 'pull.blockedHint': 'Branch protection rules blocked this merge.',
74 'pull.commentPlaceholder': 'Leave a comment…',
75 'pull.approve': 'Approve',
76 'pull.requestChanges': 'Request changes',
77 'pull.reviewSubmitted': 'Review submitted.',
78 'pull.filesChanged': 'Files changed',
79} as const
80
81/** Issue activity-line verbs, keyed by the backend's event type. */
82export const issueEvents = {
83 'issue.event.unknown': 'Updated the issue',
84 'issue.event.fallback': 'Updated the issue ({type})',
85 'issue.event.created': 'Created the issue',
86 'issue.event.title_updated': 'Changed the title',
87 'issue.event.description_updated': 'Changed the summary',
88 'issue.event.content_updated': 'Changed the description',
89 'issue.event.milestone_updated': 'Changed the milestone',
90 'issue.event.priority_updated': 'Changed the priority',
91 'issue.event.due_updated': 'Changed the due date',
92 'issue.event.closed': 'Closed the issue',
93 'issue.event.reopened': 'Reopened the issue',
94 'issue.event.comment_created': 'Added a comment',
95 'issue.event.comment_updated': 'Edited a comment',
96 'issue.event.comment_deleted': 'Deleted a comment',
97 'issue.event.member_added': 'Added a member',
98 'issue.event.member_removed': 'Removed a member',
99 'issue.event.label_added': 'Added a label',
100 'issue.event.label_removed': 'Removed a label',
101 'issue.event.dependency_added': 'Added a dependency',
102 'issue.event.dependency_removed': 'Removed a dependency',
103 'issue.event.pinned': 'Pinned the issue',
104 'issue.event.unpinned': 'Unpinned the issue',
105 'issue.event.repo_relation_added': 'Linked a repository',
106 'issue.event.repo_relation_removed': 'Unlinked a repository',
107 'issue.event.verb.milestone_updated': 'changed milestone {from} → {to}',
108 'issue.event.verb.title_updated': 'renamed {from} → {to}',
109 'issue.event.verb.label_added': 'added label {name}',
110 'issue.event.verb.label_removed': 'removed label {name}',
111 'issue.event.verb.member_added': 'added member {name}',
112 'issue.event.verb.member_removed': 'removed member {name}',
113 'issue.event.verb.pull_linked': 'linked pull request {name}',
114 'issue.event.verb.pull_unlinked': 'unlinked pull request {name}',
115 'issue.filter.searchPlaceholder': 'Search issues…',
116 'issue.filter.searchAria': 'Search issue titles',
117 'issue.filter.clearSearch': 'Clear search',
118 'issue.filter.statusGroup': 'State filter',
119 'issue.filter.allPriorities': 'All priorities',
120 'issue.filter.labelsFailed': 'Could not load labels',
121 'issue.filter.allLabels': 'All labels',
122 'issue.filter.allMilestones': 'All milestones',
123 'issue.filter.noMilestone': 'No milestone',
124 'issue.filter.allAssignees': 'All assignees',
125 'issue.filter.sortActive': 'Sort: {label}',
126 'issue.filter.viewGroup': 'View switch',
127 'issue.filter.clear': 'Clear filters',
128 'issue.filter.panelTitle': 'Filter',
129 'issue.filter.searchIn': 'Search {title}',
130 'issue.filter.searchHint': 'Press / to search',
131 'issue.filter.clearAll': 'Clear all',
132 'issue.filter.done': 'Done',
133 'issue.board.resultCount': '{count} results',
134 'issue.board.more': 'More actions',
135 'issue.board.settingsAction': 'Issue settings',
136 'issue.board.newHint': 'New issue · press C',
137 'issue.filter.chip.sort': 'Sort: {value}',
138 'issue.filter.chip.state': 'State: {value}',
139 'issue.filter.chip.query': 'Keyword: {value}',
140 'issue.filter.chip.priority': 'Priority: {value}',
141 'issue.filter.chip.label': 'Label: {value}',
142 'issue.filter.chip.milestone': 'Milestone: {value}',
143 'issue.filter.chip.assignee': 'Assignee: {value}',
144 'issue.filter.unknownLabel': 'Unknown label',
145 'issue.filter.noMilestoneShort': 'None',
146 'issue.filter.unknownMilestone': 'Unknown',
147 'issue.filter.remove': 'Remove filter {text}',
148 'issue.board.manageTitle': 'Board settings, labels and milestones',
149 'issue.board.manageButton': 'Manage',
150 'issue.board.errorList': 'Issue list',
151 'issue.board.pageOutOfRange': 'Page {page} is out of range: {total} issues in total, {limit} per page.',
152 'issue.board.backToFirst': 'Back to page 1',
153 'issue.board.emptyFilteredTitle': 'No issues match',
154 'issue.board.emptyFilteredHint': 'Adjust or clear the filters above and try again.',
155 'issue.board.emptyTitle': 'This board has no issues yet',
156 'issue.board.emptyHint': 'Turn an idea into a clear next step — start with one issue.',
157 'issue.board.milestoneClipped':
158 '"No milestone" is filtered on the current page (the backend cannot express it): {hidden} of {total} issues are hidden.',
159 'issue.board.refreshing': 'Refreshing…',
160 'issue.board.moveFailed': 'Could not move to "{priority}": {reason}',
161 'issue.board.columnAria': '{priority} priority, {count} issues',
162 'issue.board.addInColumn': 'New issue in "{column}"',
163 'issue.board.addCard': 'Add an issue',
164 'issue.board.addCardHint': 'You can also drop a card here',
165 'issue.board.cardAria': 'Issue {number} {title}',
166 'issue.board.milestoneTitle': 'Milestone: {name}',
167 'issue.board.tableCaption': 'Issue list; select a title to see the detail',
168 'issue.board.col.state': 'State',
169 'issue.board.col.number': 'No.',
170 'issue.board.col.title': 'Title',
171 'issue.board.col.priority': 'Priority',
172 'issue.board.col.milestone': 'Milestone',
173 'issue.board.col.assignee': 'Assignee',
174 'issue.board.col.updated': 'Updated',
175 'issue.board.changePriority': 'Change priority',
176 'issue.board.pager': 'Page {page} / {pages} · {total} issues',
177 'issue.board.prev': 'Previous',
178 'issue.board.next': 'Next',
179 'issue.board.loading': 'Loading issues',
180 'issue.board.backToBoard': 'Back to board',
181 'issue.board.errorIssue': 'Issue',
182 'issue.board.loadingIssue': 'Loading issue…',
183 'issue.board.actionFailed': 'Operation failed: {reason}',
184 'issue.board.actionFailedGeneric': 'The operation failed. Please try again later.',
185 'issue.board.edit': 'Edit',
186 'issue.board.closeIssue': 'Close issue',
187 'issue.board.reopen': 'Reopen',
188 'issue.board.closedIssue': 'Issue closed.',
189 'issue.board.reopenedIssue': 'Issue reopened.',
190 'issue.board.noBody': 'This issue has no description.',
191 'issue.board.unknownAuthor': 'Unknown author',
192 'issue.board.createdAt': '{author} created {time}',
193 'issue.board.updatedAt': 'updated {time}',
194 'issue.board.closedAt': 'closed {time}',
195 'issue.board.activity': 'Activity',
196 'issue.board.activityCount': '{count} entries',
197 'issue.board.collapse': 'Collapse',
198 'issue.board.expand': 'Expand',
199 'issue.board.properties': 'Properties',
200 'issue.board.showEarlier': 'Show {count} earlier updates',
201 'issue.board.hideEarlier': 'Hide earlier updates',
202 'issue.board.unassigned': 'Unassigned',
203 'issue.board.send': 'Send',
204 'issue.board.priorityField': 'Priority',
205 'issue.board.milestoneField': 'Milestone',
206 'issue.board.dueField': 'Due date',
207 'issue.board.unset': 'Not set',
208 'issue.board.clearDate': 'Clear date',
209 'issue.board.assigneeField': 'Assignee',
210 'issue.board.labelsField': 'Labels',
211 'issue.board.labelsFailed': 'Could not load labels',
212 'issue.board.noLabels': 'No labels',
213 'issue.board.linkedPulls': 'Linked pull requests',
214 'issue.board.notifyField': 'Notifications',
215 'issue.board.milestoneUnknown': 'Closed or deleted',
216 'issue.board.deleteFailed': 'Delete failed: {reason}',
217 'issue.board.linkFailed': 'Could not link',
218 'issue.board.unlink': 'Unlink',
219 'issue.board.stateOpen': 'Open',
220 'issue.board.noPullLinks': 'No pull requests linked yet.',
221 'issue.board.repoPlaceholder': 'Repository',
222 'issue.board.noRepos': 'This space has no repositories yet',
223 'issue.board.pullPlaceholder': 'PR no.',
224 'issue.board.pickPull': 'Pick a pull request',
225 'issue.board.pickRepoFirst': 'Pick a repository first',
226 'issue.board.noOpenPulls': 'No open pull requests in this repository',
227 'issue.board.link': 'Link',
228 'issue.board.noMembers': 'No members available',
229 'issue.board.assignAria': 'Assign to {name}',
230 'issue.board.done': 'Done',
231 'issue.board.addLabel': 'Add label',
232 'issue.board.allLabelsBound': 'Every label is already attached.',
233 'issue.board.comments': 'Comments',
234 'issue.board.loadingComments': 'Loading comments…',
235 'issue.board.commentsFailed': 'Could not load comments; reopen the issue to retry.',
236 'issue.board.noComments': 'No comments yet.',
237 'issue.board.commentSendFailed': 'Could not send the comment.',
238 'issue.board.commentPlaceholder': 'Add a comment… (Ctrl+Enter to send)',
239 'issue.board.newCommentAria': 'New comment',
240 'issue.board.submitComment': 'Comment',
241 'issue.board.unknownUser': 'Unknown user',
242 'issue.board.edited': 'edited',
243 'issue.board.editCommentAria': 'Edit comment',
244 'issue.board.replyFailed': 'Could not reply.',
245 'issue.board.replyPlaceholder': 'Reply… (Ctrl+Enter to send, Esc to close)',
246 'issue.board.reply': 'Reply',
247 'issue.board.deleteIssue': 'Delete issue',
248 'issue.board.linkPull': 'Link MR',
249 'issue.board.pickRepo': 'Pick a repository',
250 'issue.board.event.runExpand': '{count} changes in a row — expand (latest {time})',
251 'issue.board.event.runCollapse': 'Collapse changes',
252 'issue.board.replyAria': 'Reply to {name}',
253 'issue.board.commentFallback': 'comment',
254 'issue.board.loadingActivity': 'Loading activity…',
255 'issue.board.activityFailed': 'Could not load activity.',
256 'issue.board.noActivity': 'No activity yet.',
257 'issue.board.system': 'System',
258 'issue.board.form.createTitle': 'New issue',
259 'issue.board.form.editTitle': 'Edit issue {number}',
260 'issue.board.form.editDescription': 'Clear information keeps collaboration fast.',
261 'issue.board.form.validationTitle': 'Enter a title.',
262 'issue.board.form.validationTitleMax': 'The title may be at most {max} characters.',
263 'issue.board.form.saveFailed': 'Could not save. Please try again later.',
264 'issue.board.form.attach': 'Attach files',
265 'issue.board.form.removeAttachment': 'Remove attachment {name}',
266 'issue.board.form.attachmentTooLarge': 'File is too large; the limit is {max}',
267 'issue.board.form.uploadFailed': 'Attachment upload failed',
268 'issue.board.form.preview': 'Preview markdown',
269 'issue.board.form.editSource': 'Back to editing',
270 'issue.board.attachment.open': 'Open attachment {name}',
271 'issue.board.form.title': 'Title',
272 'issue.board.form.titlePlaceholder': 'Give this issue a clear title',
273 'issue.board.form.summary': 'Summary',
274 'issue.board.form.summaryOptional': '(optional)',
275 'issue.board.form.summaryPlaceholder': 'One sentence on the impact',
276 'issue.board.form.content': 'Details',
277 'issue.board.form.contentPlaceholder': 'Add details (steps to reproduce, expected result…)',
278 'issue.board.form.noLabels': 'This board has no labels yet — create them under Manage · Labels.',
279 'issue.board.form.noAssignees': 'No members can be assigned yet; you can add an assignee later.',
280 'issue.board.manage.title': 'Board management',
281 'issue.board.manage.description': 'Rename the board, maintain its labels and milestones, or delete it.',
282 'issue.board.manage.general': 'General',
283 'issue.board.manage.labels': 'Labels',
284 'issue.board.manage.milestones': 'Milestones',
285 'issue.board.manage.runFailed': 'The operation failed. Please try again later.',
286 'issue.board.manage.processing': 'Working…',
287 'issue.board.manage.name': 'Name',
288 'issue.board.manage.prefix': 'Issue prefix',
289 'issue.board.manage.prefixHint':
290 'Issue keys carry this prefix, e.g. CAD-12. Leave it empty to keep the #1 form. Start with a letter; up to 10 letters or digits.',
291 'issue.board.manage.prefixPlaceholder': 'e.g. CAD',
292 'issue.board.manage.prefixInvalid': 'Use a letter followed by letters or digits, up to 10 characters.',
293 'issue.board.manage.descriptionField': 'Description',
294 'issue.board.manage.stats': 'Totals: {open} open · {closed} closed',
295 'issue.board.manage.deleteBoard': 'Delete board',
296 'issue.board.manage.labelsFailed': 'Could not load the label list.',
297 'issue.board.manage.noLabels': 'No labels yet.',
298 'issue.board.manage.newLabel': 'New label name',
299 'issue.board.manage.useColor': 'Use colour {color}',
300 'issue.board.manage.selectColor': 'Pick a colour',
301 'issue.board.manage.labelName': 'Label name',
302 'issue.board.manage.saving': 'Saving…',
303 'issue.board.manage.saved': 'Saved',
304 'issue.board.manage.milestonesFailed': 'Could not load the milestone list.',
305 'issue.board.manage.noMilestones': 'No milestones yet.',
306 'issue.board.manage.milestoneName': 'Milestone name',
307 'issue.board.manage.due': 'Due',
308 'issue.board.manage.close': 'Close',
309 'issue.board.manage.newMilestone': 'New milestone name',
310 'issue.board.manage.milestoneDue': 'Milestone due date',
311 'issue.board.manage.add': 'Add',
312 'issue.board.closeDialog': 'Close',
313 'issue.board.noOptions': 'No options available',
314 'issue.board.confirmDiscard': 'Click again to confirm',
315 'pull.detail.tab.conversation': 'Conversation',
316 'pull.detail.tab.checks': 'Checks',
317 'pull.detail.loading': 'Loading pull request…',
318 'pull.detail.errorPull': 'Pull request',
319 'pull.detail.notFound': 'Pull request not found.',
320 'pull.detail.crumb': 'Pull requests',
321 'pull.detail.requestsMerge': 'wants to merge {head} into {base}',
322 'pull.detail.updatedAt': 'updated {time}',
323 'pull.detail.locked': 'Locked',
324 'pull.detail.autoMergeOn': 'Auto-merge on',
325 'pull.detail.complete': '{label} done.',
326 'pull.detail.failed': '{label} failed: {reason}',
327 'pull.detail.actionFailed': '{label} failed: {reason}',
328 'pull.detail.close': 'Close',
329 'pull.detail.reopen': 'Reopen',
330 'pull.detail.unlock': 'Unlock',
331 'pull.detail.lock': 'Lock',
332 'pull.detail.unlockDiscussion': 'Unlock discussion',
333 'pull.detail.lockDiscussion': 'Lock discussion',
334 'pull.detail.refreshSummary': 'Refresh summary',
335 'pull.detail.patch': 'Patch',
336 'pull.detail.deleteConfirm': 'Delete #{number}? This cannot be undone.',
337 'pull.detail.deleteFailed': 'Delete failed: {reason}',
338 'pull.detail.actions': 'Actions',
339 'pull.detail.reviewers': 'Reviewers',
340 'pull.detail.required': 'Required',
341 'pull.detail.noReviewers': 'No review requested yet.',
342 'pull.detail.reviewRequestPlaceholder': 'Search members to request a review',
343 'pull.detail.assignees': 'Assignees',
344 'pull.detail.noAssignees': 'No assignees yet.',
345 'pull.detail.assigneePlaceholder': 'Search members to add an assignee',
346 'pull.detail.labels': 'Labels',
347 'pull.detail.noLabels': 'No labels.',
348 'pull.detail.labelPlaceholder': 'Search labels to attach',
349 'pull.detail.pickNoMatch': 'No matching options',
350 'pull.detail.analyzingMerge': 'Analyzing merge conflicts…',
351 'pull.detail.stats': 'Stats',
352 'pull.detail.aheadBehind': 'Ahead / behind',
353 'pull.detail.filesChanged': 'Files changed',
354 'pull.detail.lines': 'Lines',
355 'pull.detail.created': 'Created',
356 'pull.detail.mergedAt': 'Merged',
357 'pull.detail.mergeCommit': 'Merge commit',
358 'pull.detail.merge': 'Merge',
359 'pull.detail.draftMergeHint':
360 'This is a draft PR — use Edit in the top right to mark it ready before reviewing and merging.',
361 'pull.detail.conflicts': '{count} conflicting files cannot be merged directly:',
362 'pull.detail.blockedByRules': 'Blocked by protection rules: {reasons}',
363 'pull.detail.needsApprovals': '{required} approvals required (currently {current})',
364 'pull.detail.canBypass': '(you can bypass)',
365 'pull.detail.staleBranch': 'The head branch is {count} commits behind the base branch.',
366 'pull.detail.readyToMerge': 'Ready to merge',
367 'pull.detail.approvals': '({current}/{required} approvals)',
368 'pull.detail.syncBranch': 'Sync base branch into head branch',
369 'pull.detail.syncFailed': 'Could not sync the branch: {reason}',
370 'pull.detail.mergeTitlePlaceholder': 'Merge title (defaults to the PR title)',
371 'pull.detail.mergeMessagePlaceholder': 'Merge message (defaults to the commit message)',
372 'pull.detail.deleteBranchAfter': 'Delete the head branch after merging',
373 'pull.detail.merging': 'Merging…',
374 'pull.detail.autoMerge': 'Auto-merge once checks pass',
375 'pull.detail.autoMergeFailed': 'Could not change auto-merge: {reason}',
376 'pull.detail.mergeSucceeded': 'Merged successfully: {sha}',
377 'pull.detail.mergeFailed': 'Merge failed: {reason}',
378 'pull.detail.viewConflict': 'Click to see the conflict',
379 'pull.detail.noConflictText': '(no conflict text to show)',
380 'pull.detail.conflictContent': 'Conflict · {path}',
381 'pull.detail.loadingConflict': 'Loading conflict text…',
382 'pull.detail.updateFailed': 'Update failed: {reason}',
383 'pull.detail.editNumber': 'Edit #{number}',
384 'pull.detail.draftCheckbox': 'Draft (no reviews or merges)',
385 'pull.detail.requestReviewFailed': 'Could not request a review: {reason}',
386 'pull.detail.remove': 'Remove {name}',
387 'pull.detail.addAssigneeFailed': 'Could not add the assignee: {reason}',
388 'pull.detail.attachLabelFailed': 'Could not attach the label: {reason}',
389 'pull.conv.locked': 'This PR discussion is locked; writes may be rejected by the server.',
390 'pull.conv.saveFailed': 'Could not save',
391 'pull.conv.deleteConfirm': 'Delete this comment?',
392 'pull.conv.edited': '(edited)',
393 'pull.conv.edit': 'Edit',
394 'pull.conv.delete': 'Delete',
395 'pull.conv.inline': 'Inline review comment',
396 'pull.conv.thread': 'Conversation comment',
397 'pull.conv.resolved': 'Resolved',
398 'pull.conv.markResolved': 'Resolve',
399 'pull.conv.replyPlaceholder': 'Reply to this comment…',
400 'pull.conv.send': 'Send',
401 'pull.conv.reply': 'Reply',
402 'pull.conv.official': 'Official',
403 'pull.conv.inlineCount': '{count} inline comments',
404 'pull.conv.publishApprove': 'Approve and publish',
405 'pull.conv.publishRequestChanges': 'Request changes and publish',
406 'pull.conv.publishComment': 'Publish comment only',
407 'pull.conv.dismiss': 'Dismiss review',
408 'pull.conv.loadDetails': 'Load review details ({count} inline comments with authors and state)',
409 'pull.conv.mode.comment': 'Comment',
410 'pull.conv.mode.approve': 'Approve',
411 'pull.conv.mode.requestChanges': 'Request changes',
412 'pull.conv.empty': 'The body cannot be empty.',
413 'pull.conv.submitFailed': 'Could not submit',
414 'pull.conv.append': 'Add a note',
415 'pull.conv.write': 'Comment / submit a review',
416 'pull.conv.commentPlaceholder': 'Comment on this pull request…',
417 'pull.conv.reviewPlaceholder': 'Add your review notes…',
418 'pull.conv.draftReviewHint': 'Draft PRs usually do not accept reviews; the server may reject this.',
419 'pull.conv.submitting': 'Submitting…',
420 'pull.conv.post': 'Comment',
421 'pull.conv.submitReview': 'Submit review',
422 'pull.conv.clear': 'Clear',
423 'pull.files.commentFailed': 'Could not comment',
424 'pull.files.reviewFailed': 'Could not submit the review',
425 'pull.files.loading': 'Loading changed files…',
426 'pull.files.errorFiles': 'Changed files',
427 'pull.files.empty': 'No file changes.',
428 'pull.files.summary': '{count} files changed',
429 'pull.files.compareVersion': 'Comparing',
430 'pull.files.linePlaceholder': 'Comment on {path}:{line} ({side})…',
431 'pull.files.sideOld': 'old line',
432 'pull.files.sideNew': 'new line',
433 'pull.files.postLine': 'Comment on this line',
434 'pull.files.queueReview': 'Add to pending review',
435 'pull.files.pendingTitle': 'Pending review ({count} inline comments)',
436 'pull.files.removeDraft': 'Remove this comment',
437 'pull.files.reviewSummaryPlaceholder': 'Review summary (optional)…',
438 'pull.files.approve': 'Approve',
439 'pull.files.requestChanges': 'Request changes',
440 'pull.files.commentOnly': 'Comment only',
441 'pull.files.saveAsPending': 'Save as pending review',
442 'pull.files.discard': 'Discard',
443 'pull.files.status.added': 'Added',
444 'pull.files.status.deleted': 'Deleted',
445 'pull.files.status.renamed': 'Renamed',
446 'pull.files.status.copied': 'Copied',
447 'pull.files.status.binary': 'Binary',
448 'pull.files.status.modified': 'Modified',
449 'pull.mergeable.clean': 'Mergeable',
450 'pull.mergeable.blocked': 'Blocked',
451 'pull.mergeable.dirty': 'Conflicts',
452 'pull.mergeable.unstable': 'Checks failing',
453 'pull.mergeable.hasHooks': 'Waiting on hooks',
454 'pull.mergeable.unknown': 'Unknown state',
455 'pull.parts.binaryDiff': 'Binary file — no diff is shown.',
456 'pull.parts.metaOnly': 'Metadata-only change.',
457 'pull.parts.addComment': 'Add a review comment on this line',
458 'pull.parts.truncated': 'The diff is long; showing the first 1500 lines.',
459 'pull.checks.pending': 'Queued',
460 'pull.checks.running': 'Running',
461 'pull.checks.success': 'Passed',
462 'pull.checks.failed': 'Failed',
463 'pull.checks.cancelled': 'Cancelled',
464 'pull.checks.waiting': 'Waiting',
465 'pull.checks.error': 'Error',
466 'pull.checks.loading': 'Loading checks…',
467 'pull.checks.errorChecks': 'Checks',
468 'pull.checks.empty': 'No check data.',
469 'pull.checks.versionEmpty': 'Revision {sha} has no pipeline runs or commit statuses.',
470 'pull.checks.version': 'Revision',
471 'pull.checks.details': 'Details',
472 'pull.commits.loading': 'Loading commits…',
473 'pull.commits.errorList': 'Commit list',
474 'pull.commits.empty': 'This pull request has no commits.',
475 'pull.commits.noSubject': '(no subject)',
476 'pull.commits.unknownAuthor': 'Unknown',
477 'pull.commits.mergeCommit': 'Merge commit ({count} parents)',
478 'pull.commits.view': 'View commit',
479 'pull.list.scope.created': 'Created by me',
480 'pull.list.scope.reviewRequested': 'Awaiting my review',
481 'pull.list.scope.reviewed': 'Reviewed by me',
482 'pull.list.repo': 'This repository',
483 'pull.list.mine': 'My PRs',
484 'pull.list.searchPlaceholder': 'Search title/description',
485 'pull.list.new': 'New pull request',
486 'pull.list.loading': 'Loading pull requests…',
487 'pull.list.errorList': 'Pull request list',
488 'pull.list.emptyInbox': 'This inbox is empty.',
489 'pull.list.empty': 'No pull requests match.',
490 'pull.list.pager': '{total} entries',
491 'pull.list.total': '{total} entries',
492 'pull.form.title': 'New pull request',
493 'pull.form.description': '{repo} · Merge the head branch into the base branch',
494 'pull.form.missingFields': 'Title, head branch and base branch are all required.',
495 'pull.form.sameBranch': 'The head and base branches must differ.',
496 'pull.form.createFailed': 'Could not create',
497 'pull.form.creating': 'Creating…',
498 'pull.form.createDraft': 'Create draft',
499 'pull.form.create': 'Create pull request',
500 'pull.form.head': 'Head branch',
501 'pull.form.base': 'Base branch',
502 'pull.form.comparing': 'Comparing {base} … {head}…',
503 'pull.form.fastForward': 'Fast-forward merge possible, no conflicts',
504 'pull.form.willMergeCommit': '(a merge commit will be created)',
505 'pull.form.conflicts': '{count} conflicts',
506 'pull.form.conflictsHint': ' — you can still open the PR, but they must be resolved before merging.',
507 'pull.form.titlePlaceholder': 'Merge {head} into {base}',
508 'pull.form.titlePlaceholderFallback': 'Briefly describe this change',
509 'pull.form.bodyPlaceholder': 'Explain the motivation, how you verified it, and the risks…',
510 'pull.form.draftCheckbox': 'Create as draft (mark ready before requesting reviews)',
511 'pull.form.descriptionField': 'Description (plain text, line breaks kept)',
512 'issue.board.removeLabel': 'Remove label {name}',
513 'issue.board.filterLabelPrefix': 'Priority',
514 'issue.board.form.createDescription': 'Record the problem so collaboration has something to stand on.',
515 'issue.board.filter.due': 'Due date',
516 'issue.board.milestoneLabel': 'Milestone {name}',
517 'issue.board.pullState': '({state})',
518 'issue.filter.triggerAria': '{label}: {value}',
519 'pull.reviewState.approved': 'Approved',
520 'pull.reviewState.changesRequested': 'Changes requested',
521 'pull.reviewState.commented': 'Commented',
522 'pull.reviewState.pending': 'Pending',
523 'pull.reviewState.dismissed': 'Dismissed',
524 'pull.reviewerState.requested': 'Review requested',
525 'pull.reviewerState.approved': 'Approved',
526 'pull.reviewerState.changesRequested': 'Changes requested',
527 'pull.reviewerState.commented': 'Commented',
528 'pull.reviewerState.dismissed': 'Dismissed',
529 'pull.event.created': 'created this pull request',
530 'pull.event.updated': 'updated the pull request',
531 'pull.event.titleChanged': 'changed the title',
532 'pull.event.bodyEdited': 'edited the description',
533 'pull.event.baseChanged': 'changed the base branch',
534 'pull.event.markedReady': 'marked it ready for review',
535 'pull.event.markedDraft': 'converted it to a draft',
536 'pull.event.closed': 'closed the pull request',
537 'pull.event.reopened': 'reopened the pull request',
538 'pull.event.merged': 'merged this pull request',
539 'pull.event.reviewRequested': 'requested a review',
540 'pull.event.reviewRequestRemoved': 'removed a review request',
541 'pull.event.reviewSubmitted': 'submitted a review',
542 'pull.event.approved': 'approved this pull request',
543 'pull.event.changesRequested': 'requested changes',
544 'pull.event.comment': 'commented',
545 'pull.event.labeled': 'added a label',
546 'pull.event.unlabeled': 'removed a label',
547 'pull.event.assigned': 'added an assignee',
548 'pull.event.unassigned': 'removed an assignee',
549 'pull.event.locked': 'locked the discussion',
550 'pull.event.unlocked': 'unlocked the discussion',
551 'pull.event.forcePush': 'force-pushed to the head branch',
552 'pull.event.branchUpdated': 'synced the base branch',
553 'pull.event.headRefUpdated': 'updated the head branch',
554 'pull.event.autoMergeEnabled': 'enabled auto-merge',
555 'pull.event.autoMergeDisabled': 'disabled auto-merge',
556 'pull.badge.draft': 'Draft',
557 'pull.files.summary_one': '{count} file changed',
558 'pull.files.summary_other': '{count} files changed',
559 'issue.board.pager_one': 'Page {page} / {pages} · {total} issue',
560 'issue.board.pager_other': 'Page {page} / {pages} · {total} issues',
561 'issue.board.activityCount_one': '{count} entry',
562 'issue.board.activityCount_other': '{count} entries',
563 'issue.board.columnAria_one': '{priority} priority, {count} issue',
564 'issue.board.columnAria_other': '{priority} priority, {count} issues',
565 'issue.board.pageOutOfRange_one': 'Page {page} is out of range: {total} issue in total, {limit} per page.',
566 'issue.board.pageOutOfRange_other': 'Page {page} is out of range: {total} issues in total, {limit} per page.',
567 'pull.conv.inlineCount_one': '{count} inline comment',
568 'pull.conv.inlineCount_other': '{count} inline comments',
569 'pull.list.total_one': '{total} entry',
570 'pull.list.total_other': '{total} entries',
571} as const
Added view/app/src/i18n/messages/en/labels.ts View view/app/src/i18n/messages/en/labels.ts
@@ -0,0 +1,44 @@
1/** Domain label tables: status/priority/role names reused by tables and filters. */
2export const labels = {
3 'label.priority.urgent': 'Urgent',
4 'label.priority.high': 'High',
5 'label.priority.medium': 'Medium',
6 'label.priority.low': 'Low',
7 'label.priority.none': 'No priority',
8 'label.issueState.open': 'Open',
9 'label.issueState.closed': 'Closed',
10 'label.issueState.all': 'All',
11 'label.issueSort.created': 'Newest',
12 'label.issueSort.updated': 'Recently updated',
13 'label.issueSort.due': 'Due date',
14 'label.issueSort.priority': 'Priority',
15 'label.issueSort.number': 'Number',
16 'label.issueView.board': 'Board view',
17 'label.issueView.list': 'List view',
18 'label.role.owner': 'Owner',
19 'label.role.admin': 'Admin',
20 'label.role.member': 'Member',
21 'label.role.guest': 'Guest',
22 'label.visibility.public': 'Public',
23 'label.visibility.registered': 'Registered users',
24 'label.visibility.members': 'Members only',
25 'label.visibility.private': 'Private',
26 'label.audience.everyone': 'Everyone',
27 'label.audience.following': 'People I follow',
28 'label.audience.nobody': 'Nobody',
29 'label.runStatus.queued': 'Queued',
30 'label.runStatus.running': 'Running',
31 'label.runStatus.success': 'Succeeded',
32 'label.runStatus.failed': 'Failed',
33 'label.runStatus.cancelled': 'Cancelled',
34 'label.runStatus.skipped': 'Skipped',
35 'label.authEvent.push': 'Push',
36 'label.authEvent.pullRequest': 'Pull request',
37 'label.authEvent.comment': 'Comment',
38 'label.authEvent.issue': 'Issue',
39 'label.presence.online': 'Online',
40 'label.presence.away': 'Away',
41 'label.presence.busy': 'Busy',
42 'label.presence.offline': 'Offline',
43 'label.presence.invisible': 'Invisible',
44} as const
Added view/app/src/i18n/messages/en/livekit.ts View view/app/src/i18n/messages/en/livekit.ts
@@ -0,0 +1,21 @@
1/** Copy owned by the vendored LiveKit component sources in shared/livekit. */
2export const livekit = {
3 'livekit.microphone': 'Microphone',
4 'livekit.camera': 'Camera',
5 'livekit.shareScreen': 'Share screen',
6 'livekit.stopScreenShare': 'Stop screen share',
7 'livekit.chat': 'Chat',
8 'livekit.settings': 'Settings',
9 'livekit.leave': 'Leave',
10 'livekit.audioInputs': 'Audio inputs',
11 'livekit.videoInputs': 'Video inputs',
12 'livekit.allowAudio': 'Allow Audio',
13 'livekit.startAudio': 'Start Audio',
14 'livekit.startVideo': 'Start Video',
15 'livekit.reconnecting': 'Reconnecting...',
16 'livekit.connecting': 'Connecting...',
17 'livekit.disconnected': 'Disconnected',
18 'livekit.chatPlaceholder': 'Enter a message...',
19 'livekit.screenSuffix': ' ’s screen',
20 'livekit.participantPlaceholder': 'No video yet',
21}
Added view/app/src/i18n/messages/en/misc.ts View view/app/src/i18n/messages/en/misc.ts
@@ -0,0 +1,41 @@
1/** Notifications, search results and miscellany that lacks a home domain. */
2export const misc = {
3 'notifications.title': 'Notifications',
4 'notifications.empty': 'No notifications yet',
5 'notifications.markAllRead': 'Mark all as read',
6 'notifications.markedAll': 'All notifications marked as read.',
7 'notifications.unread': 'Unread',
8 'notifications.filter.all': 'All',
9 'notifications.filter.unread': 'Unread',
10 'notifications.filter.mentions': 'Mentions',
11 'notifications.delete': 'Delete notification',
12 'notifications.deleted': 'Notification deleted.',
13 'notifications.sub':
14 'Invites, assignments and system reminders live here; mentions and replies take you straight back to their channel or issue.',
15 'notifications.filter': 'Notification filter',
16 'notifications.emptyUnread': 'No unread notifications.',
17 'notifications.emptyAll': 'Nothing needs your attention right now.',
18 'notifications.inviteByEmail': 'Handle this invite from your email.',
19 'notifications.inviteVoid':
20 'This invite is no longer valid: it may have been accepted elsewhere, revoked or expired.',
21 'notifications.joined': 'Joined',
22 'notifications.reject': 'Decline',
23 'notifications.accept': 'Accept invite',
24 'notifications.type.mention': 'Mention',
25 'notifications.type.reply': 'Reply',
26 'notifications.type.follow': 'Follow',
27 'notifications.type.invite': 'Invite',
28 'notifications.type.assignment': 'Assignment',
29 'notifications.type.system': 'System',
30 'notifications.target.issue': 'View issue #{number}',
31 'notifications.target.room': 'Open channel',
32 'notifications.target.space': 'Enter space',
33 'notifications.target.joinRequests': 'Review join request',
34 'notifications.joinRequestSubmitted': 'New join request',
35 'notifications.joinRequestApproved': 'Join request approved',
36 'notifications.joinRequestRejected': 'Join request declined',
37 'notifications.bell': 'Notifications',
38 'notifications.bellUnread': 'Notifications, {count} unread',
39 'notifications.region': 'System notifications',
40 'notifications.closeToast': 'Dismiss notification',
41} as const
Added view/app/src/i18n/messages/en/nav.ts View view/app/src/i18n/messages/en/nav.ts
@@ -0,0 +1,13 @@
1/** Workspace navigation: sidebar rows, drag-and-drop notices, empty states. */
2export const nav = {
3 'nav.empty': 'Nothing here yet. Create a channel from the space menu to start collaborating.',
4 'nav.emptyDms': 'No direct messages yet. Start one from a member profile in a space.',
5 'nav.dropHere': 'Drop here: move to root',
6 'nav.savingPosition': 'Saving position…',
7 'nav.moveFailed': 'Move failed. Please try again.',
8 'nav.moveIntoSelf': 'A folder cannot be moved into itself or one of its subfolders.',
9 'nav.unsupportedFolderMove': 'The backend rejected folder placement for channels (update and restart apihost).',
10 'nav.favorite': 'Favorited',
11 'nav.inCall': '{count} in call',
12 'nav.unreadCount': '{count} unread',
13} as const
Added view/app/src/i18n/messages/en/repo.ts View view/app/src/i18n/messages/en/repo.ts
@@ -0,0 +1,581 @@
1/** Repository surfaces: browse, history, branches, releases, settings. */
2export const repo = {
3 'repo.tabs.code': 'Code',
4 'repo.tabs.commits': 'Commits',
5 'repo.tabs.branches': 'Branches',
6 'repo.tabs.tags': 'Tags',
7 'repo.tabs.releases': 'Releases',
8 'repo.tabs.settings': 'Settings',
9 'repo.tabs.pulls': 'Pull requests',
10 'repo.unknownPath': '(unknown path)',
11 'repo.empty': 'This repository has no files',
12 'repo.loading': 'Loading repository…',
13 'repo.notFound': 'That repository does not exist or has been deleted.',
14 'repo.branch.default': 'Default branch',
15 'repo.branch.switch': 'Switch branch',
16 'repo.branch.create': 'New branch',
17 'repo.branch.name': 'Branch name',
18 'repo.branch.created': 'Branch created.',
19 'repo.branch.deleted': 'Branch deleted.',
20 'repo.branch.deleteConfirm': 'Delete branch {branch}?',
21 'repo.branch.empty': 'No branches',
22 'repo.tag.create': 'New tag',
23 'repo.tag.name': 'Tag name',
24 'repo.tag.created': 'Tag created.',
25 'repo.tag.deleted': 'Tag deleted.',
26 'repo.tag.empty': 'No tags',
27 'repo.file.history': 'File history',
28 'repo.file.download': 'Download file',
29 'repo.file.raw': 'Raw view',
30 'repo.file.blame': 'Blame',
31 'repo.file.copyPath': 'Copy path',
32 'repo.file.tooLarge': 'This file is too large to preview.',
33 'repo.file.binary': 'Binary files cannot be previewed.',
34 'repo.clone.title': 'Clone repository',
35 'repo.clone.https': 'HTTPS',
36 'repo.clone.ssh': 'SSH',
37 'repo.clone.copied': 'Clone URL copied.',
38 'repo.clone.button': 'Clone / Code',
39 'repo.clone.withSsh': 'Clone with SSH',
40 'repo.clone.withHttps': 'Clone with HTTPS',
41 'repo.clone.download': 'Download source code',
42 'repo.clone.downloadFailed': 'Could not build the archive — try again.',
43 'repo.commit.message': 'Commit message',
44 'repo.commit.author': 'Author',
45 'repo.commit.date': 'Date',
46 'repo.commit.sha': 'Commit hash',
47 'repo.commit.parent': 'Parent',
48 'repo.commit.empty': 'No commits',
49 'repo.commit.diffEmpty': 'No differences to display',
50 'repo.commit.unified': 'Unified view',
51 'repo.commit.split': 'Split view',
52 'repo.commit.whitespace': 'Ignore whitespace',
53 'repo.commit.filesChanged': '{count} files changed',
54 'repo.commit.insights': 'Commit insights',
55 'repo.release.create': 'New release',
56 'repo.release.title': 'Release title',
57 'repo.release.tag': 'Tag',
58 'repo.release.notes': 'Release notes',
59 'repo.release.draft': 'Draft',
60 'repo.release.prerelease': 'Pre-release',
61 'repo.release.published': 'Release published.',
62 'repo.release.deleted': 'Release deleted.',
63 'repo.release.empty': 'No releases yet',
64 'repo.release.downloads': 'Downloads',
65 'repo.settings.general': 'General',
66 'repo.settings.name': 'Repository name',
67 'repo.settings.description': 'Description',
68 'repo.settings.visibility': 'Visibility',
69 'repo.settings.defaultBranch': 'Default branch',
70 'repo.settings.saved': 'Repository settings saved.',
71 'repo.settings.danger': 'Danger zone',
72 'repo.settings.deleteRepo': 'Delete repository',
73 'repo.settings.deleteConfirm': 'Deletion cannot be undone. Type the repository name to confirm: {name}',
74 'repo.settings.deleted': 'Repository deleted.',
75 'repo.settings.sections.protection': 'Branch protection',
76 'repo.settings.sections.archives': 'Archives',
77 'repo.settings.sections.remotes': 'Remote mirrors',
78 'repo.settings.sections.locks': 'File locks',
79 'repo.settings.sections.lfs': 'LFS',
80 'repo.settings.sections.ci': 'CI configuration',
81 'repo.settings.sections.webhooks': 'Webhooks',
82 'repo.settings.sections.collaborators': 'Collaborators',
83 'repo.lfs.enabled': 'LFS enabled',
84 'repo.lfs.patterns': 'Tracked patterns',
85 'repo.lfs.objects': 'LFS objects',
86 'repo.action.add': 'Add',
87 'repo.action.browseCode': 'Browse code',
88 'repo.action.confirmDelete': 'Confirm delete',
89 'repo.action.details': 'Details',
90 'repo.action.refreshing': 'Refreshing…',
91 'repo.action.rename': 'Rename',
92 'repo.action.saving': 'Saving…',
93 'repo.action.setDefault': 'Set as default',
94 'repo.action.viewChanges': 'View changes',
95 'repo.error.actionFailed': 'The operation failed. Please try again later.',
96 'repo.error.actionFailedShort': 'The operation failed',
97 'repo.error.loadFailed': 'Failed to load',
98 'repo.error.saveFailed': 'Failed to save',
99 'repo.view.loadFailed': 'Failed to load the repository',
100 'repo.view.notFound': 'This repository does not exist or you do not have access to it',
101 'repo.view.archived': 'This repository is archived (read-only). Restore it in Settings.',
102 'repo.clone.notConfigured': 'Clone URL not configured',
103 'repo.clone.notConfiguredHint':
104 'The backend has no [git].base_url / http_clone_base_url, so clone URLs cannot be generated',
105 'repo.clone.sshHint': 'SSH clones require a public key registered under User settings → SSH keys',
106 'repo.clone.copiedLabel': 'Clone URL copied',
107 'repo.clone.copy': 'Copy clone URL',
108 'repo.clone.copyFailedHint': 'Copy failed — select the address on the left and copy it manually',
109 'repo.clone.copyManually': 'Copy manually',
110 'repo.clone.copiedStatus': 'Clone URL copied to the clipboard',
111 'repo.clone.copyFailedStatus': 'Copy failed — select the clone URL and copy it manually',
112 'repo.branch.defaultBadge': 'Default',
113 'repo.branch.current': 'Viewing',
114 'repo.branch.protected': 'Protected',
115 'repo.branch.protectedNote': 'Protected branches cannot be deleted, and renaming requires an administrator',
116 'repo.branch.protectedRenameBlocked': 'Protected branches cannot be renamed',
117 'repo.branch.deleteBlocked': 'The default branch and protected branches cannot be deleted',
118 'repo.branch.alreadyDefault': 'Already the default branch',
119 'repo.branch.makeDefaultHint': 'Make this the repository default branch',
120 'repo.branch.defaultSet': '“{name}” is now the default branch.',
121 'repo.branch.deletedName': 'Deleted branch “{name}”.',
122 'repo.branch.updated': 'Updated {time}',
123 'repo.branch.createHint':
124 'Branch names may contain letters, digits and . - _ / (no .., //, spaces, etc.) and must be at most 250 characters.',
125 'repo.branch.createSubmit': 'Create branch',
126 'repo.branch.startPoint': 'Start point',
127 'repo.branch.startPointHint': 'A branch name, tag or commit; defaults to the current revision.',
128 'repo.branch.createdName': 'Created branch “{name}”.',
129 'repo.branch.rename': 'Rename branch',
130 'repo.branch.renameHint': 'Renaming only affects this repository branch reference; local clones need to fetch again.',
131 'repo.branch.renameSubmit': 'Rename',
132 'repo.branch.newName': 'New branch name',
133 'repo.branch.sameName': 'The new name is the same as the current name.',
134 'repo.branch.renamed': 'Renamed branch “{from}” to “{next}”.',
135 'repo.branch.validate.empty': 'Enter a branch name.',
136 'repo.branch.validate.tooLong': 'A branch name can be at most {count} characters.',
137 'repo.branch.validate.startsWithDash': 'A branch name cannot start with a dash.',
138 'repo.branch.validate.endsWithSlash': 'A branch name cannot end with a slash.',
139 'repo.branch.validate.endsWithLock': 'A branch name cannot end with .lock.',
140 'repo.branch.validate.doubleDot': 'A branch name cannot contain two consecutive dots.',
141 'repo.branch.validate.atBrace': 'A branch name cannot contain the sequence @{.',
142 'repo.branch.validate.doubleSlash': 'A branch name cannot contain two consecutive slashes.',
143 'repo.branch.validate.backslash': 'A branch name cannot contain a backslash.',
144 'repo.branch.validate.whitespace': 'A branch name cannot contain spaces or control characters.',
145 'repo.branches.title': 'Branches',
146 'repo.branches.total': '{count} total',
147 'repo.branches.filter': 'Filter branches',
148 'repo.branches.filterPlaceholder': 'Filter branches…',
149 'repo.branches.listName': 'Branch list',
150 'repo.branches.loading': 'Loading branches…',
151 'repo.branches.noMatch': 'No matching branches.',
152 'repo.branches.none': 'This repository has no branches yet.',
153 'repo.branches.limitNote': 'Showing the first {count} branches; filtering only applies to the loaded branches.',
154 'repo.tags.title': 'Tags',
155 'repo.tags.total': '{count} total',
156 'repo.tags.listName': 'Tag list',
157 'repo.tags.loading': 'Loading tags…',
158 'repo.tags.none': 'This repository has no tags yet.',
159 'repo.tags.filterPlaceholder': 'Filter tags…',
160 'repo.tag.switch': 'Switch tag',
161 'repo.tag.annotated': 'Annotated tag',
162 'repo.tag.lightweight': 'Lightweight tag',
163 'repo.tag.deletedName': 'Deleted tag “{name}”.',
164 'repo.tag.createdName': 'Created tag “{name}”.',
165 'repo.tag.createHint':
166 'An annotated tag records the tagger and a message; a lightweight tag only points at the revision.',
167 'repo.tag.createSubmit': 'Create tag',
168 'repo.tag.target': 'Target revision',
169 'repo.tag.targetHint': 'A branch name, commit sha or another tag.',
170 'repo.tag.message': 'Message',
171 'repo.tag.messageLabel': 'Message (annotated tags)',
172 'repo.tag.messagePlaceholder': 'Highlights of this release…',
173 'repo.tag.nameRequired': 'The tag name is required.',
174 'repo.tag.detailTitle': 'Tag details · {name}',
175 'repo.tag.type': 'Type',
176 'repo.tag.targetCommit': 'Points at commit',
177 'repo.tag.tagger': 'Tagger',
178 'repo.tag.createdAt': 'Created at',
179 'repo.submodules.title': 'Submodules',
180 'repo.submodules.resolve': 'Resolve repository',
181 'repo.submodules.unresolved': 'Cannot resolve (the submodule may not exist or may not be visible)',
182 'repo.submodules.loadFailed': 'Failed to load the submodule list.',
183 'repo.breadcrumb.label': 'Repository path',
184 'repo.breadcrumb.root': 'Repository root',
185 'repo.commit.history': 'Commit history',
186 'repo.commit.backToHistory': 'Back to commit history',
187 'repo.commit.browseRevision': 'Browse this revision',
188 'repo.commit.loading': 'Loading commit…',
189 'repo.commit.what': 'commit',
190 'repo.commit.missing': 'This commit does not exist or has been rewritten.',
191 'repo.commit.noMessage': '(no commit message)',
192 'repo.commit.unknownAuthor': 'Unknown author',
193 'repo.commit.committer': 'Committer',
194 'repo.commit.root': 'First commit (against the empty tree)',
195 'repo.commit.compareTo': 'Compared to {sha}',
196 'repo.commit.parentChip': 'Parent {sha}',
197 'repo.commit.parents': '{count} parent commits',
198 'repo.commit.parents_one': '{count} parent commit',
199 'repo.commit.parents_other': '{count} parent commits',
200 'repo.commit.latest': 'Latest commit',
201 'repo.commit.collapsedHint': 'Many changes — collapsed by default; click a file to expand it.',
202 'repo.commit.noFiles': 'This commit has no file changes (it may be empty or a merge commit).',
203 'repo.commit.status.added': 'Added',
204 'repo.commit.status.deleted': 'Deleted',
205 'repo.commit.status.renamed': 'Renamed',
206 'repo.commit.status.copied': 'Copied',
207 'repo.commit.status.modified': 'Modified',
208 'repo.commit.status.binary': 'Binary',
209 'repo.commit.filesChanged_one': '{count} file changed',
210 'repo.commit.filesChanged_other': '{count} files changed',
211 'repo.commits.total': '{count} total',
212 'repo.commits.viewAll': 'View all history',
213 'repo.commits.firstParent': 'First-parent only',
214 'repo.commits.loading': 'Loading commit history…',
215 'repo.commits.missing': 'This revision may have been deleted or rewritten.',
216 'repo.commits.none': 'This revision has no commits.',
217 'repo.commits.pageEmpty': 'This page is empty; go back to the previous page.',
218 'repo.diff.followAppearance': 'Currently following User settings · Appearance',
219 'repo.diff.unified': 'Unified',
220 'repo.diff.split': 'Split',
221 'repo.diff.binary': 'Binary file — no diff content is shown.',
222 'repo.diff.metaOnly': 'Only permission or metadata changes.',
223 'repo.diff.lineComments': '{count} comments on this line',
224 'repo.diff.lineComments_one': '{count} comment on this line',
225 'repo.diff.lineComments_other': '{count} comments on this line',
226 'repo.diff.commentOnLine': 'Comment on this line',
227 'repo.diff.truncated': 'The diff is long — truncated to the first {count} lines.',
228 'repo.pager.prev': 'Previous',
229 'repo.pager.next': 'Next',
230 'repo.pager.page': 'Page {current} / {pages}',
231 'repo.tree.loading': 'Loading directory…',
232 'repo.tree.what': 'directory',
233 'repo.tree.empty': 'This directory is empty.',
234 'repo.tree.tableLabel': 'Repository files',
235 'repo.tree.name': 'Name',
236 'repo.tree.size': 'Size',
237 'repo.tree.parent': 'Up to parent directory',
238 'repo.file.backToList': 'Back to file list',
239 'repo.file.loading': 'Loading file…',
240 'repo.file.what': 'file',
241 'repo.file.blameHint': 'Show the last commit that changed each line (blame)',
242 'repo.file.blameJsonHint': 'Available in JSON mode for private repositories',
243 'repo.file.missingOid': 'Missing file identifier; cannot preview in private-repository mode.',
244 'repo.file.binaryDetail': 'Binary file · {size} · Preview not supported',
245 'repo.file.truncated': 'The file is large; the server truncated the response.',
246 'repo.file.markdownView': 'Markdown display mode',
247 'repo.file.preview': 'Preview',
248 'repo.file.source': 'Source',
249 'repo.file.unavailable': 'This file cannot be previewed.',
250 'repo.file.truncatedDetail': 'Too large to preview ({size}); the server truncated the response.',
251 'repo.file.pdfLoading': 'Rasterising the first page…',
252 'repo.file.pdfStreaming': 'Rendered {done}/{total} pages; the rest are still arriving…',
253 'repo.file.pdfTruncated': 'This document exceeds 400 pages; only the first 400 are shown.',
254 'repo.file.pdfPageAlt': 'Page {page}',
255 'repo.file.pdfPageFailed': 'Page {page} failed to render.',
256 'repo.file.pdfCounter': '{page} / {total}',
257 'repo.file.pdfPrev': 'Previous page',
258 'repo.file.pdfNext': 'Next page',
259 'repo.file.pdfZoomIn': 'Zoom in',
260 'repo.file.pdfZoomOut': 'Zoom out',
261 'repo.file.pdfFailed': 'This PDF could not be loaded.',
262 'repo.file.pdfUnreadable': 'This PDF could not be parsed; it may be encrypted or corrupt.',
263 'repo.empty.startTitle': 'Start with the first commit',
264 'repo.empty.startNote': 'The repository is ready. Add your project files and let the idea take shape here.',
265 'repo.empty.addFiles': '# add files',
266 'repo.empty.cloneNotConfigured':
267 'No clone URL is configured yet. Once it is, the push instructions will appear here.',
268 'repo.blame.loading': 'Computing line attribution…',
269 'repo.blame.unsupported': 'Blame is not available for this file (binary or currently unreadable).',
270 'repo.blame.empty':
271 'The server returned no attribution data (the file may be empty or not present at this revision).',
272 'repo.blame.what': 'Blame',
273 'repo.blame.hint': 'Click the attribution on the left to open the commit.',
274 'repo.revision': 'Revision {rev}',
275 'repo.release.loading': 'Loading releases…',
276 'repo.release.what': 'releases',
277 'repo.release.backToList': 'Back to list',
278 'repo.release.savedDraft': 'Draft saved.',
279 'repo.release.publishedNow': 'Published.',
280 'repo.release.prereleaseUpdated': 'Pre-release flag updated.',
281 'repo.release.deletedNow': 'Release deleted.',
282 'repo.release.notFound': 'That release does not exist.',
283 'repo.release.created': 'Release created.',
284 'repo.release.none': 'No releases yet. Pushing a tag creates a draft automatically, or you can create one manually.',
285 'repo.release.publishedBadge': 'Published',
286 'repo.release.commitCount': '{count} commits',
287 'repo.release.commitCount_one': '{count} commit',
288 'repo.release.commitCount_other': '{count} commits',
289 'repo.release.system': 'System',
290 'repo.release.body': 'Body',
291 'repo.release.saveDraft': 'Save draft',
292 'repo.release.publish': 'Publish',
293 'repo.release.markPrerelease': 'Mark pre-release',
294 'repo.release.unmarkPrerelease': 'Unmark pre-release',
295 'repo.release.noBody': '(no content)',
296 'repo.release.publishedImmutable': 'A published release’s content can no longer be edited.',
297 'repo.release.new': 'New release',
298 'repo.release.pickTagNote': 'Pick an existing tag; the release will point at that tag’s commit.',
299 'repo.release.tagsFailed': 'Failed to load tags. Please try again later.',
300 'repo.release.noTags': 'No tags available. Create one under Branches & tags first, or push a new tag.',
301 'repo.release.selectTag': 'Select a tag…',
302 'repo.release.tagAge': '({age})',
303 'repo.release.titleOptional': 'Title (optional)',
304 'repo.release.saveAsDraft': 'Save as a draft first',
305 'repo.comment.unknownUser': 'Unknown user',
306 'repo.comment.resolved': 'Resolved',
307 'repo.comment.outdated': 'Outdated',
308 'repo.comment.resolve': 'Resolve',
309 'repo.comment.unresolve': 'Unresolve',
310 'repo.comment.updateFailed': 'Failed to update the comment status',
311 'repo.comment.deleteFailed': 'Failed to delete the comment',
312 'repo.comment.delete': 'Delete comment',
313 'repo.comment.createFailed': 'Failed to post the comment',
314 'repo.comment.placeholder': 'Comment on this line…',
315 'repo.comment.submit': 'Post comment',
316 'repo.refs.selectBranch': 'Select branch',
317 'repo.refs.commit': '{sha} (commit)',
318 'repo.refs.current': 'Current revision {rev}; select a branch',
319 'repo.refs.branchLabel': 'Branches',
320 'repo.refs.tagLabel': 'Tags',
321 'repo.refs.commitLabel': 'Commits',
322 'repo.refs.searchBranches': 'Search branches',
323 'repo.refs.searchTags': 'Search tags',
324 'repo.refs.noMatch': 'No matches — try a different keyword.',
325 'repo.refs.noBranches': 'No branches yet',
326 'repo.refs.noTags': 'No tags yet',
327 'repo.aside.starFailed': 'Cannot update the star right now. Please try again later.',
328 'repo.aside.overview': 'Repository overview',
329 'repo.aside.noDescription': 'This repository’s story starts with its code.',
330 'repo.aside.website': 'Website',
331 'repo.aside.openWebsite': 'Open {url} in a new window',
332 'repo.aside.size': 'Repository size',
333 'repo.aside.license': 'License',
334 'repo.aside.starBusy': 'Updating…',
335 'repo.aside.starred': 'Starred · {count}',
336 'repo.aside.star': 'Star repository · {count}',
337 'repo.aside.languages': 'Languages',
338 'repo.aside.primaryLanguage': 'Primary language: {language}',
339 'repo.settings.nav.general': 'General',
340 'repo.settings.nav.features': 'Tabs',
341 'repo.featuresSettings.sub': 'Control which tabs the repository strip shows and in what order. Space owners only.',
342 'repo.featuresSettings.allowPr': 'Allow pull requests',
343 'repo.featuresSettings.allowPrHint':
344 'When off, the Pull requests tab is hidden for everyone and creating pull requests is rejected.',
345 'repo.featuresSettings.orderTitle': 'Tab order',
346 'repo.featuresSettings.orderHint':
347 'Reorder Commits / Pull requests / Branches / Pipelines / Releases. Code and Settings stay pinned.',
348 'repo.featuresSettings.moveUp': 'Move {name} up',
349 'repo.featuresSettings.moveDown': 'Move {name} down',
350 'repo.featuresSettings.saved': 'Tab settings saved.',
351 'repo.settings.nav.topics': 'Topics',
352 'repo.settings.nav.remotes': 'Remotes',
353 'repo.settings.nav.import': 'Import',
354 'repo.settings.nav.archives': 'Archives & downloads',
355 'repo.settings.nav.insights': 'Insights & maintenance',
356 'repo.settings.nav.locks': 'LFS locks',
357 'repo.settings.nav.protection': 'Protection rules',
358 'repo.settings.nav.ci': 'CI/CD',
359 'repo.settings.navLabel': 'Repository settings sections',
360 'repo.settings.generalSub': 'Description, homepage, visibility, Git LFS and the default branch. Space owners only.',
361 'repo.settings.field.description': 'Description',
362 'repo.settings.field.website': 'Website',
363 'repo.settings.field.websiteHint': 'Public homepage; leave blank when unset.',
364 'repo.settings.field.defaultBranchLoadFailed': 'Failed to load branches; only the saved default branch is shown.',
365 'repo.settings.field.privateLabel': 'Private repository',
366 'repo.settings.field.private': 'Private (visible to members only)',
367 'repo.settings.field.enableLfs': 'Enable Git LFS',
368 'repo.settings.generalSaved': 'General settings saved.',
369 'repo.settings.saveSettings': 'Save settings',
370 'repo.settings.unsaved': 'Unsaved changes',
371 'repo.settings.readonlyInfo': 'Repository info (read-only)',
372 'repo.settings.fact.id': 'Repository ID',
373 'repo.settings.fact.objectFormat': 'Object format',
374 'repo.settings.fact.createdAt': 'Created',
375 'repo.settings.fact.updatedAt': 'Last updated',
376 'repo.settings.fact.lastPushed': 'Last push',
377 'repo.settings.fact.never': 'Never',
378 'repo.settings.fact.sizes': 'Git / LFS size',
379 'repo.settings.fact.storageLimit': 'Storage limit',
380 'repo.settings.fact.unlimited': 'Unlimited',
381 'repo.settings.fact.counts': 'Branches / tags / commits',
382 'repo.settings.fact.social': 'PRs / stars / watchers',
383 'repo.settings.fact.license': 'License',
384 'repo.settings.fact.licenseUnknown': 'Not detected',
385 'repo.settings.fact.primaryLanguage': 'Primary language',
386 'repo.settings.fact.languageUnknown': 'Not analyzed',
387 'repo.settings.topicAdded': 'Added topic “{name}”.',
388 'repo.settings.topicRemoved': 'Removed topic “{name}”.',
389 'repo.settings.topicPlaceholder': 'e.g. devops, cli-tool',
390 'repo.settings.topicWillSavePrefix': 'Will be saved as',
391 'repo.settings.topicsSub':
392 'Topics make a repository discoverable. Only lowercase letters, digits and hyphens are allowed; the server creates missing topics automatically.',
393 'repo.settings.topicsLoading': 'Loading topics…',
394 'repo.settings.topicsEmpty': 'This repository has no topics yet.',
395 'repo.protection.sub':
396 'Branch protection policies gate pushes, force pushes and merges; whitelisted members can bypass the matching restriction.',
397 'repo.protection.orderNote':
398 'Matching order: when several policies match a branch, the server picks the more specific pattern.',
399 'repo.protection.create': 'New branch protection policy',
400 'repo.protection.loading': 'Loading protection policies…',
401 'repo.protection.what': 'protection policies',
402 'repo.protection.empty': 'No protection rules yet. At least one is recommended for the default branch.',
403 'repo.protection.deleteConfirm': 'Delete the protection for “{pattern}”?',
404 'repo.protection.deleteLabel': 'Delete policy',
405 'repo.protection.doneSuffix': ' completed.',
406 'repo.protection.failedSuffix': ' failed: ',
407 'repo.protection.unknownError': 'unknown error',
408 'repo.protection.chip.approvals': 'Needs {count} approvals',
409 'repo.protection.chip.noApprovals': 'No approvals required',
410 'repo.protection.chip.restrictPush': 'Restrict pushes',
411 'repo.protection.chip.noForcePush': 'Block force pushes',
412 'repo.protection.chip.pushWhitelist': 'Push whitelist',
413 'repo.protection.chip.mergeWhitelist': 'Merge whitelist',
414 'repo.protection.chip.approvalWhitelist': 'Approval whitelist',
415 'repo.protection.chip.dismissStale': 'Dismiss stale approvals',
416 'repo.protection.chip.blockRejected': 'Block merges on rejected reviews',
417 'repo.protection.chip.blockChangeRequested': 'Block merges on change requests',
418 'repo.protection.chip.blockOutdated': 'Block merges when the branch is behind',
419 'repo.protection.chip.signedCommits': 'Require signed commits',
420 'repo.protection.chip.protectedFiles': 'Protected files: {patterns}',
421 'repo.protection.chip.unprotectedFiles': 'Unprotected files: {patterns}',
422 'repo.protection.edit': 'Edit policy',
423 'repo.protection.deleteTitle': 'Delete policy',
424 'repo.protection.hideWhitelist': 'Hide whitelist',
425 'repo.protection.manageWhitelist': 'Manage whitelist',
426 'repo.protection.kind.push': 'Push',
427 'repo.protection.kind.merge': 'Merge',
428 'repo.protection.kind.approval': 'Approval bypass',
429 'repo.protection.addFailed': 'Failed to add',
430 'repo.protection.removeFailed': 'Failed to remove',
431 'repo.protection.selectUser': 'Select a user…',
432 'repo.protection.userOption': '{name} (@{username})',
433 'repo.protection.groupLabel': '(group)',
434 'repo.protection.whitelistEmpty': 'This policy has no whitelisted members.',
435 'repo.protection.whitelistDisabledNote':
436 'The matching whitelist switch is off in this policy; entries will have no effect.',
437 'repo.protection.tagWhitelistTitle': 'Tag protection whitelist',
438 'repo.protection.tagWhitelistNote':
439 'The backend does not yet expose list/create endpoints for tag protection policies; manage the user whitelist by policy UUID here.',
440 'repo.protection.tagPolicyIdPlaceholder': 'Tag protection policy UUID',
441 'repo.protection.branchRequired': 'The branch pattern is required.',
442 'repo.protection.updated': 'Policy updated.',
443 'repo.protection.created': 'Policy created.',
444 'repo.protection.editTitle': 'Edit protection policy',
445 'repo.protection.formDesc': 'Branch patterns support globs (for example main, release/*, *).',
446 'repo.protection.branchPattern': 'Branch pattern',
447 'repo.protection.requiredApprovals': 'Required approvals (0–10)',
448 'repo.protection.field.enablePush': 'Restrict pushes (allowed by default)',
449 'repo.protection.field.enableForcePush': 'Block force pushes',
450 'repo.protection.field.enablePushWhitelist': 'Enable the push whitelist',
451 'repo.protection.field.enableMergeWhitelist': 'Enable the merge whitelist',
452 'repo.protection.field.enableApprovalsWhitelist': 'Enable the approval whitelist',
453 'repo.protection.field.blockOnRejected': 'Block merges on rejected reviews',
454 'repo.protection.field.blockOnChangeRequested': 'Block merges on change-requested reviews',
455 'repo.protection.field.blockOnOutdated': 'Block merges when the source branch is behind',
456 'repo.protection.field.dismissStale': 'Dismiss stale approvals after new commits',
457 'repo.protection.field.requireSigned': 'Require signed commits',
458 'repo.protection.protectedPatterns': 'Protected file patterns (comma-separated globs, optional)',
459 'repo.protection.unprotectedPatterns': 'Unprotected file patterns (comma-separated globs, optional)',
460 'repo.locks.sub':
461 'Lock large-file paths to avoid concurrent overwrites. Locks held by others need an administrator to force-release.',
462 'repo.locks.disabledNote':
463 'Git LFS is not enabled, so the server rejects every lock request. Enable it to create and release locks.',
464 'repo.locks.enabled': 'Git LFS enabled — you can now create locks.',
465 'repo.locks.summary': 'You hold {ours} locks; others hold {theirs}.',
466 'repo.locks.pathPlaceholder': 'Path to lock, e.g. assets/video.mp4',
467 'repo.locks.pickFile': 'Choose file',
468 'repo.locks.submitting': 'Submitting…',
469 'repo.locks.create': 'New lock',
470 'repo.locks.unlocked': 'Released {path} (the request fails when it was not locked).',
471 'repo.locks.pathHint':
472 'Paths are relative to the repository root; press Enter or click “New lock” to submit. Locks are unique per path.',
473 'repo.locks.loading': 'Loading locks…',
474 'repo.locks.empty': 'No LFS locks right now.',
475 'repo.locks.locked': 'Locked {path}.',
476 'repo.locks.released': 'Released {path}.',
477 'repo.locks.forceReleased': 'Force-released {path}.',
478 'repo.locks.confirmRelease': 'Confirm release',
479 'repo.locks.forceRelease': 'Force release',
480 'repo.locks.release': 'Release',
481 'repo.locks.releasePath': 'Release this path',
482 'repo.locks.loadMore': 'Load more',
483 'repo.locks.firstPage': 'Back to the first page',
484 'repo.locks.treeFailed': 'Failed to read the repository tree: {reason} You can also type the path directly.',
485 'repo.locks.parentDir': 'Parent directory',
486 'repo.locks.validate.required': 'Specify the file path to lock first (relative to the repository root).',
487 'repo.locks.validate.nul': 'The path cannot contain null characters.',
488 'repo.locks.validate.tooLong': 'A path can be at most {count} characters.',
489 'repo.locks.validate.trailingSlash': 'Locks target a single file, so the path must not end with a slash.',
490 'repo.locks.validate.dotdot': 'The path cannot contain a .. segment.',
491 'repo.locks.validate.empty': 'The path cannot be empty.',
492 'repo.locks.validate.tooLongDetail': 'The path is too long: {detail}.',
493 'repo.locks.validate.badOid': 'The oid must be `sha256:<64 hex digits>`.',
494 'repo.locks.explanation.401': 'Your session has expired; sign in again and retry.',
495 'repo.locks.explanation.403':
496 'Your account cannot write to this repository (Write/Admin or a space owner is required).',
497 'repo.locks.explanation.404': 'The repository does not exist, or Git LFS is not enabled. Locks require it.',
498 'repo.locks.explanation.409': 'This path already has a lock, or conflicts with another lock.',
499 'repo.locks.explanation.500': 'The backend failed; retry later or try another path.',
500 'repo.locks.rejected': 'The server rejected this path: {detail}',
501 'repo.locks.invalid': 'The submitted value failed server validation.',
502 'repo.locks.explainedWithDetail': '{explanation} ({detail})',
503 'repo.locks.failedWithCode': 'The operation failed ({code}).',
504 'repo.archives.formatTar': 'tar (uncompressed)',
505 'repo.archives.archivedNote': 'The repository is archived; writes will be rejected.',
506 'repo.archives.restored': 'The repository is writable again.',
507 'repo.archives.sub': 'Archive state, on-demand snapshot archives and full-reference bundle packaging.',
508 'repo.archives.status': 'Archive state: {state}',
509 'repo.archives.statusArchived': 'Archived (read-only)',
510 'repo.archives.statusWritable': 'Active (writable)',
511 'repo.archives.archiveNote':
512 'Once archived, the repository is no longer a regular write target and pushes are rejected; you can restore it at any time.',
513 'repo.archives.restore': 'Restore repository',
514 'repo.archives.confirmArchive': 'Confirm archive',
515 'repo.archives.archive': 'Archive repository',
516 'repo.archives.snapshotTitle': 'Create a snapshot archive',
517 'repo.archives.revision': 'Revision',
518 'repo.archives.format': 'Format',
519 'repo.archives.pathPrefix': 'Path prefix (optional)',
520 'repo.archives.pathPrefixHint': 'Package only the contents of this directory, e.g. app/.',
521 'repo.archives.created': 'Archive created.',
522 'repo.archives.createAndLink': 'Create and get the download link',
523 'repo.archives.reuseHint':
524 'Do not repackage; just issue a fresh short-lived link for an existing archive. It fails when that revision has no archive yet — use “Create and get the download link” instead.',
525 'repo.archives.linkRefreshed': 'A fresh download link was issued.',
526 'repo.archives.reuse': 'Reuse an existing archive',
527 'repo.archives.bundleLabel': 'git bundle (all refs)',
528 'repo.archives.bundleCreated': 'Bundle packaged.',
529 'repo.archives.bundle': 'Package all refs as a bundle',
530 'repo.archives.factContent': 'Content',
531 'repo.archives.factSize': 'Size',
532 'repo.archives.openDownload': 'Download in a new tab',
533 'repo.archives.linkNote': 'Links are short-lived; click “Reuse an existing archive” to get a new one.',
534 'repo.remotes.sub':
535 'Git remotes configured for this repository. URLs must use https://, ssh:// or git:// and must not point at localhost or private addresses.',
536 'repo.remotes.loading': 'Loading remotes…',
537 'repo.remotes.empty': 'No remotes configured yet.',
538 'repo.remotes.fetchResult': 'Fetch: {added} new refs, {updated} updated.',
539 'repo.remotes.fetched': 'Fetched from “{name}”.',
540 'repo.remotes.fetch': 'Fetch',
541 'repo.remotes.pushResult': 'Push: {pushed} succeeded, {rejected} rejected.',
542 'repo.remotes.pushed': 'Pushed to “{name}”.',
543 'repo.remotes.push': 'Push',
544 'repo.remotes.deleted': 'Deleted remote “{name}”.',
545 'repo.remotes.upsertTitle': 'Add or update a remote',
546 'repo.remotes.name': 'Name',
547 'repo.remotes.nameHint': 'Cannot start with a dash, cannot contain spaces, at most 255 characters.',
548 'repo.remotes.url': 'URL',
549 'repo.remotes.tokenPlaceholder': 'Token (optional, this request only)',
550 'repo.remotes.refspecPlaceholder': 'refspec (for pushes; blank uses the default)',
551 'repo.remotes.saved': 'Saved remote “{name}”.',
552 'repo.remotes.save': 'Save remote',
553 'repo.remotes.mirrorResult': 'Mirror import: {commits} commits, default branch {branch}.',
554 'repo.remotes.mirrorDone': 'Mirror import complete.',
555 'repo.remotes.mirrorHint': 'Import every ref of an external repository into this one in mirror mode',
556 'repo.remotes.mirror': 'Mirror-import this URL',
557 'repo.remotes.note':
558 'The token is used for this request only and is never stored; a mirror import overwrites refs with the same name, so double-check the URL. To create and migrate a repository from GitHub / GitLab / Gitea, use the community menu’s “Import repository”; job progress shows under Settings → Import.',
559 'repo.insights.sub':
560 'Language stats, contributor rankings and commit activity. Stats are a persisted cache and need a rescan after changes.',
561 'repo.insights.rescanned': 'Language stats rescanned.',
562 'repo.insights.scanning': 'Scanning…',
563 'repo.insights.rescan': 'Rescan languages',
564 'repo.insights.rescanNote':
565 'The scan walks the directory tree of the current revision and takes longer for large repositories.',
566 'repo.insights.languages': 'Languages ({count})',
567 'repo.insights.languagesEmpty': 'No language stats yet — click “Rescan languages” above.',
568 'repo.insights.primary': 'Primary',
569 'repo.insights.total': 'Total {size}',
570 'repo.insights.contributors': 'Contributors ({count})',
571 'repo.insights.contributorsEmpty': 'No contributor stats yet.',
572 'repo.insights.commits': '{count} commits',
573 'repo.insights.commits_one': '{count} commit',
574 'repo.insights.commits_other': '{count} commits',
575 'repo.insights.activity': 'Commit activity',
576 'repo.insights.branchPlaceholder': 'Branch; blank uses the default',
577 'repo.insights.activityTotal': 'Total {count} commits',
578 'repo.insights.activityEmpty': 'No activity stats for this branch.',
579 'repo.release.titleField': 'Title',
580 'repo.branch.updatedAt': 'Updated {time}',
581} as const
Added view/app/src/i18n/messages/en/settings.ts View view/app/src/i18n/messages/en/settings.ts
@@ -0,0 +1,148 @@
1/** User settings modal: section list, form labels, notices and errors. */
2export const settings = {
3 'settings.title': 'User settings',
4 'settings.section.account': 'My account',
5 'settings.group.account': 'Account',
6 'settings.group.preferences': 'Preferences',
7 'settings.group.advanced': 'Advanced',
8 'settings.section.profile': 'Public profile',
9 'settings.section.security': 'Password & security',
10 'settings.section.sessions': 'Devices & sessions',
11 'settings.section.emails': 'Email addresses',
12 'settings.section.privacy': 'Privacy',
13 'settings.section.notifications': 'Notifications',
14 'settings.section.appearance': 'Appearance',
15 'settings.section.keys': 'Access keys',
16 'settings.section.sshKeys': 'SSH keys',
17 'settings.section.status': 'Account status',
18 'settings.account.title': 'My account',
19 'settings.account.sub': 'Account identity, language region and destructive actions.',
20 'settings.account.uploadAvatar': 'Upload avatar',
21 'settings.account.removeAvatar': 'Remove avatar',
22 'settings.account.avatarUpdated': 'Avatar updated.',
23 'settings.account.avatarRemoved': 'Avatar removed.',
24 'settings.account.displayName': 'Display name',
25 'settings.account.username': 'Username',
26 'settings.account.usernameHint': 'Lowercase letters, digits and underscores; takes effect immediately.',
27 'settings.account.language': 'Language',
28 'settings.account.locale': 'Region',
29 'settings.account.timezone': 'Time zone',
30 'settings.account.saved': 'Account details saved.',
31 'settings.account.userId': 'User ID',
32 'settings.account.createdAt': 'Joined',
33 'settings.account.status': 'Status',
34 'settings.account.active': 'Active',
35 'settings.account.inactive': 'Deactivated',
36 'settings.account.restricted': 'Restricted',
37 'settings.account.danger': 'Danger zone',
38 'settings.account.revokeNote': 'Sign out every session except this one.',
39 'settings.account.revokeAll': 'Sign out all sessions',
40 'settings.account.revoked': 'All other sessions signed out.',
41 'settings.error.generic': 'That action failed. Please try again.',
42 'settings.profile.title': 'Public profile',
43 'settings.profile.sub': 'What other people see on your profile page.',
44 'settings.profile.realName': 'Real name',
45 'settings.profile.bio': 'Bio',
46 'settings.profile.company': 'Company',
47 'settings.profile.location': 'Location',
48 'settings.profile.website': 'Website',
49 'settings.profile.pronouns': 'Pronouns',
50 'settings.profile.dateOfBirth': 'Date of birth',
51 'settings.profile.showEmail': 'Show email publicly',
52 'settings.profile.showLocation': 'Show location publicly',
53 'settings.profile.showRealName': 'Show real name publicly',
54 'settings.profile.showActivity': 'Show activity publicly',
55 'settings.profile.saved': 'Public profile saved.',
56 'settings.security.title': 'Password & security',
57 'settings.security.sub': 'Password, two-factor authentication and recovery codes.',
58 'settings.security.currentPassword': 'Current password',
59 'settings.security.newPassword': 'New password',
60 'settings.security.confirmPassword': 'Confirm new password',
61 'settings.security.changePassword': 'Change password',
62 'settings.security.passwordChanged': 'Password changed.',
63 'settings.security.totp': 'Two-factor authentication',
64 'settings.security.totpEnabled': 'Two-factor authentication enabled.',
65 'settings.security.totpDisabled': 'Two-factor authentication disabled.',
66 'settings.security.enableTotp': 'Enable two-factor',
67 'settings.security.disableTotp': 'Disable two-factor',
68 'settings.security.recoveryCodes': 'Recovery codes',
69 'settings.security.regenerateCodes': 'Regenerate recovery codes',
70 'settings.security.codesRegenerated': 'Recovery codes regenerated.',
71 'settings.security.scanQr': 'Scan the QR code with your authenticator app, then enter the 6-digit code.',
72 'settings.security.verifyCode': 'Verification code',
73 'settings.privacy.title': 'Privacy',
74 'settings.privacy.sub': 'Direct-message and mention permissions for your account, across all communities.',
75 'settings.privacy.allowDms': 'Allow direct messages',
76 'settings.privacy.allowMentions': 'Allow mentions',
77 'settings.privacy.showOnline': 'Show online status',
78 'settings.privacy.saved': 'Privacy settings saved.',
79 'settings.notifications.title': 'Notifications',
80 'settings.notifications.sub': 'Choose which events reach you in-app, by email and by push.',
81 'settings.notifications.inApp': 'In-app notifications',
82 'settings.notifications.email': 'Email notifications',
83 'settings.notifications.push': 'Push notifications',
84 'settings.notifications.digest': 'Email digest',
85 'settings.notifications.saved': 'Notification settings saved.',
86 'settings.notifications.pushEnabled': 'System notifications on this device',
87 'settings.notifications.pushEnable': 'Turn on',
88 'settings.notifications.pushGranted':
89 'This device receives system notifications. Turning it off unsubscribes the browser.',
90 'settings.notifications.pushDenied':
91 'Notifications are blocked for this site. Re-allow them in the browser address bar, then try again.',
92 'settings.notifications.pushUnsupported': 'This browser cannot show system notifications.',
93 'settings.notifications.pushTypes': 'Push these to the system',
94 'settings.notifications.pushMention': 'Mentions',
95 'settings.notifications.pushReply': 'Replies',
96 'settings.notifications.pushDm': 'Direct messages',
97 'settings.notifications.pushReminder': 'Message reminders',
98 'settings.notifications.pushFailed': 'Could not change push notifications. Please try again.',
99 'settings.appearance.title': 'Appearance',
100 'settings.appearance.sub': 'Palette, theme mode and how code differences are displayed.',
101 'settings.appearance.themeMode': 'Theme mode',
102 'settings.appearance.themeSystem': 'Follow system',
103 'settings.appearance.themeLight': 'Light',
104 'settings.appearance.themeDark': 'Dark',
105 'settings.appearance.diffStyle': 'Diff display',
106 'settings.appearance.diffDefault': 'Default (follow server setting)',
107 'settings.appearance.diffDefaultNote': 'Leaves diff_view_style unset; the server decides',
108 'settings.appearance.diffUnified': 'Unified · additions and deletions in one column',
109 'settings.appearance.diffUnifiedNote': 'Stores the value unified',
110 'settings.appearance.diffSplit': 'Split · deletions left, additions right',
111 'settings.appearance.diffSplitNote': 'Stores the value split',
112 'settings.appearance.diffHintSuffix': 'the server only recognises split; any other value renders unified',
113 'settings.appearance.boardView': 'Default issue-tracker view',
114 'settings.appearance.boardViewHint':
115 'Whether a problem set opens as a board or a list; a link carrying ?view= still wins.',
116 'userSettings.appearance.themeSystem': 'Follow system',
117 'settings.appearance.saved': 'Appearance saved.',
118 'settings.keys.title': 'Access keys',
119 'settings.keys.sub': 'Personal access tokens for Git and the API.',
120 'settings.keys.create': 'New access key',
121 'settings.keys.name': 'Name',
122 'settings.keys.created': 'Access key created — copy it now.',
123 'settings.keys.revoked': 'Access key revoked.',
124 'settings.keys.empty': 'No access keys yet.',
125 'settings.ssh.title': 'SSH keys',
126 'settings.ssh.sub': 'Public keys used to push over SSH.',
127 'settings.ssh.add': 'Add SSH key',
128 'settings.ssh.key': 'Public key',
129 'settings.ssh.added': 'SSH key added.',
130 'settings.ssh.removed': 'SSH key removed.',
131 'settings.ssh.empty': 'No SSH keys yet.',
132 'settings.sessions.title': 'Devices & sessions',
133 'settings.sessions.sub': 'Every signed-in session for this account.',
134 'settings.sessions.revoke': 'Revoke',
135 'settings.sessions.revoked': 'Session revoked.',
136 'settings.sessions.empty': 'No other active sessions.',
137 'settings.emails.title': 'Email addresses',
138 'settings.emails.sub': 'Addresses used for sign-in and notifications.',
139 'settings.emails.add': 'Add email',
140 'settings.emails.primary': 'Primary',
141 'settings.emails.setPrimary': 'Set as primary',
142 'settings.emails.verified': 'Verified',
143 'settings.emails.unverified': 'Unverified',
144 'settings.emails.removed': 'Email removed.',
145 'settings.status.title': 'Account status',
146 'settings.status.sub': 'Restrictions, suspensions and deletion requests.',
147 'settings.status.empty': 'Your account is in good standing, with no active restrictions.',
148} as const
Added view/app/src/i18n/messages/en/shell.ts View view/app/src/i18n/messages/en/shell.ts
@@ -0,0 +1,112 @@
1/** Workspace frame: rail, sidebar, route titles and account menu. */
2export const shell = {
3 'shell.rail.personal': 'Personal',
4 'shell.rail.createSpace': 'New space',
5 'shell.nav.chat': 'Chat',
6 'shell.nav.board': 'Issues',
7 'shell.nav.repo': 'Repositories',
8 'shell.nav.notifications': 'Notifications',
9 'shell.nav.settings': 'Settings',
10 'shell.nav.backToWorkspace': 'Back to workspace',
11 'shell.sidebar.rooms': 'Channels',
12 'shell.sidebar.directMessages': 'Direct messages',
13 'shell.sidebar.repositories': 'Repositories',
14 'shell.sidebar.boards': 'Issue boards',
15 'shell.sidebar.searchRooms': 'Search channels',
16 'shell.sidebar.emptyRooms': 'No channels yet',
17 'shell.sidebar.emptyRoomsNote': 'Create a channel to start collaborating.',
18 'shell.sidebar.emptyRepos': 'No repositories yet',
19 'shell.sidebar.emptyBoards': 'No issue boards yet',
20 'shell.sidebar.newRoom': 'New channel',
21 'shell.account.menu': 'Account menu',
22 'shell.account.profile': 'Public profile',
23 'shell.account.settings': 'User settings',
24 'shell.account.logout': 'Sign out',
25 'shell.account.logoutFailed': 'Sign-out failed. Please try again.',
26 'shell.search.placeholder': 'Search repos, boards, channels…',
27 'shell.search.empty': 'No matches',
28 'shell.search.hint': 'Type a keyword to search',
29 'shell.title': 'GitDataAI',
30 'shell.titleSpace': '{space} · GitDataAI',
31 'shell.titleRepo': '{repo} · {space} · GitDataAI',
32 'shell.titleBoard': '{board} · {space} · GitDataAI',
33 'shell.titleChat': '{room} · {space} · GitDataAI',
34 'shell.board.title': 'Issue boards',
35 'shell.board.pick': 'Pick an issue board from the left.',
36 'shell.board.notFound': 'That issue board does not exist or has been deleted',
37 'shell.board.forbidden': 'You do not have permission to view this issue board',
38 'shell.board.loadFailed': 'Could not load the issue board',
39 'shell.board.deletedHint': 'The board may have been deleted, or your account cannot read it.',
40 'shell.board.return': 'Back to communities',
41 'shell.board.newIssue': 'New issue',
42 'shell.board.colPending': 'Todo',
43 'shell.board.colInProgress': 'In progress',
44 'shell.board.colReview': 'In review',
45 'shell.board.colDone': 'Done',
46 'shell.repo.overview': 'Repository overview',
47 'shell.repo.tabs.code': 'Code',
48 'shell.repo.tabs.commits': 'Commits',
49 'shell.repo.tabs.pulls': 'Pull requests',
50 'shell.repo.tabs.branches': 'Branches',
51 'shell.repo.tabs.releases': 'Releases',
52 'shell.repo.tabs.settings': 'Settings',
53 'shell.repo.tabs.more': 'More',
54 'shell.repo.loading': 'Loading repository…',
55 'shell.repo.notFound': 'The repository does not exist or you cannot access it',
56 'shell.repo.loadFailed': 'Could not load the repository',
57 'shell.repo.archived': 'This repository is archived (read-only). Restore it from Settings.',
58 'shell.repo.cloneUnconfigured':
59 'The backend has no [git].base_url / http_clone_base_url, so no clone URL can be generated',
60 'shell.repo.cloneUnconfiguredShort': 'Clone URL not configured',
61 'shell.repo.sshHint': 'To clone over SSH, register a key in User settings → SSH keys first',
62 'shell.repo.cloneCopied': 'Clone URL copied',
63 'shell.repo.copyClone': 'Copy clone URL',
64 'shell.repo.copyFailed': 'Copy failed — select the URL on the left and copy it manually',
65 'shell.repo.copyManually': 'Copy manually',
66 'shell.repo.clipboardOk': 'Clone URL copied to the clipboard',
67 'shell.repo.clipboardFail': 'Copy failed — select the clone URL and copy it manually',
68 'shell.repo.starFailed': 'Could not update the star right now. Please retry.',
69 'shell.repo.descriptionFallback': "This repository's story starts with the code.",
70 'shell.repo.website': 'Website',
71 'shell.repo.openWebsite': 'Open {url} in a new window',
72 'shell.repo.license': 'License',
73 'shell.repo.starUpdating': 'Updating…',
74 'shell.repo.starred': 'Starred {count}',
75 'shell.repo.star': 'Star {count}',
76 'shell.repo.watch': 'Watch {count}',
77 'shell.repo.watching': 'Watching {count}',
78 'shell.repo.watchFailed': 'Could not update the watch right now. Please retry.',
79 'shell.repo.latestRelease': 'Latest release',
80 'shell.repo.noRelease': 'No releases yet',
81 'shell.repo.latestCommit': 'Latest commit',
82 'shell.repo.contributors': 'Contributors',
83 'shell.repo.languages': 'Languages',
84 'shell.repo.primaryLanguage': 'Primary language: {language}',
85 'shell.rail.home': 'Home',
86 'shell.rail.addSpace': 'Add a space',
87 'shell.rail.discover': 'Discover',
88 'shell.pending.title': 'Start collaborating here',
89 'shell.pending.body': '{label} — keep the conversation going, share an idea, or catch up on what the team shipped.',
90 'shell.pending.hint': 'Pick something in the workspace navigation',
91 'shell.pending.pickChannel': 'Pick a channel on the left',
92 'shell.user.settings': 'User settings',
93 'shell.user.mic': 'Microphone',
94 'shell.user.mute': 'Mute',
95 'shell.user.unmute': 'Unmute',
96 'shell.user.headphones': 'Headphones',
97 'shell.user.deafen': 'Deafen',
98 'shell.user.undeafen': 'Undeafen',
99 'shell.user.card': 'Profile',
100 'shell.user.openCard': 'View profile',
101 'shell.user.online': 'Online',
102 'shell.user.copyUsername': 'Copy username',
103 'shell.user.copyId': 'Copy user ID',
104 'shell.user.copied': 'Copied',
105 'shell.user.editProfile': 'Edit profile',
106 'shell.chat.unknownUser': 'Unknown user',
107 'shell.chat.me': 'Me',
108 'shell.chat.userFallback': 'User {id}',
109 'shell.chat.dm': 'Direct message',
110 'shell.chat.channel': 'Channel',
111 'shell.titleNotifications': 'Notifications · GitDataAI',
112} as const
Added view/app/src/i18n/messages/en/space.ts View view/app/src/i18n/messages/en/space.ts
@@ -0,0 +1,513 @@
1/** Space management: creation, members, roles, invites, settings. */
2export const space = {
3 'space.create': 'New space',
4 'space.create.name': 'Space name',
5 'space.create.slug': 'URL slug',
6 'space.create.description': 'Space description',
7 'space.settings.title': 'Space settings',
8 'space.settings.general': 'General',
9 'space.settings.members': 'Members',
10 'space.settings.roles': 'Roles',
11 'space.settings.invites': 'Invites',
12 'space.settings.channelPermissions': 'Channel permissions',
13 'space.settings.name': 'Space name',
14 'space.settings.description': 'Space description',
15 'space.settings.avatar': 'Space avatar',
16 'space.settings.saved': 'Space settings saved.',
17 'space.settings.danger': 'Danger zone',
18 'space.settings.deleteSpace': 'Delete space',
19 'space.settings.deleteConfirm': 'Deletion cannot be undone. Type the space name to confirm: {name}',
20 'space.settings.deleted': 'Space deleted.',
21 'space.members.title': 'Members',
22 'space.members.sub': 'Browse and search the people in this community.',
23 'space.members.recent': 'Recently joined',
24 'space.members.shown': 'Showing {shown} of {total} members',
25 'space.members.sort': 'Sort',
26 'space.members.sortJoined': 'Recently joined',
27 'space.members.sortName': 'By name',
28 'space.members.rowsPerPage': 'Rows per page',
29 'space.members.col.name': 'Name',
30 'space.members.col.joined': 'Joined',
31 'space.members.col.method': 'Join method',
32 'space.members.col.status': 'Status',
33 'space.members.owner': 'Community owner',
34 'space.members.invitedBy': 'Invited by {name}',
35 'space.members.joinedSelf': 'Joined directly',
36 'space.members.noMatch': 'No members match',
37 'space.members.refresh': 'Refresh members',
38 'space.members.open': 'Open the member list',
39 'space.members.invite': 'Invite members',
40 'space.members.remove': 'Remove member',
41 'space.members.removed': 'Member removed.',
42 'space.members.empty': 'No members yet',
43 'space.members.role': 'Role',
44 'space.members.joinedAt': 'Joined',
45 'space.roles.title': 'Roles',
46 'space.roles.groupCaption': 'Roles',
47 'space.roles.back': 'Back',
48 'space.roles.editorSub': 'Configure this role and its permissions — saved changes apply right away.',
49 'space.roles.settingsGroup': 'Display',
50 'space.roles.editTitle': 'Edit "{name}"',
51 'space.roles.permissionSearch': 'Search permissions',
52 'space.roles.scopeCommunity': 'Community scope',
53 'space.roles.clearPermissions': 'Clear permissions',
54 'space.roles.colorHint': 'Free-form colour: hex, rgb, hsl or a colour name.',
55 'space.roles.emptyHint': 'No roles yet — create one with “Create role” above.',
56 'space.roles.cancel': 'Cancel',
57 'space.roles.noPermissionMatch': 'No permissions match',
58 'space.roles.create': 'New role',
59 'space.roles.name': 'Role name',
60 'space.roles.permissions': 'Permissions',
61 'space.roles.created': 'Role created.',
62 'space.roles.deleted': 'Role deleted.',
63 'space.invites.title': 'Invites',
64 'space.invites.create': 'Create invite link',
65 'space.invites.expiresAt': 'Expires at',
66 'space.invites.maxUses': 'Maximum uses',
67 'space.invites.created': 'Invite link created.',
68 'space.invites.revoked': 'Invite link revoked.',
69 'space.invites.empty': 'No invite links yet',
70 'space.invites.copyLink': 'Copy invite link',
71 'space.invites.used': 'Used {used}/{max}',
72 'space.channelPermissions.title': 'Channel permissions',
73 'space.channelPermissions.sub': 'Override channel-level read and write access per member or role.',
74 'space.channelPermissions.saved': 'Channel permissions saved.',
75 'space.leave': 'Leave space',
76 'space.leave.confirm': 'Leave the space {name}?',
77 'space.left': 'Left the space.',
78 'space.join': 'Join space',
79 'space.joined': 'Joined the space.',
80 'space.create.nameRequired': 'Space name · Required',
81 'space.create.nameHint': 'Lowercase letters, digits and hyphens, up to 64 characters',
82 'space.create.nameInvalid': 'The name may contain only lowercase letters, digits and hyphens, 1-64 characters.',
83 'space.create.failed': 'Could not create the space. Please try again later.',
84 'space.create.descriptionPlaceholder': 'What is this space for?',
85 'space.create.private': 'Private space',
86 'space.create.creating': 'Creating…',
87 'space.menu.invite': 'Invite friends',
88 'space.menu.createRepo': 'Create repository',
89 'space.menu.createBoard': 'Create issue board',
90 'space.menu.createRoom': 'Create channel',
91 'space.menu.createDir': 'Create folder',
92 'space.menu.settings': 'Space settings',
93 'space.menu.privacy': 'Privacy settings',
94 'space.menu.leave': 'Leave space',
95 'space.invite.title': 'Invite friends',
96 'space.invite.sub': 'Invite new members to {space}. They receive the selected roles once they accept.',
97 'space.invite.submit': 'Send invite',
98 'space.invite.email': 'Email',
99 'space.invite.emailHint': 'For people outside the platform — they receive an invite email',
100 'space.invite.username': 'Or username',
101 'space.invite.usernameHint': 'For registered users — they also get an in-app notification',
102 'space.invite.expires': 'Expires at',
103 'space.invite.expiresHint': 'Leave empty for no expiry',
104 'space.invite.groups': 'Grant roles on join',
105 'space.invite.groupsEmpty': 'This space has no roles yet. Create one under Space settings → Roles first.',
106 'space.invite.groupsHint': 'Pick at least one role',
107 'space.invite.missingTarget': 'Provide an email or a username.',
108 'space.invite.sent': 'Invite sent: {target} can join after accepting.',
109 'space.invite.accept.crumb': 'INVITE',
110 'space.invite.accept.hint': 'PRIVATE LINK FROM AN INVITE EMAIL',
111 'space.invite.accept.heading': 'Accept invitation',
112 'space.invite.accept.headingSpace': 'Join {space}',
113 'space.invite.accept.sub': 'Accepting makes you a member with the roles chosen for this invitation.',
114 'space.invite.accept.joining': 'Joining…',
115 'space.invite.accept.signInNote':
116 'This invitation was addressed to a specific account: sign in (or sign up) first, then open the link from the email again.',
117 'space.invite.accept.login': 'Sign in',
118 'space.invite.accept.signup': 'Create an account',
119 'space.invite.accept.invalid': 'The link is missing its invite code — reopen it from the email.',
120 'space.invite.accept.notFound': 'This invitation does not exist or was revoked.',
121 'space.invite.accept.mismatch':
122 'The signed-in account is not the invitee. Sign in as the invited address and try again.',
123 'space.invite.accept.used': 'This invitation was already used or has expired.',
124 'space.invite.accept.failed': 'Could not join. Try again in a moment.',
125 'space.invite.accept.retry': 'Retry',
126 'space.invite.accept.home': 'Back to home',
127 'space.repo.title': 'Create repository',
128 'space.repo.name': 'Repository name',
129 'space.repo.nameHint': 'Lowercase letters, digits and hyphens',
130 'space.repo.description': 'Description · Optional',
131 'space.repo.dir': 'Parent folder',
132 'space.repo.defaultBranch': 'Default branch · Optional',
133 'space.repo.license': 'License · Optional',
134 'space.repo.initMessage': 'Initial commit message · Optional',
135 'space.repo.enableLfs': 'Enable Git LFS',
136 'space.repo.private': 'Private repository',
137 'space.board.create.title': 'Create issue board',
138 'space.board.nameField': 'Name',
139 'space.board.description': 'Description · Optional',
140 'space.board.icon': 'Icon · Optional',
141 'space.board.dir': 'Parent folder',
142 'space.room.title': 'Create channel',
143 'space.room.name': 'Channel name',
144 'space.room.nameHint': 'Lowercase letters, digits and hyphens',
145 'space.room.kind': 'Type',
146 'space.room.kind.text': 'Text channel',
147 'space.room.kind.voice': 'Voice channel',
148 'space.room.kind.stage': 'Stage channel',
149 'space.room.dir': 'Parent folder',
150 'space.room.topic': 'Topic · Optional',
151 'space.room.userLimit': 'Voice user limit · Optional',
152 'space.room.rateLimit': 'Slowmode in seconds · Optional',
153 'space.room.noLimit': '0 = no limit',
154 'space.room.private': 'Private channel (invite only)',
155 'space.dir.title': 'Create folder',
156 'space.dir.description': 'Folders group repositories and issue boards in the space tree.',
157 'space.dir.name': 'Folder name',
158 'space.dir.parent': 'Parent folder',
159 'space.dir.root': 'Root',
160 'space.privacy.title': 'Privacy settings',
161 'space.privacy.description': 'Direct message and mention permissions for your account, across every space.',
162 'space.privacy.dms': 'Allow direct messages',
163 'space.privacy.dmsHint': 'Let others send you direct messages',
164 'space.privacy.mentions': 'Allow mentions',
165 'space.privacy.mentionsHint': 'Let others mention you in messages',
166 'space.privacy.online': 'Show online status',
167 'space.privacy.onlineHint': 'Show your online status in member lists',
168 'space.privacy.saved': 'Privacy settings saved.',
169 'space.leave.title': 'Leave space',
170 'space.leave.sub': 'You will need a new invite to come back to {space}.',
171 'space.userLabel': '{name} (@{username})',
172 'space.permission.viewSpace': 'View space',
173 'space.permission.read': 'Read content',
174 'space.permission.message': 'Send messages',
175 'space.permission.manageMembers': 'Manage members',
176 'space.permission.manageRoles': 'Manage roles',
177 'space.permission.manageInvites': 'Manage invites',
178 'space.permission.manageBans': 'Manage bans',
179 'space.permission.manageChannels': 'Manage channels',
180 'space.permission.manageOverwrites': 'Manage permission overrides',
181 'space.discover.title': 'Discover spaces',
182 'space.discover.sub': 'Browse public spaces, their intro and member count, then ask to join.',
183 'space.discover.search': 'Search space names or descriptions',
184 'space.discover.sort.members': 'Recommended',
185 'space.discover.sort.newest': 'Newest',
186 'space.discover.cardSize': 'Card size',
187 'space.discover.cardSize.large': 'Large',
188 'space.discover.cardSize.medium': 'Medium',
189 'space.discover.cardSize.small': 'Small',
190 'space.discover.resultCount': '{count} public spaces',
191 'space.discover.loading': 'Loading spaces…',
192 'space.discover.empty': 'No matching spaces',
193 'space.discover.emptyHint': 'Try another keyword, or come back later.',
194 'space.discover.loadFailed': 'Failed to load spaces',
195 'space.discover.loadMore': 'Load more',
196 'space.discover.members': '{count} members',
197 'space.discover.joinLevel': 'Join requirement: {level}',
198 'space.discover.joinLevel.none': 'No extra requirement',
199 'space.discover.enter': 'Enter space',
200 'space.discover.requested': 'Request submitted',
201 'space.discover.pending': 'Pending review',
202 'space.discover.withdraw': 'Withdraw request',
203 'space.discover.withdrawn': 'Request withdrawn.',
204 'space.discover.cooldown': 'Cooling down',
205 'space.discover.cooldownUntil': 'You can apply again {time}',
206 'space.discover.apply': 'Ask to join',
207 'space.discover.closed': 'Not accepting requests',
208 'space.discover.rejected': 'Last request was declined',
209 'space.discover.dialogTitle': 'Ask to join {space}',
210 'space.discover.note': 'Message (optional)',
211 'space.discover.noteHint': 'Briefly say why you want to join, up to 512 characters.',
212 'space.discover.notePlaceholder': 'I would like to join because…',
213 'space.discover.submit': 'Submit request',
214 'space.discover.submitted': 'Request submitted; an admin will review it.',
215 'space.discover.submitFailed': 'Failed to submit request',
216 'space.discover.withdrawFailed': 'Failed to withdraw request',
217 'space.discover.cooldownError': 'Your last request was just declined; try again later.',
218 'space.discover.requirementError': "You do not meet this space's join requirement yet.",
219 'space.discover.notAllowedError': 'This space is not accepting join requests.',
220 'space.discover.docTitle': 'Discover spaces · GitDataAI',
221 'space.joinRequests.title': 'Join requests',
222 'space.joinRequests.sub': 'Review people asking to join; approving adds them as members.',
223 'space.joinRequests.filter.pending': 'Pending',
224 'space.joinRequests.filter.approved': 'Approved',
225 'space.joinRequests.filter.rejected': 'Declined',
226 'space.joinRequests.filter.all': 'All',
227 'space.joinRequests.empty': 'No join requests yet',
228 'space.joinRequests.emptyHint': 'Requests appear here, with a notification, as soon as someone applies.',
229 'space.joinRequests.loadFailed': 'Failed to load join requests',
230 'space.joinRequests.note': 'Message',
231 'space.joinRequests.noNote': 'No message provided',
232 'space.joinRequests.approve': 'Approve',
233 'space.joinRequests.reject': 'Decline',
234 'space.joinRequests.approved': 'Approved {name}.',
235 'space.joinRequests.rejected': 'Declined {name}.',
236 'space.joinRequests.status.pending': 'Pending',
237 'space.joinRequests.status.approved': 'Approved',
238 'space.joinRequests.status.rejected': 'Declined',
239 'space.joinRequests.status.withdrawn': 'Withdrawn',
240 'space.joinRequests.decidedBy': 'Decided by {name}',
241 'space.joinRequests.requestedAt': 'Requested {time}',
242 'space.joinRequests.loadMore': 'Load more',
243 'space.joinRequests.count': '{count} total',
244 'space.joinLevel.none': 'No extra requirement',
245 'space.joinLevel.mail': 'Verified email required',
246 'space.joinLevel.mailDay': 'Verified email and one-day-old account required',
247 'space.joinLevel.mailHour': 'Verified email and one-hour-old account required',
248 'space.joinLevel.mailPhone': 'Verified email and phone required',
249 'space.notify.all': 'All messages',
250 'space.notify.mentions': 'Mentions only',
251 'space.notify.nothing': 'Nothing',
252 'space.memberStatus.active': 'Active',
253 'space.memberStatus.inactive': 'Inactive',
254 'space.memberStatus.left': 'Left',
255 'space.memberStatus.removed': 'Removed',
256 'space.joinTrigger.issueReaction': 'Issue reaction',
257 'space.joinTrigger.commentReaction': 'Comment reaction',
258 'space.image.avatar': 'Space icon',
259 'space.image.avatarHint': 'Square recommended, at least 128×128',
260 'space.image.banner': 'Space banner',
261 'space.image.bannerHint': 'Shown atop lists and the space page',
262 'space.image.inviteBanner': 'Invite card image',
263 'space.image.inviteBannerHint': 'Used as the preview when sharing an invite',
264 'space.image.embedSplash': 'Embed background',
265 'space.image.embedSplashHint': 'Used by external embed pages',
266 'space.settings.sections.mine': 'My membership',
267 'space.settings.group.community': 'Community',
268 'space.settings.group.members': 'Members & permissions',
269 'space.settings.sections.general': 'Space profile',
270 'space.settings.sections.images': 'Icons and banners',
271 'space.settings.sections.join': 'Join rules',
272 'space.settings.sections.joinRequests': 'Join requests',
273 'space.settings.sections.bans': 'Bans',
274 'space.settings.sections.cleanup': 'Object cleanup',
275 'space.cleanup.ownerSub': 'Bulk-clean directories and objects in the space. Space owners only.',
276 'space.cleanup.notOwner': 'Object cleanup is available to the space owner only.',
277 'space.cleanup.sub':
278 'Delete or adjust visibility quickly: repositories can toggle public/private and be archived, channels can toggle public/private or be deleted, boards can be deleted, empty directories can be deleted, and nodes with visibility overwrites can be reset to the default.',
279 'space.cleanup.filterPlaceholder': 'Filter by name / path / type',
280 'space.cleanup.empty': 'No matching objects.',
281 'space.cleanup.children': '{count} child node(s)',
282 'space.cleanup.overwrites': '{count} visibility overwrite(s)',
283 'space.cleanup.archived': 'Archived',
284 'space.cleanup.makePrivate': 'Make private',
285 'space.cleanup.makePublic': 'Make public',
286 'space.cleanup.archive': 'Archive',
287 'space.cleanup.unarchive': 'Unarchive',
288 'space.cleanup.clearOverwrites': 'Clear visibility overwrites',
289 'space.cleanup.deleteBlocked': 'This node still has children; empty it first.',
290 'space.cleanup.confirmDelete': 'Delete "{name}"? This cannot be undone.',
291 'space.cleanup.confirmClearOverwrites': 'Clear every visibility overwrite on "{name}" and restore the default?',
292 'space.cleanup.updated': 'Updated.',
293 'space.cleanup.deleted': 'Deleted.',
294 'space.cleanup.overwritesCleared': 'Visibility overwrites cleared.',
295 'space.cleanup.kind.dir': 'Directory',
296 'space.cleanup.kind.object': 'Object',
297 'space.cleanup.kind.repo': 'Repository',
298 'space.cleanup.kind.board': 'Board',
299 'space.cleanup.kind.text': 'Text channel',
300 'space.cleanup.kind.voice': 'Voice channel',
301 'space.cleanup.kind.stage': 'Stage channel',
302 'space.cleanup.kind.category': 'Category',
303 'space.cleanup.kind.news': 'News channel',
304 'space.cleanup.kind.guildRoom': 'Channel',
305 'space.cleanup.kind.thread': 'Thread',
306 'space.settings.fullLoadFailed':
307 'Could not load the full space record; these forms fall back to the cached list row and some fields may be stale.',
308 'space.general.title': 'Space profile',
309 'space.general.sub': 'Name, description and how people join.',
310 'space.general.ownerOnly': 'Only the space owner can change this; you have read-only access.',
311 'space.general.name': 'Space name',
312 'space.general.nameHint': 'Lowercase letters, digits and hyphens only, up to 64 characters',
313 'space.general.description': 'Description',
314 'space.general.descriptionHint': 'Shown on the space card and directory page, up to 512 characters',
315 'space.general.welcome': 'Welcome message',
316 'space.general.welcomeHint': 'A note posted in the channel when someone joins',
317 'space.general.welcomePlaceholder': 'Welcome back — please read the channel topic before posting.',
318 'space.general.access': 'Access and joining',
319 'space.general.private': 'Private space (members only)',
320 'space.general.allowJoin': 'Allow join requests',
321 'space.general.joinLevel': 'Join requirement',
322 'space.general.allowInvite': 'Allow members to send invites',
323 'space.general.require2fa': 'Require two-factor authentication',
324 'space.general.save': 'Save profile',
325 'space.general.saved': 'Space profile saved.',
326 'space.general.readonly': 'Read-only information',
327 'space.general.id': 'Space ID',
328 'space.general.owner': 'Owner',
329 'space.general.members': 'Members',
330 'space.general.status': 'Status',
331 'space.general.createdUpdated': 'Created / updated',
332 'space.general.active': 'Active',
333 'space.general.inactive': 'Disabled',
334 'space.danger.ownerSub': 'Irreversible operations such as deleting the space are owner-only.',
335 'space.danger.notOwner': 'You are not the owner of this space, so you cannot delete it.',
336 'space.danger.sub': 'The space disappears immediately; restoring it requires the platform administrators.',
337 'space.danger.deleteName': 'Delete {name}',
338 'space.danger.description': 'Repositories, issue boards and channels go offline with this address. Members: {count}.',
339 'space.danger.confirmLabel': 'Type the space name {name} to confirm',
340 'space.danger.deleting': 'Deleting…',
341 'space.danger.confirm': 'Delete space',
342 'space.images.sub': 'Images are uploaded as raw bytes straight to object storage; each file must stay under 5 MB.',
343 'space.images.loadFailed': 'Could not load the images — you may lack the manage-channels permission.',
344 'space.images.tooLarge': 'The image exceeds 5 MB. Compress it and try again.',
345 'space.images.updated': '{label} updated.',
346 'space.images.deleted': '{label} deleted.',
347 'space.images.loading': 'Loading',
348 'space.images.unset': 'Not set',
349 'space.images.replace': 'Replace',
350 'space.membership.sub': 'These settings only affect your experience in this space.',
351 'space.membership.subJoined': 'Joined {date}. These settings only affect your experience in this space.',
352 'space.membership.save': 'Save my settings',
353 'space.membership.saved': 'Your space settings were saved.',
354 'space.alias.label': 'Space nickname',
355 'space.alias.hint': 'Leave empty to use your global name, up to 64 characters',
356 'space.alerts.label': 'Message alerts',
357 'space.alerts.mobilePush': 'Receive mobile push',
358 'space.alerts.suppressEveryone': 'Ignore @everyone mentions',
359 'space.alerts.suppressRoles': 'Ignore role mentions',
360 'space.alerts.hideMuted': 'Hide muted channels',
361 'space.members.count': '{count} members. Nicknames and alerts apply in this space only.',
362 'space.members.searchPlaceholder': 'Search members…',
363 'space.members.searchAria': 'Search members',
364 'space.members.usernamePlaceholder': 'Username',
365 'space.members.usernameAria': 'Username to add',
366 'space.members.add': 'Add member',
367 'space.members.added': 'Member added.',
368 'space.members.loading': 'Loading members…',
369 'space.members.emptyFiltered': 'No members match.',
370 'space.members.muted': 'Muted',
371 'space.members.collapse': 'Collapse',
372 'space.members.expand': 'Edit',
373 'space.members.mute': 'Mute this member',
374 'space.members.muteUntil': 'Muted until',
375 'space.members.muteForever': 'Leave empty for permanent',
376 'space.members.allowPush': 'Allow mobile push',
377 'space.members.save': 'Save member settings',
378 'space.members.saved': 'Member settings saved.',
379 'space.members.missingUser': 'This member has no user id',
380 'space.roles.sub': 'Roles group members and grant permissions in bulk. Their order sets how members are listed.',
381 'space.roles.namePlaceholder': 'Role name',
382 'space.roles.nameAria': 'New role name',
383 'space.roles.color': 'Colour',
384 'space.roles.colorAria': 'New role colour',
385 'space.roles.colorAria2': 'Role colour',
386 'space.roles.emojiAria': 'New role emoji',
387 'space.roles.loading': 'Loading roles…',
388 'space.roles.empty': 'No roles yet. Create one, then tick its permissions on the member row to grant them.',
389 'space.roles.noPermissions': 'No permissions yet',
390 'space.roles.createdAt': 'Created {date}',
391 'space.roles.permissionCount': '{count} permissions',
392 'space.roles.emoji': 'Emoji',
393 'space.roles.emojiHint': 'Optional, shown before the role name',
394 'space.roles.position': 'Order',
395 'space.roles.positionHint': 'Lower numbers come first',
396 'space.roles.save': 'Save role',
397 'space.roles.saved': 'Role updated.',
398 'space.roles.dirty': 'Unsaved changes',
399 'space.roles.selectMember': 'Select a member',
400 'space.roles.selectMemberPlaceholder': 'Select a member…',
401 'space.roles.granted': 'Role granted.',
402 'space.roles.grant': 'Grant',
403 'space.roles.revoked': 'Role revoked.',
404 'space.roles.revoke': 'Revoke',
405 'space.roles.joined': 'You joined the role.',
406 'space.roles.join': 'Join',
407 'space.roles.left': 'You left the role.',
408 'space.roles.leave': 'Leave',
409 'space.roles.noReadback':
410 'The server only exposes role writes — there is no read endpoint for which roles a member holds, so granted members cannot be listed here.',
411 'space.roles.delete': 'Delete role',
412 'space.invites.sub': 'Accepting makes someone a member with the selected roles; at least one role is required.',
413 'space.invites.missingTarget': 'Provide an email or a username.',
414 'space.invites.missingRole': 'Pick at least one role to grant.',
415 'space.invites.mode': 'Invite method',
416 'space.invites.byEmail': 'By email',
417 'space.invites.byUsername': 'By username',
418 'space.invites.email': 'Email address',
419 'space.invites.username': 'Username',
420 'space.invites.oneOf': 'Fill in either one',
421 'space.invites.expiresHint': 'Leave empty for no expiry',
422 'space.invites.grantLabel': 'Roles granted on accept · Required',
423 'space.invites.send': 'Send invite',
424 'space.invites.activeTitle': 'Outstanding invites',
425 'space.invites.loading': 'Loading invites…',
426 'space.invites.historyTitle': 'Past invites',
427 'space.invites.noEmail': 'No email recorded',
428 'space.invites.expiresOn': 'expires {date}',
429 'space.invites.acceptedOn': 'accepted {date}',
430 'space.invites.revoke': 'Revoke',
431 'space.invites.redeemTitle': 'Redeem an invite code',
432 'space.invites.redeemNote':
433 'The accept code only appears in the invite email — the API never returns it. If the recipient has no verified email they cannot open the link; invite them by username instead and they will see it in their notifications.',
434 'space.invites.code': 'Invite code',
435 'space.invites.redeemed': 'Invite code redeemed.',
436 'space.invites.state.pending': 'Pending',
437 'space.invites.state.accepted': 'Accepted',
438 'space.invites.state.revoked': 'Revoked',
439 'space.invites.state.expired': 'Expired',
440 'space.invites.nonePending': 'No pending invites.',
441 'space.joinRules.sub':
442 'People who meet the condition gain a role automatically — a way to promote keen issue-board contributors to members.',
443 'space.joinRules.deletedRole': 'Deleted role',
444 'space.joinRules.needRole': 'Create a role under Roles before configuring a rule.',
445 'space.joinRules.grantRole': 'Grant role',
446 'space.joinRules.selectRole': 'Select a role…',
447 'space.joinRules.trigger': 'Trigger',
448 'space.joinRules.matcher': 'Match',
449 'space.joinRules.matcherHint': 'Emoji name, for example thumbs_up',
450 'space.joinRules.created': 'Rule created.',
451 'space.joinRules.create': 'New rule',
452 'space.joinRules.loading': 'Loading rules…',
453 'space.joinRules.empty': 'No join rules yet.',
454 'space.joinRules.enabled': 'Active',
455 'space.joinRules.disabled': 'Disabled',
456 'space.joinRules.enable': 'Enable this rule',
457 'space.joinRules.updated': 'Rule updated.',
458 'space.joinRules.save': 'Save rule',
459 'space.joinRules.deleted': 'Rule deleted.',
460 'space.invites.expires': 'Expires at',
461 'space.bans.sub': 'Banned users cannot join unless a rule re-allows an individual capability.',
462 'space.bans.expires': 'Expires at',
463 'space.bans.expiresHint': 'Leave empty to ban permanently',
464 'space.bans.reason': 'Reason',
465 'space.bans.reasonHint': 'Admins only; recorded on the ban row',
466 'space.bans.banned': 'User banned.',
467 'space.bans.banUser': 'Ban user',
468 'space.bans.loading': 'Loading bans…',
469 'space.bans.empty': 'No bans recorded.',
470 'space.bans.unbanAt': 'lifts {date}',
471 'space.bans.permanent': 'permanent',
472 'space.bans.by': 'by {by} on {date}',
473 'space.bans.unbanned': 'Ban lifted.',
474 'space.bans.unban': 'Lift ban',
475 'space.bans.missingUser': 'This ban row has no user id',
476 'space.bans.saved': 'Ban settings updated.',
477 'space.bans.saveExceptions': 'Save exceptions',
478 'space.bans.allowSpeak': 'Allow speaking in voice channels',
479 'space.bans.allowPushRepo': 'Allow pushing to repositories',
480 'space.bans.allowCommentAny': 'Allow commenting on any issue',
481 'space.channelPermissions.sub2': 'Hide a folder or channel from specific roles and members, or force it visible.',
482 'space.channelPermissions.stance.deny': 'Hidden',
483 'space.channelPermissions.stance.inherit': 'Inherit',
484 'space.channelPermissions.stance.allow': 'Visible',
485 'space.channelPermissions.selectNode': 'Select a node',
486 'space.channelPermissions.emptyTree': 'The tree is empty',
487 'space.channelPermissions.repaired': 'Directory checked: {scanned} nodes scanned, {fixed} fixed.',
488 'space.channelPermissions.repair': 'Repair directory tree',
489 'space.channelPermissions.emptyNote': 'This space has no folders or channels yet.',
490 'space.channelPermissions.folder': 'Folder',
491 'space.channelPermissions.entry': 'Entry',
492 'space.channelPermissions.position': 'Order {position}',
493 'space.channelPermissions.archived': 'Archived',
494 'space.channelPermissions.noTopic': 'No topic',
495 'space.channelPermissions.byRole': 'By role',
496 'space.channelPermissions.byMember': 'By member',
497 'space.channelPermissions.defaultAlias': 'Default nickname',
498 'space.channelPermissions.rename': 'Rename',
499 'space.channelPermissions.renameHint': 'The server only allows changing the name',
500 'space.channelPermissions.renamed': 'Node renamed.',
501 'space.channelPermissions.saveName': 'Save name',
502 'space.channelPermissions.visibility': '{title} visibility',
503 'space.channelPermissions.addByUsername': 'Add an exception by username',
504 'space.channelPermissions.addByUsernameHint': 'Type a username directly when the member list is unavailable',
505 'space.channelPermissions.alreadyExceptional': 'That member is already in the exception list.',
506 'space.channelPermissions.exceptionAdded': 'Member exception added.',
507 'space.channelPermissions.addVisible': 'Add as visible',
508 'space.roles.emptyShort': 'No roles yet.',
509 'space.members.count_one': '{count} member. Nicknames and alerts apply in this space only.',
510 'space.members.count_other': '{count} members. Nicknames and alerts apply in this space only.',
511 'space.roles.permissionCount_one': '{count} permission',
512 'space.roles.permissionCount_other': '{count} permissions',
513} as const
Added view/app/src/i18n/messages/en/ui.ts View view/app/src/i18n/messages/en/ui.ts
@@ -0,0 +1,49 @@
1/** Shared UI chrome: shell landmarks, modal scaffolding, panel states. */
2export const ui = {
3 'ui.skipToContent': 'Skip to main content',
4 'ui.openNavigation': 'Open workspace navigation',
5 'ui.closeNavigation': 'Close workspace navigation',
6 'ui.collapseNavigation': 'Collapse navigation',
7 'ui.navigation': 'Workspace navigation',
8 'ui.spaceList': 'Spaces',
9 'ui.currentSpace': 'Current space',
10 'ui.personalSpace': 'Personal space',
11 'ui.shortcuts': 'Shortcuts',
12 'ui.workspace': 'Workspace',
13 'ui.collapseSidebar': 'Collapse sidebar',
14 'ui.expandSidebar': 'Expand sidebar',
15 'ui.navScope': 'Channels, boards and repositories',
16 'ui.workspaceLabel': 'workspace',
17 'ui.close': 'Close',
18 'ui.loading': 'Loading',
19 'ui.loadFailed': 'Loading failed. Please try again.',
20 'ui.actionFailed': 'That action failed. Please try again.',
21 'ui.fieldRequired': 'Please fill in "{field}".',
22 'ui.fieldNonNegativeInteger': '"{field}" must be a non-negative integer.',
23 'ui.noOptions': 'No options available',
24 'ui.saveChanges': 'Save changes',
25 'ui.saving': 'Saving…',
26 'ui.notFoundOnRevision': 'The {what} does not exist in this revision.',
27 'ui.forbidden': 'You do not have permission to access this {what}.',
28 'ui.loadFailedWithNote': 'Could not load the {what}: {note}',
29 'ui.loadFailedWithStatus': 'Could not load the {what} ({status}).',
30 'ui.readFileFailed': 'Could not read the file',
31 'ui.themeContextMissing': 'useThemeContext must be used inside a ThemeProvider',
32 'ui.settingsCategories': '{title} categories',
33 'palette.graphite.name': 'Graphite',
34 'palette.graphite.origin': 'GitData default',
35 'palette.paper.name': 'Paper',
36 'palette.paper.origin': 'Warm reading',
37 'palette.mint.name': 'Mint',
38 'palette.mint.origin': 'Cool and crisp',
39 'palette.github.name': 'Clear sky',
40 'palette.solarized.name': 'Solarized',
41 'palette.nord.name': 'Nordic',
42 'palette.catppuccin.name': 'Catppuccin',
43 'palette.everforest.name': 'Everforest',
44 'palette.gruvbox.name': 'Gruvbox',
45 'palette.onedark.name': 'One Dark',
46 'palette.tokyonight.name': 'Tokyo Night',
47 'palette.dracula.name': 'Dracula',
48 'ui.closeSettings': 'Close settings',
49} as const
Added view/app/src/i18n/messages/en/userSettings.ts View view/app/src/i18n/messages/en/userSettings.ts
@@ -0,0 +1,134 @@
1/** User settings: modal sections, per-section copy, and validation messages. */
2export const userSettings = {
3 'userSettings.profile.visibility': 'Profile visibility',
4 'userSettings.profile.bio': 'Bio',
5 'userSettings.profile.website': 'Website',
6 'userSettings.profile.company': 'Company',
7 'userSettings.profile.location': 'Location',
8 'userSettings.profile.saved': 'Profile saved.',
9 'userSettings.sessions.devices': 'Devices',
10 'userSettings.sessions.noDevices': 'No devices recorded yet.',
11 'userSettings.sessions.unknownDevice': 'Unknown device',
12 'userSettings.sessions.renamePlaceholder': 'Rename…',
13 'userSettings.sessions.rename': 'Rename',
14 'userSettings.sessions.renamed': 'Device renamed.',
15 'userSettings.sessions.recently': 'last seen {when}',
16 'userSettings.emails.primary': 'Primary',
17 'userSettings.emails.verified': 'Verified',
18 'userSettings.emails.pending': 'Pending',
19 'userSettings.emails.setPrimary': 'Make primary',
20 'userSettings.emails.primaryChanged': 'Primary email changed.',
21 'userSettings.emails.deleted': 'Email removed.',
22 'userSettings.emails.newPlaceholder': 'New email address',
23 'userSettings.emails.add': 'Add email',
24 'userSettings.emails.added': 'Add request submitted.',
25 'userSettings.emails.codePlaceholder': 'Code sent to {email}',
26 'userSettings.emails.verify': 'Verify',
27 'userSettings.emails.verifiedNotice': 'Email verified.',
28 'userSettings.keys.namePlaceholder': 'Key name, e.g. laptop-git',
29 'userSettings.keys.scope': 'Scope',
30 'userSettings.keys.create': 'Create',
31 'userSettings.keys.newToken': 'New key (shown once)',
32 'userSettings.keys.empty': 'No access keys yet.',
33 'userSettings.keys.createdAt': 'Created {created}',
34 'userSettings.keys.lastUsed': 'Last used {used}',
35 'userSettings.keys.rotate': 'Rotate',
36 'userSettings.keys.rotated': 'Rotated — copy the new key.',
37 'userSettings.keys.delete': 'Delete',
38 'userSettings.keys.deleted': 'Key deleted.',
39 'userSettings.security.changePassword': 'Change password',
40 'userSettings.security.currentPlaceholder': 'Current password',
41 'userSettings.security.newPlaceholder': 'New password',
42 'userSettings.security.confirmPlaceholder': 'Confirm new password',
43 'userSettings.security.update': 'Update password',
44 'userSettings.security.passwordUpdated': 'Password updated.',
45 'userSettings.security.fillBoth': 'Enter both the current and the new password.',
46 'userSettings.security.mismatch': 'The two new passwords do not match.',
47 'userSettings.security.totp': 'Two-factor authentication',
48 'userSettings.security.recoveryCodes': 'Recovery codes',
49 'userSettings.security.generateCodes': 'Generate recovery codes',
50 'userSettings.security.codesGenerated': 'Recovery codes generated.',
51 'userSettings.ssh.title': 'SSH keys',
52 'userSettings.ssh.sub':
53 'Used for cloning and pushing over SSH. Key fingerprints are unique platform-wide; duplicates are rejected.',
54 'userSettings.ssh.duplicate':
55 'This key is already registered (fingerprints are unique platform-wide and may belong to another account).',
56 'userSettings.ssh.deleted': 'The key does not exist or was deleted. Refresh the list.',
57 'userSettings.ssh.unparsable': 'The key could not be parsed. Make sure you pasted the complete single-line key.',
58 'userSettings.ssh.unsupported': 'Unsupported key type: only RSA, Ed25519 and ECDSA P-256 are accepted.',
59 'userSettings.ssh.nameLength': 'The name must be 1–64 characters.',
60 'userSettings.ssh.keyPlaceholder': 'ssh-ed25519 AAAA… user@host',
61 'userSettings.ssh.namePlaceholder': 'Key name',
62 'userSettings.ssh.add': 'Add key',
63 'userSettings.ssh.empty': 'No SSH keys yet.',
64 'userSettings.sessions.title': 'Devices & sessions',
65 'userSettings.sessions.sub': 'Devices you have signed in from and the sessions currently active.',
66 'userSettings.sessions.sessions': 'Sessions',
67 'userSettings.sessions.noSessions': 'No active sessions.',
68 'userSettings.sessions.kindWeb': 'Web sign-in',
69 'userSettings.sessions.kindKey': 'Access key',
70 'userSettings.sessions.current': 'This device',
71 'userSettings.sessions.revoked': 'Revoked',
72 'userSettings.sessions.createdAt': 'Created {when}',
73 'userSettings.sessions.revoke': 'Revoke',
74 'userSettings.sessions.revokeAll': 'Revoke all other sessions',
75 'userSettings.sessions.deviceDeleted': 'Device deleted.',
76 'userSettings.emails.title': 'Email addresses',
77 'userSettings.emails.sub': 'Sign-in addresses, the primary address and verification state.',
78 'userSettings.emails.refresh': 'Refresh list',
79 'userSettings.sessions.refresh': 'Refresh list',
80 'userSettings.security.sub': 'Password, two-factor authentication and recovery codes.',
81 'userSettings.security.totpHeading': 'Two-factor authentication (TOTP)',
82 'userSettings.security.status': 'Status:',
83 'userSettings.security.enabled': 'Enabled',
84 'userSettings.security.notEnabled': 'Not enabled',
85 'userSettings.security.setupHint':
86 'Add this secret to your authenticator app, then enter the 6-digit code to finish enabling it.',
87 'userSettings.security.enable': 'Enable two-factor',
88 'userSettings.security.secret': 'Secret',
89 'userSettings.security.codePlaceholder': 'Authenticator 6-digit code',
90 'userSettings.security.passwordConfirmPlaceholder': 'Current password (to confirm)',
91 'userSettings.security.verifyAndEnable': 'Verify and enable',
92 'userSettings.security.enabledNotice': 'Two-factor enabled — save the recovery codes below.',
93 'userSettings.security.codesOnce': 'Recovery codes (shown once; keep them safe)',
94 'userSettings.security.viewCodes': 'View recovery codes',
95 'userSettings.security.codesLoaded': 'Recovery codes loaded.',
96 'userSettings.security.regenerate': 'Regenerate',
97 'userSettings.security.regenPasswordPlaceholder': 'Current password (required to regenerate)',
98 'userSettings.security.code6Placeholder': '6-digit code',
99 'userSettings.security.disable': 'Disable two-factor',
100 'userSettings.ssh.registered': 'Registered keys ({count})',
101 'userSettings.ssh.none': 'No SSH keys have been added yet.',
102 'userSettings.ssh.addedAt': 'Added {when}',
103 'userSettings.ssh.confirmDelete': 'Confirm delete',
104 'userSettings.ssh.addedNotice': 'SSH key deleted.',
105 'userSettings.ssh.modalTitle': 'Add SSH key',
106 'userSettings.ssh.modalDescription': 'Accepts a single-line OpenSSH key: RSA, Ed25519 or ECDSA P-256.',
107 'userSettings.ssh.generateHint': 'No key yet? Generate one first',
108 'userSettings.ssh.privateHint': 'The public key lives in ',
109 'userSettings.ssh.privateHintTail': ' (never share the private key).',
110 'userSettings.ssh.nameLabel': 'Name',
111 'userSettings.ssh.nameExample': 'e.g. work-laptop',
112 'userSettings.ssh.nameHint': '1–64 characters, only to help you tell devices apart.',
113 'userSettings.ssh.contentLabel': 'Public key',
114 'userSettings.ssh.contentHint':
115 'Paste the whole single line; newlines and extra whitespace are folded automatically.',
116 'userSettings.ssh.pasteRequired': 'Paste the SSH public key content.',
117 'userSettings.ssh.badFormat': 'Unrecognized key format — it should look like ssh-ed25519 AAAAC3Nza… comment.',
118 'userSettings.ssh.addedFlash': 'Added "{name}".',
119 'userSettings.notifications.digest': 'Receive the daily email digest',
120 'userSettings.notifications.muteUntil': 'Do not disturb until',
121 'userSettings.notifications.muteHint': 'Leave empty to disable do-not-disturb.',
122 'userSettings.status.sub': 'Suspensions and restrictions (read-only).',
123 'userSettings.status.clean': 'No enforcement history — the account is in good standing.',
124 'userSettings.status.permanent': 'Permanent',
125 'userSettings.appearance.example': 'Example · {style}',
126 'userSettings.appearance.splitExample': 'Split (original left, new right)',
127 'userSettings.appearance.unifiedExample': 'Unified (single column)',
128 'userSettings.appearance.themeColor': 'Theme palette',
129 'userSettings.appearance.currentPalette': 'Using "{name}" · rendered {mode} · stored on this device',
130 'userSettings.appearance.resetDefault': 'Reset to default',
131 'userSettings.privacy.dms': 'Allow direct messages',
132 'userSettings.privacy.mentions': 'Allow mentions',
133 'userSettings.privacy.online': 'Show online status',
134} as const
Added view/app/src/i18n/messages/zh-CN/auth.ts View view/app/src/i18n/messages/zh-CN/auth.ts
@@ -0,0 +1,89 @@
1/** Authentication flow: shell chrome plus the six auth screens. */
2export const auth = {
3 'auth.shell.breadcrumb': '面包屑导航',
4 'auth.shell.switchToLight': '切换到浅色主题',
5 'auth.shell.switchToDark': '切换到深色主题',
6 'auth.shell.region': '区域 {region}',
7 'auth.login.crumb': '登录',
8 'auth.login.hint': '同一账号 5 分钟内最多尝试 5 次',
9 'auth.login.heading': '登录 GitDataAI',
10 'auth.login.subheading': '登录后进入仓库、看板与团队协作。',
11 'auth.login.identifier': '邮箱或用户名',
12 'auth.login.passphrase': '密码',
13 'auth.login.forgot': '忘记密码?',
14 'auth.login.keepSignedIn': '保持登录状态',
15 'auth.login.keepSignedInAria': '保持登录状态',
16 'auth.login.submit': '登录',
17 'auth.login.losingAccess': '登录遇到问题?',
18 'auth.login.resendVerify': '重发验证邮件',
19 'auth.login.noAccount': '还没有账号?',
20 'auth.login.requestAccess': '创建账号',
21 'auth.forgot.crumb': '找回访问',
22 'auth.forgot.hint': '重置链接 30 分钟内有效',
23 'auth.forgot.heading': '忘记密码?',
24 'auth.forgot.subheading': '输入账号邮箱,我们会发送一次性重置链接,30 分钟内有效。',
25 'auth.forgot.email': '账号邮箱',
26 'auth.forgot.privacyNote': '若邮箱已注册,你会收到重置邮件;出于安全考虑,不会提示账号是否存在。',
27 'auth.forgot.submit': '发送重置链接',
28 'auth.forgot.remembered': '想起来了?',
29 'auth.forgot.backToSignIn': '返回登录',
30 'auth.reset.crumb': '重置密码',
31 'auth.reset.hint': '修改后所有已登录设备需重新登录',
32 'auth.reset.heading': '设置新密码',
33 'auth.reset.subheading': '链接已验证,请设置新密码。',
34 'auth.reset.subheadingInvalid': '请从邮件中打开重置链接以继续。',
35 'auth.reset.noAddress': '未填写邮箱',
36 'auth.reset.tokenValid': '链接有效',
37 'auth.reset.noToken': '链接无效',
38 'auth.reset.newPassword': '新密码',
39 'auth.reset.confirmPassword': '确认新密码',
40 'auth.reset.strength.strong': '强',
41 'auth.reset.strength.fair': '中',
42 'auth.reset.strength.weak': '弱',
43 'auth.reset.error.mismatch': '两次输入的密码不一致。',
44 'auth.reset.noTokenNote': '请从邮件中打开重置链接,或',
45 'auth.reset.requestNewLink': '重新获取链接',
46 'auth.reset.done': '密码已更新,正在返回登录页…',
47 'auth.reset.submit': '更新密码',
48 'auth.verify.crumb': '验证邮箱',
49 'auth.verify.hint': '没收到邮件?请检查垃圾邮件目录',
50 'auth.verify.heading': '查看你的收件箱',
51 'auth.verify.subheading': '验证邮件已发送至',
52 'auth.verify.resetSubheading': '重置邮件已发送至',
53 'auth.verify.purpose': '用途 · {purpose}',
54 'auth.verify.purpose.signup': '注册',
55 'auth.verify.purpose.password_reset': '重置密码',
56 'auth.verify.purpose.add_email': '添加邮箱',
57 'auth.verify.purpose.reclaim': '账号找回',
58 'auth.verify.linkOpened': '链接已打开,点击下方按钮完成验证。',
59 'auth.verify.linkExpiry': '邮件中的验证码和链接 10 分钟内有效。',
60 'auth.verify.resetBody': '请在 30 分钟内通过邮件中的链接设置新密码。',
61 'auth.verify.verified': '邮箱已验证,正在返回登录页…',
62 'auth.verify.resent': '验证邮件已重新发送至 {email}。',
63 'auth.verify.resetResent': '重置邮件已重新发送至 {email}。',
64 'auth.verify.waiting': '请打开邮件中的验证链接完成验证。',
65 'auth.verify.confirm': '完成验证',
66 'auth.verify.divider': '没收到?',
67 'auth.verify.resend': '重发验证邮件',
68 'auth.verify.resendCountdown': '重发验证邮件({time})',
69 'auth.verify.resetResend': '重发重置邮件',
70 'auth.verify.resetResendCountdown': '重发重置邮件({time})',
71 'auth.verify.wrongAddress': '邮箱填错了?',
72 'auth.verify.changeEmail': '返回修改',
73 'auth.twofa.crumb': '两步验证',
74 'auth.twofa.hint': '验证码 5 分钟内有效 · 最多尝试 5 次',
75 'auth.twofa.heading': '两步验证',
76 'auth.twofa.subheading': '请输入验证器应用中的 6 位验证码。',
77 'auth.twofa.recoveryCode': '恢复码',
78 'auth.twofa.authCode': '验证码',
79 'auth.twofa.expiresIn': '{time} 后过期',
80 'auth.twofa.noChallengeError': '登录请求已过期,请重新登录。',
81 'auth.twofa.noChallengeNote': '登录请求已过期 —— 请先',
82 'auth.twofa.signIn': '登录',
83 'auth.twofa.submit': '验证',
84 'auth.twofa.trouble': '遇到问题?',
85 'auth.twofa.useAuthenticator': '改用验证器验证码',
86 'auth.twofa.useRecovery': '使用恢复码',
87 'auth.error.network': '网络连接失败,请检查网络后重试。',
88 'auth.error.unknown': '出错了,请稍后重试。',
89} as const
Added view/app/src/i18n/messages/zh-CN/chat.ts View view/app/src/i18n/messages/zh-CN/chat.ts
@@ -0,0 +1,350 @@
1/** Chat: sidebar lists, composer, transcript, threads and voice. */
2export const chat = {
3 'chat.rooms.title': '频道',
4 'chat.rooms.empty': '还没有频道',
5 'chat.rooms.create': '新建频道',
6 'chat.rooms.name': '频道名称',
7 'chat.rooms.topic': '话题',
8 'chat.rooms.private': '私有频道',
9 'chat.rooms.created': '频道已创建。',
10 'chat.rooms.updated': '频道已更新。',
11 'chat.rooms.deleted': '频道已删除。',
12 'chat.dms.title': '私信',
13 'chat.dms.empty': '还没有私信',
14 'chat.upload.failedStatus': '上传失败({status})',
15 'chat.composer.placeholder': '发送消息到 #{room}',
16 'chat.composer.send': '发送',
17 'chat.composer.attach': '添加附件',
18 'chat.composer.emoji': '表情',
19 'chat.composer.tooLong': '消息过长(最多 {max} 个字符)。',
20 'chat.composer.empty': '消息不能为空。',
21 'chat.composer.sent': '消息已发送。',
22 'chat.composer.failed': '发送失败,请稍后重试。',
23 'chat.composer.unauthorized': '登录状态已失效,请重新登录',
24 'chat.composer.forbidden': '没有权限在该频道发言',
25 'chat.composer.notFound': '频道或消息已不存在',
26 'chat.composer.rateLimited': '发送太快了,请稍后再试',
27 'chat.composer.unavailable': '服务未响应,消息未发送',
28 'chat.message.edited': '已编辑',
29 'chat.message.deleted': '消息已删除',
30 'chat.message.edit': '编辑消息',
31 'chat.message.delete': '删除消息',
32 'chat.message.reply': '回复',
33 'chat.message.thread': '话题',
34 'chat.message.save': '收藏',
35 'chat.message.unsave': '取消收藏',
36 'chat.message.pin': '置顶',
37 'chat.message.unpin': '取消置顶',
38 'chat.message.copyLink': '复制链接',
39 'chat.message.saved': '已收藏消息。',
40 'chat.message.unsaved': '已取消收藏。',
41 'chat.message.deletedNotice': '消息已删除。',
42 'chat.message.editedNotice': '消息已更新。',
43 'chat.message.deleteConfirm': '确定删除这条消息吗?此操作不可撤销。',
44 'chat.day.today': '今天',
45 'chat.day.yesterday': '昨天',
46 'chat.day.weekday': '周{weekday}',
47 'chat.thread.title': '话题',
48 'chat.thread.empty': '还没有话题',
49 'chat.thread.create': '新建话题',
50 'chat.thread.replies': '{count} 条回复',
51 'chat.thread.archived': '已归档',
52 'chat.thread.archivedNotice': '话题已归档。',
53 'chat.attachment.image': '图片',
54 'chat.attachment.video': '视频',
55 'chat.attachment.audio': '音频',
56 'chat.attachment.pdf': 'PDF',
57 'chat.attachment.text': '文本',
58 'chat.attachment.file': '文件',
59 'chat.attachment.download': '下载附件',
60 'chat.attachment.tooLarge': '附件超过大小上限。',
61 'chat.attachment.uploadFailed': '附件上传失败,请重试。',
62 'chat.voice.join': '加入语音',
63 'chat.voice.leave': '离开语音',
64 'chat.voice.mute': '静音',
65 'chat.voice.unmute': '取消静音',
66 'chat.voice.stage': '舞台',
67 'chat.voice.connected': '已连接语音频道',
68 'chat.voice.disconnected': '已离开语音频道',
69 'chat.voice.restore': '恢复声音',
70 'chat.voice.failed': '无法连接语音频道,请重试。',
71 'chat.voice.members': '在线成员',
72 'chat.voice.empty': '频道内还没有其他人',
73 'chat.voice.membersCount': '已有 {count} 人在频道里',
74 'chat.member.list': '成员',
75 'chat.member.invite': '邀请成员',
76 'chat.member.online': '在线',
77 'chat.mention.everyone': '提及所有人',
78 'chat.notification.newMessage': '{author} 在 #{room} 中提到了你',
79 'chat.notification.reply': '{author} 回复了你的消息',
80 'chat.notification.viewChannel': '查看频道',
81 // Room header, connection banner and room lifecycle.
82 'chat.room.slowModeTitle': '慢速模式:每 {seconds} 秒可发一条',
83 'chat.room.dmTopicPlaceholder': '每一段交流,都从一句问候开始',
84 'chat.room.topicPlaceholder': '在这里分享想法,一起把事情做好',
85 'chat.room.transcriptPane': '文字记录',
86 'chat.room.callPane': '通话视图',
87 'chat.room.pins': '置顶消息',
88 'chat.room.threads': '话题列表',
89 'chat.room.members': '频道成员',
90 'chat.room.membersWithCount': '频道成员 · {count} 人',
91 'chat.room.options': '频道选项',
92 'chat.room.mute': '静音此频道',
93 'chat.room.unmute': '取消静音',
94 'chat.room.favorite': '收藏此频道',
95 'chat.room.unfavorite': '取消收藏',
96 'chat.room.clearDraft': '清空草稿',
97 'chat.room.closeDm': '关闭此会话',
98 'chat.room.settings': '频道设置',
99 'chat.room.info': '频道信息',
100 'chat.room.dmSettings': '会话设置',
101 'chat.room.ownedByViewer': '频道由你拥有',
102 'chat.room.leave': '离开频道',
103 'chat.room.details': '频道详情',
104 'chat.room.accessDenied': '你没有权限查看这个频道',
105 'chat.room.gone': '这个频道不存在或已被删除',
106 'chat.room.goneHint': '链接可能已经过期,或者频道的所有者把它删掉了。从左侧列表选择其他频道。',
107 'chat.room.settingsNoteDm': '私信会话只能改名以外的少量设置;关闭会话会从列表中移除。',
108 'chat.room.settingsNoteManage': '这些字段写入房间本身,所有成员都会看到变化。',
109 'chat.room.settingsNoteView': '你只有读取权限,以下设置为只读。',
110 'chat.room.nameHint': '小写字母、数字与连字符最稳妥',
111 'chat.room.topicHint': '显示在标题栏,最多 1024 字符',
112 'chat.room.private': '私密频道(仅成员可读)',
113 'chat.room.rateLimit': '慢速模式(秒)',
114 'chat.room.rateLimitHint': '0 表示关闭;成员两条消息之间的最短间隔',
115 'chat.room.deleteNote': '删除后频道不再对外可见,消息也不会恢复。',
116 'chat.room.deleteConfirm': '确认删除频道',
117 'chat.room.deleteAction': '删除此频道',
118 'chat.room.saved': '已保存',
119 'chat.room.slowModeShort': '慢速模式 {seconds}s',
120
121 // Realtime connection banner.
122 'chat.connection.authRequired': '实时连接需要重新登录',
123 'chat.connection.stopped': '实时连接已停止,消息不会自动更新',
124 'chat.connection.connecting': '正在连接实时通道…',
125 'chat.connection.closed': '实时通道已断开,消息不会自动更新',
126 'chat.connection.retry': '重连',
127
128 // Transcript.
129 'chat.transcript.aria': '聊天消息',
130 'chat.transcript.accessDenied': '你已无法访问这个频道',
131 'chat.transcript.accessDeniedHint': '服务器撤回了这里的读取权限。请从左侧选择其他频道。',
132 'chat.transcript.loadingOlder': '加载更早的消息…',
133 'chat.transcript.threadStart': '话题从这里开始',
134 'chat.transcript.start': '每一段精彩的交流,都从这里开始',
135 'chat.transcript.loadFailed': '消息加载失败',
136 'chat.transcript.seedFailed': '无法加载最新消息',
137 'chat.transcript.threadEmptyTitle': '让讨论继续',
138 'chat.transcript.threadEmptyBody': '分享你的想法,成为这段讨论的第一位回应者。',
139 'chat.transcript.roomEmptyTitle': '欢迎来到 {room}',
140 'chat.transcript.roomEmptyBody': '一个新话题、一点新灵感,或者一句简单的你好。让我们从这里开始。',
141 'chat.transcript.newMessages': '{count} 条新消息',
142 'chat.transcript.newMessages_one': '{count} 条新消息',
143 'chat.transcript.newMessages_other': '{count} 条新消息',
144 'chat.transcript.backToLatest': '回到最新消息',
145 'chat.transcript.pinned': '已置顶',
146 'chat.transcript.replyDeleted': '引用了已删除的消息',
147 'chat.transcript.thisDeleted': '(此消息已删除)',
148 'chat.transcript.deletedShort': '(已删除)',
149 'chat.transcript.jumpToOriginal': '跳转到原消息',
150 'chat.transcript.actions': '消息操作',
151 'chat.transcript.typingOne': '{name} 正在输入…',
152 'chat.transcript.typingTwo': '{first} 和 {second} 正在输入…',
153 'chat.transcript.typingMany': '{names} 正在输入…',
154 'chat.transcript.mine': '我',
155 'chat.transcript.unsent': '未发送',
156 'chat.transcript.sending': '发送中',
157 'chat.transcript.sendFailed': '发送失败',
158 'chat.transcript.editHint': 'Enter 保存 · Shift+Enter 换行 · Esc 取消',
159 'chat.transcript.react': '表情回应',
160 'chat.transcript.replyInThread': '在话题中回复',
161 'chat.transcript.quoteReply': '引用回复',
162 'chat.transcript.copied': '已复制',
163 'chat.transcript.copyText': '复制文本',
164 'chat.transcript.more': '更多操作',
165 'chat.transcript.lastReply': '最后回复 {time}',
166 'chat.transcript.locked': '已锁定',
167
168 // Reactions and unread markers.
169 'chat.reaction.aria': '{emoji},{count} 人回应',
170 'chat.reaction.ariaMine': '{emoji},{count} 人回应,你已回应',
171 'chat.reaction.title': '{count} 人回应',
172 'chat.unread.count': '{count} 条未读',
173 'chat.unread.label': '未读',
174
175 // System lines written by the backend into a room.
176 'chat.system.integration': '集成',
177 'chat.system.postWithContent': '{author}:{content}',
178 'chat.system.post': '{author} 发布了一条消息',
179 'chat.system.threadCreated': '{author} 开启了一个话题',
180 'chat.system.joined': '{author} 加入了这个频道',
181 'chat.system.left': '{author} 离开了这个频道',
182 'chat.system.topicChanged': '{author} 把主题改为「{topic}」',
183 'chat.system.topicUpdated': '{author} 更新了频道主题',
184 'chat.system.pinned': '{author} 置顶了一条消息',
185 'chat.system.reminder': '{author} 设置了提醒:{content}',
186 'chat.system.reminderSet': '{author} 设置了一个提醒',
187 'chat.system.message': '系统消息',
188 'chat.system.sent': '{author} 发送了一条消息',
189
190 // Thread panels.
191 'chat.panel.close': '关闭面板',
192 'chat.panel.resize': '调整侧栏宽度',
193 'chat.panel.clearFocus': '恢复聊天区清晰度',
194 'chat.panel.loadedMembers': '加载社区成员…',
195 'chat.panel.searching': '搜索中…',
196 'chat.thread.lockedNotice': '这个话题已被锁定,无法继续回复',
197 'chat.thread.archivedReadOnly': '这个话题已归档,只能查看',
198 'chat.thread.byOwner': '{name} 的话题',
199 'chat.thread.fromOwner': '来自 {name} 的话题',
200 'chat.thread.summary': '{replies} 条回复 · {participants} 人参与',
201 'chat.thread.participants': '{count} 人',
202 'chat.thread.participants_one': '{count} 人',
203 'chat.thread.participants_other': '{count} 人',
204 'chat.thread.count': '{count} 个',
205 'chat.thread.follow': '开启提醒',
206 'chat.thread.followTitle': '话题内每条新回复都提醒我',
207 'chat.thread.following': '已开启提醒',
208 'chat.thread.followingTitle': '新回复会提醒你;点击可关闭提醒',
209 'chat.thread.unfollow': '关闭提醒',
210 'chat.thread.unfollowTitle': '取消话题提醒',
211 'chat.thread.lock': '锁定话题(停止新回复)',
212 'chat.thread.unlock': '解锁话题',
213 'chat.thread.locked': '已锁定',
214 'chat.thread.missing': '未找到',
215 'chat.thread.missingHint': '这个话题可能已被删除,或仍在加载中。关闭后可以从「话题列表」重新进入。',
216 'chat.thread.emptyHint': '还没有话题。在消息的操作栏选择「在话题中回复」,让讨论更有条理。',
217
218 // Pin list.
219 'chat.pins.count': '{count} 条',
220 'chat.pins.empty': '还没有置顶消息。把鼠标移到一条消息上,在「更多操作」里置顶。',
221 'chat.pins.jump': '跳转到该消息',
222 'chat.pins.jumpShort': '跳转',
223
224 // Member roster.
225 'chat.member.count': '{count} 人',
226 'chat.member.count_one': '{count} 人',
227 'chat.member.count_other': '{count} 人',
228 'chat.member.owner': '房主',
229 'chat.member.forbidden': '这是私密频道,成员列表未对你开放。',
230 'chat.member.loadFailed': '成员列表加载失败。',
231 'chat.member.self': '这是你自己',
232 'chat.member.dmTitle': '与 @{username} 私信',
233 'chat.member.addCount': '添加 ({count})',
234 'chat.member.allInRoom': '社区里的人已经都在这个频道里了。',
235 'chat.member.listFailed': '社区成员读取失败',
236
237 // Search panel.
238
239 // Attachments.
240 'chat.attachment.unknownType': '未知类型',
241 'chat.attachment.preview': '预览',
242 'chat.attachment.closePreview': '关闭预览',
243
244 // Emoji picker.
245 'chat.emoji.common': '常用',
246 'chat.emoji.gestures': '手势',
247 'chat.emoji.symbols': '符号',
248 'chat.emoji.custom': '社区',
249 'chat.emoji.search': '搜索表情',
250 'chat.emoji.searchClear': '清除搜索',
251
252 // Voice surface.
253 'chat.voice.channelFallback': '语音频道',
254 'chat.voice.callAria': '通话',
255 'chat.voice.barDragHint': '按住空白处可拖动,双击复位',
256 'chat.voice.mic': '麦克风',
257 'chat.voice.camera': '摄像头',
258 'chat.voice.screenShare': '共享屏幕',
259 'chat.voice.noPublish': '当前没有发言权',
260 'chat.voice.return': '回到语音频道',
261 'chat.voice.hangUp': '离开通话',
262 'chat.voice.disabled': '语音功能未启用',
263 'chat.voice.joinFailed': '加入语音失败',
264 'chat.voice.handFailed': '举手失败',
265 'chat.voice.handCancelFailed': '撤回失败',
266 'chat.voice.handDecideFailed': '审批失败',
267 'chat.voice.sharing': '{name} 正在共享屏幕',
268 'chat.voice.stageHint': '这是舞台频道:加入后可收听;想要发言请举手,主持人通过后自动开麦。',
269 'chat.voice.joinHint': '加入语音后会开启麦克风,可随时静音或关闭摄像头。',
270 'chat.voice.handPending': '已举手,等待主持人通过',
271 'chat.voice.cancelHand': '撤回举手',
272 'chat.voice.joining': '正在加入…',
273 'chat.voice.tilePlaced': '拖动可摆位,右下角缩放,双击放回网格',
274 'chat.voice.tileHint': '按住可拖到任意空位',
275 'chat.voice.forceUnmute': '解除静音',
276 'chat.voice.forceMute': '强制静音',
277 'chat.voice.kick': '移出通话',
278 'chat.voice.tileSpeaking': '{name} 正在说话',
279 'chat.voice.solo': '还没有其他人和你一起,邀请成员加入吧。',
280 'chat.voice.hands': '发言申请',
281 'chat.voice.canSpeak': '已获得发言权',
282 'chat.voice.raiseHand': '举手申请发言',
283 'chat.voice.pendingCount': '{count} 待处理',
284 'chat.voice.noHands': '暂无待处理申请',
285 'chat.voice.revoke': '收回',
286 'chat.voice.speaking': '{name} · 发言中',
287 'chat.voice.notice.micDenied': '未授予麦克风权限,将以只听模式加入',
288 'chat.voice.notice.connectFailed': '语音连接失败',
289 'chat.voice.notice.kicked': '你已被管理员移出通话',
290 'chat.voice.notice.duplicate': '该账号已在其他地方加入通话',
291 'chat.voice.notice.dropped': '语音连接已中断,请点击重新加入',
292 'chat.voice.notice.micUnavailable': '无法访问麦克风',
293 'chat.voice.notice.cameraUnavailable': '无法访问摄像头',
294 'chat.voice.notice.screenUnavailable': '无法共享屏幕',
295 'chat.voice.notice.audioBlocked': '浏览器仍阻止了音频播放,请检查站点声音权限',
296 'chat.voice.notice.mediaUnreachable':
297 '语音通道没有建立起来:浏览器连不上语音服务。请重新加入;一直失败请联系管理员。',
298 'chat.voice.notice.permissionDenied': '浏览器拒绝了麦克风/摄像头权限,请在地址栏恢复后重试',
299 'chat.voice.notice.tokenInvalid': '语音令牌无效或已过期,请重新加入',
300 'chat.voice.notice.deviceMissing': '没有找到可用的设备,请检查麦克风/摄像头是否连接。',
301 'chat.voice.notice.deviceInUse': '设备被其他应用占用,请关闭占用后重试。',
302 'chat.voice.notice.browserUnsupported': '当前浏览器不支持语音通话,请使用 Chrome 或 Edge。',
303 'chat.voice.mutedByHost': '你已被主持人静音。',
304 'chat.voice.unmutedByHost': '主持人已恢复你的发言权。',
305 'chat.voice.reconnecting': '语音连接中断,正在自动重连…',
306 'chat.voice.reconnectFailed': '语音自动重连失败,请点击重新加入。',
307 'chat.voice.rejoin': '重新加入',
308 'chat.voice.deviceSystemDefault': '系统默认',
309 'chat.voice.deviceOff': '关闭',
310 'chat.voice.joinWithMic': '进入频道时开启麦克风',
311 'chat.voice.deviceNoMic': '授权后才能看到麦克风列表',
312 'chat.voice.deviceNoOutput': '系统默认输出',
313 'chat.voice.deviceNoCamera': '授权后才能看到摄像头列表',
314 'chat.voice.debugDetails': '连接细节',
315 'chat.voice.deviceTitle': '设备与预览',
316 'chat.voice.deviceHint': '选好设备后直接进入频道,麦克风按你的选择开启。',
317 'chat.voice.deviceJoin': '进入频道',
318 'chat.voice.speaker': '扬声器',
319 'chat.voice.micTest': '说句话,下面的音量条会跟着动',
320 'chat.voice.micLevel': '麦克风音量',
321 'chat.voice.volume': '{name} 的音量',
322 'chat.voice.focus': '聚焦 {name}',
323 'chat.voice.clearFocus': '取消聚焦',
324 'chat.voice.shareAudio': '同时共享电脑声音',
325 'chat.voice.shareCancelled': '已取消屏幕共享',
326 'chat.voice.autoLayout': '恢复自动排列',
327 'chat.voice.prevPage': '上一页',
328 'chat.voice.nextPage': '下一页',
329 'chat.voice.aria.joined': '{name} 加入了语音',
330 'chat.voice.aria.left': '{name} 离开了语音',
331 'chat.voice.tileMenu': '{name} 的操作',
332 'chat.voice.qualityHigh': '网络良好',
333 'chat.voice.qualityMedium': '网络一般',
334 'chat.voice.qualityLow': '网络较差',
335 'chat.voice.qualityUnknown': '网络状态未知',
336
337 // Bullet comments (danmaku).
338 'chat.danmaku.title': '弹幕记录',
339 'chat.danmaku.count': '{count} 条',
340 'chat.danmaku.loadOlder': '加载更早',
341 'chat.danmaku.empty': '还没有弹幕',
342 'chat.danmaku.flying': '实时飘屏',
343 'chat.danmaku.placeholder': '发条弹幕…',
344 'chat.danmaku.label': '弹幕',
345
346 // Composer leftovers.
347 'chat.composer.attachmentLimit': '一条消息最多 {max} 个附件,多出的没有添加',
348 'chat.composer.blockedUploading': '附件上传中,完成后会自动可发送',
349 'chat.composer.blockedFailed': '有附件上传失败,请重试或移除后再发送',
350} as const
Added view/app/src/i18n/messages/zh-CN/chatComposer.ts View view/app/src/i18n/messages/zh-CN/chatComposer.ts
@@ -0,0 +1,30 @@
1/** Chat composer: entry hints, mention list, attachment strip, send states. */
2export const chatComposer = {
3 'chat.composer.editLabel': '编辑消息',
4 'chat.composer.replyLabel': '回复 {author}',
5 'chat.composer.someMessage': '某条消息',
6 'chat.composer.escCancel': 'Esc 取消',
7 'chat.composer.uploadFailedShort': '失败',
8 'chat.composer.uploading': '上传中…',
9 'chat.composer.retryUpload': '重试上传',
10 'chat.composer.removeFile': '移除 {name}',
11 'chat.composer.entryHint': '发消息给 {room},分享你的想法…',
12 'chat.composer.inputAria': '消息输入框',
13 'chat.composer.mentionMembers': '提及成员',
14 'chat.composer.addAttachmentHint': '添加附件(也可以拖入或粘贴文件)',
15 'chat.composer.addAttachment': '添加附件',
16 'chat.composer.insertEmoji': '插入表情',
17 'chat.composer.enterSends': '发送',
18 'chat.composer.shiftEnterNewline': '换行',
19 'chat.composer.sendAria': '发送',
20 'chat.composer.slowModeWait': '慢速模式,{seconds}s 后可发送',
21 'chat.composer.tooLongBy': '超出 {count} 字',
22 'chat.composer.sendWithEnter': '发送(Enter)',
23 'chat.composer.save': '保存',
24 'chat.composer.tooLongDetail': '超出 {over} 字(上限 {max})',
25 'chat.composer.slowModeDetail': '此频道开启慢速模式:每 {seconds}s 可发一条',
26 'chat.composer.attachmentPlaceholder': '[附件]',
27 'chat.composer.cancel': '取消',
28 'chat.composer.draftSaveFailed': '草稿保存失败(消息本身不受影响)',
29 'chat.composer.overLimit': '超过 {mb} MB 上限',
30} as const
Added view/app/src/i18n/messages/zh-CN/common.ts View view/app/src/i18n/messages/zh-CN/common.ts
@@ -0,0 +1,51 @@
1/** Brand and language-neutral copy shared by every screen. */
2export const common = {
3 'common.app.name': 'GitDataAI',
4 'common.action.save': '保存',
5 'common.action.cancel': '取消',
6 'common.action.close': '关闭',
7 'common.action.confirm': '确认',
8 'common.action.create': '创建',
9 'common.action.delete': '删除',
10 'common.action.edit': '编辑',
11 'common.action.remove': '移除',
12 'common.action.retry': '重试',
13 'common.action.copy': '复制',
14 'common.action.copied': '已复制',
15 'common.action.refresh': '刷新',
16 'common.action.loading': '加载中…',
17 'common.action.working': '处理中…',
18 'common.action.back': '返回',
19 'common.action.next': '下一步',
20 'common.action.search': '搜索',
21 'common.action.upload': '上传',
22 'common.action.download': '下载',
23 'common.action.enable': '启用',
24 'common.action.disable': '停用',
25 'common.state.none': '无',
26 'common.state.all': '全部',
27 'common.state.empty': '暂无内容',
28 'common.state.unknown': '未知',
29 'common.state.enabled': '已启用',
30 'common.state.disabled': '已停用',
31 'common.state.optional': '可选',
32 'common.state.required': '必填',
33 'common.state.busy': '处理中…',
34 'common.time.justNow': '刚刚',
35 'common.time.expiresIn': '{time} 后过期',
36 'common.theme.light': '浅色主题',
37 'common.theme.dark': '深色主题',
38 'common.language.label': '界面语言',
39 'common.language.system': '跟随系统',
40 'common.language.saved': '界面语言已更新。',
41 'common.confirm.unsaved': '有未保存的更改,确定要离开吗?',
42 'common.copy.failed': '复制失败,请手动选中后复制。',
43 'common.state.unknownUser': '未知用户',
44 'common.state.noSelection': '暂无可选项',
45 'common.action.dismiss': '丢弃',
46 'common.action.ignore': '忽略',
47 'common.action.add': '添加',
48 'common.action.clear': '清空',
49 'common.action.approve': '通过',
50 'common.action.reject': '拒绝',
51} as const
Added view/app/src/i18n/messages/zh-CN/errors.ts View view/app/src/i18n/messages/zh-CN/errors.ts
@@ -0,0 +1,44 @@
1/** Shared error surface plus HTTP/status fallbacks for localized API errors. */
2export const errors = {
3 'error.status.400': '请求参数有误,请检查后重试。',
4 'error.status.401': '登录状态已失效,请重新登录。',
5 'error.status.403': '你没有执行该操作的权限。',
6 'error.status.404': '请求的资源不存在或已被删除。',
7 'error.status.409': '操作与当前状态冲突,请刷新后重试。',
8 'error.status.429': '请求过于频繁,请稍后再试。',
9 'error.status.500': '服务端出错了,请稍后重试。',
10 'error.status.502': '服务暂时不可用,请稍后重试。',
11 'error.status.503': '服务暂时不可用,请稍后重试。',
12 'error.status.504': '服务响应超时,请稍后重试。',
13 'error.status.0': '网络错误(无响应)。',
14 'error.status.unknown': '请求失败,请稍后重试。',
15 'error.code.bad_request': '请求参数有误,请检查后重试。',
16 'error.code.unauthorized': '登录状态已失效,请重新登录。',
17 'error.code.token_expired': '登录状态已过期,正在自动续期。',
18 'error.code.session_revoked': '会话已被撤销,请重新登录。',
19 'error.code.forbidden': '你没有执行该操作的权限。',
20 'error.code.not_found': '请求的资源不存在或已被删除。',
21 'error.code.conflict': '操作与当前状态冲突,请刷新后重试。',
22 'error.code.rate_limited': '请求过于频繁,请稍后再试。',
23 'error.code.invalid_credentials': '账号或密码不正确。',
24 'error.code.pow_failed': '工作量证明校验失败,请重试。',
25 'error.code.invalid_request_body': '请求内容格式不正确。',
26 'error.code.merge_blocked': '当前分支保护规则阻止了这次合并。',
27 'error.code.import_disabled': '仓库导入功能当前已关闭。',
28 'error.code.import_upstream_failed': '无法访问上游仓库,请检查地址或凭据。',
29 'error.code.password_change_required': '需要先修改密码才能继续。',
30 'error.gate.title': '服务暂不可用',
31 'error.gate.body': '接口调用持续失败(连续 {count} 次请求无成功响应),页面已暂停渲染。请确认后端服务是否已启动。',
32 'error.gate.lastFailure': '最近一次失败:{reason}{path} | 窗口失败率 {percent}%({failures}/{samples})',
33 'error.gate.noResponse': '无响应',
34 'error.gate.networkError': '网络错误(无响应)',
35 'error.gate.serverError': 'HTTP {status} · 服务端错误',
36 'error.gate.retry': '重试连接',
37 'error.gate.connecting': '连接中…',
38 'error.gate.probeInterval': '每 {seconds}s 自动探测一次',
39 'error.gate.lastProbe': '上次 {time}',
40 'error.gate.imageAlt': '服务暂不可用',
41 'error.notFound.title': '页面不存在',
42 'error.notFound.body': '你访问的页面不存在或已被移动。',
43 'error.notFound.back': '返回工作台',
44} as const
Added view/app/src/i18n/messages/zh-CN/index.ts View view/app/src/i18n/messages/zh-CN/index.ts
@@ -0,0 +1,44 @@
1import { auth } from './auth'
2import { chat } from './chat'
3import { chatComposer } from './chatComposer'
4import { common } from './common'
5import { errors } from './errors'
6import { issueEvents, issues } from './issues'
7import { labels } from './labels'
8import { livekit } from './livekit'
9import { misc } from './misc'
10import { nav } from './nav'
11import { repo } from './repo'
12import { settings } from './settings'
13import { shell } from './shell'
14import { space } from './space'
15import { ui } from './ui'
16import { userSettings } from './userSettings'
17
18/**
19 * The reference dictionary. Spreading the per-domain modules keeps them
20 * reviewable in isolation while this file stays the single source of truth for
21 * the key set: `MessageKey` is derived from it and the English dictionary is
22 * checked against it, so drift between the two fails `tsc`.
23 */
24export const zhCN = {
25 ...common,
26 ...auth,
27 ...errors,
28 ...shell,
29 ...labels,
30 ...livekit,
31 ...settings,
32 ...chat,
33 ...chatComposer,
34 ...repo,
35 ...issues,
36 ...issueEvents,
37 ...space,
38 ...misc,
39 ...nav,
40 ...ui,
41 ...userSettings,
42} as const
43
44export type MessageKey = keyof typeof zhCN
Added view/app/src/i18n/messages/zh-CN/issues.ts View view/app/src/i18n/messages/zh-CN/issues.ts
@@ -0,0 +1,570 @@
1/** Issue board and pull requests. */
2export const issues = {
3 'issue.board.new': '新建工单',
4 'issue.board.manage': '管理看板',
5 'issue.board.empty': '还没有工单',
6 'issue.board.emptyNote': '创建一个工单开始跟踪工作。',
7 'issue.board.viewBoard': '看板',
8 'issue.board.viewList': '列表',
9 'issue.board.filter.state': '状态',
10 'issue.board.filter.priority': '优先级',
11 'issue.board.filter.assignee': '负责人',
12 'issue.board.filter.label': '标签',
13 'issue.board.filter.milestone': '里程碑',
14 'issue.board.filter.sort': '排序',
15 'issue.board.filter.search': '搜索工单',
16 'issue.board.filter.reset': '重置筛选',
17 'issue.detail.title': '标题',
18 'issue.detail.description': '描述',
19 'issue.detail.assignee': '负责人',
20 'issue.detail.labels': '标签',
21 'issue.detail.milestone': '里程碑',
22 'issue.detail.dueDate': '截止日期',
23 'issue.detail.comments': '评论',
24 'issue.detail.commentPlaceholder': '写下评论…',
25 'issue.detail.commentSubmit': '发表评论',
26 'issue.detail.noComments': '还没有评论',
27 'issue.detail.activity': '活动记录',
28 'issue.detail.close': '关闭工单',
29 'issue.detail.reopen': '重新打开',
30 'issue.detail.closed': '工单已关闭。',
31 'issue.detail.reopened': '工单已重新打开。',
32 'issue.detail.created': '工单已创建。',
33 'issue.detail.updated': '工单已更新。',
34 'issue.detail.deleted': '工单已删除。',
35 'issue.detail.deleteConfirm': '确定删除这个工单吗?',
36 'issue.board.name': '看板名称',
37 'issue.board.created': '看板已创建。',
38 'issue.board.updated': '看板已更新。',
39 'issue.board.deleted': '看板已删除。',
40 'issue.label.create': '新建标签',
41 'issue.label.color': '颜色',
42 'issue.label.name': '标签名称',
43 'issue.milestone.create': '新建里程碑',
44 'issue.milestone.title': '里程碑标题',
45 'issue.milestone.due': '截止日期',
46 'issue.watch.watching': '关注',
47 'issue.watch.unwatched': '默认',
48 'issue.watch.ignore': '忽略',
49 'pull.new': '新建拉取请求',
50 'pull.title': '标题',
51 'pull.description': '描述',
52 'pull.base': '目标分支',
53 'pull.head': '来源分支',
54 'pull.created': '拉取请求已创建。',
55 'pull.updated': '拉取请求已更新。',
56 'pull.empty': '还没有拉取请求',
57 'pull.tabs.conversation': '讨论',
58 'pull.tabs.files': '文件变更',
59 'pull.tabs.commits': '提交',
60 'pull.state.open': '待合并',
61 'pull.state.merged': '已合并',
62 'pull.state.closed': '已关闭',
63 'pull.merge': '合并',
64 'pull.mergeMethod': '合并方式',
65 'pull.mergeMethod.merge': '创建合并提交',
66 'pull.mergeMethod.squash': '压缩合并',
67 'pull.mergeMethod.rebase': '变基合并',
68 'pull.merged': '拉取请求已合并。',
69 'pull.closedNotice': '拉取请求已关闭。',
70 'pull.reopened': '拉取请求已重新打开。',
71 'pull.draft': '草稿',
72 'pull.readyForReview': '标记为可评审',
73 'pull.blockedHint': '分支保护规则阻止了这次合并。',
74 'pull.commentPlaceholder': '留下评论…',
75 'pull.approve': '批准',
76 'pull.requestChanges': '请求修改',
77 'pull.reviewSubmitted': '评审已提交。',
78 'pull.filesChanged': '文件变更',
79} as const
80
81/** Issue activity-line verbs, keyed by the backend's event type. */
82export const issueEvents = {
83 'issue.event.unknown': '更新了工单',
84 'issue.event.fallback': '更新了工单({type})',
85 'issue.event.created': '创建了工单',
86 'issue.event.title_updated': '修改了标题',
87 'issue.event.description_updated': '修改了摘要',
88 'issue.event.content_updated': '修改了正文',
89 'issue.event.milestone_updated': '调整了里程碑',
90 'issue.event.priority_updated': '调整了优先级',
91 'issue.event.due_updated': '调整了截止日期',
92 'issue.event.closed': '关闭了工单',
93 'issue.event.reopened': '重新打开了工单',
94 'issue.event.comment_created': '添加了评论',
95 'issue.event.comment_updated': '编辑了评论',
96 'issue.event.comment_deleted': '删除了评论',
97 'issue.event.member_added': '添加了成员',
98 'issue.event.member_removed': '移除了成员',
99 'issue.event.label_added': '添加了标签',
100 'issue.event.label_removed': '移除了标签',
101 'issue.event.dependency_added': '添加了依赖',
102 'issue.event.dependency_removed': '移除了依赖',
103 'issue.event.pinned': '置顶了工单',
104 'issue.event.unpinned': '取消置顶',
105 'issue.event.repo_relation_added': '关联了仓库',
106 'issue.event.repo_relation_removed': '取消了仓库关联',
107 'issue.event.verb.milestone_updated': '将里程碑 {from} → {to}',
108 'issue.event.verb.title_updated': '将标题 {from} → {to}',
109 'issue.event.verb.label_added': '添加了标签 {name}',
110 'issue.event.verb.label_removed': '移除了标签 {name}',
111 'issue.event.verb.member_added': '添加了成员 {name}',
112 'issue.event.verb.member_removed': '移除了成员 {name}',
113 'issue.event.verb.pull_linked': '关联了合并请求 {name}',
114 'issue.event.verb.pull_unlinked': '解除了合并请求关联 {name}',
115 'issue.filter.searchPlaceholder': '搜索工单…',
116 'issue.filter.searchAria': '搜索工单标题',
117 'issue.filter.clearSearch': '清空搜索',
118 'issue.filter.statusGroup': '状态筛选',
119 'issue.filter.allPriorities': '全部优先级',
120 'issue.filter.labelsFailed': '标签加载失败',
121 'issue.filter.allLabels': '全部标签',
122 'issue.filter.allMilestones': '全部里程碑',
123 'issue.filter.noMilestone': '未设里程碑',
124 'issue.filter.allAssignees': '全部负责人',
125 'issue.filter.sortActive': '排序:{label}',
126 'issue.filter.viewGroup': '视图切换',
127 'issue.filter.clear': '清除筛选',
128 'issue.filter.panelTitle': '筛选',
129 'issue.filter.searchIn': '搜索{title}',
130 'issue.filter.searchHint': '按 / 聚焦搜索',
131 'issue.filter.clearAll': '全部清除',
132 'issue.filter.done': '完成',
133 'issue.board.resultCount': '共 {count} 个结果',
134 'issue.board.more': '更多操作',
135 'issue.board.settingsAction': '工单设置',
136 'issue.board.newHint': '新建工单 · 快捷键 C',
137 'issue.filter.chip.sort': '排序:{value}',
138 'issue.filter.chip.state': '状态:{value}',
139 'issue.filter.chip.query': '关键词:{value}',
140 'issue.filter.chip.priority': '优先级:{value}',
141 'issue.filter.chip.label': '标签:{value}',
142 'issue.filter.chip.milestone': '里程碑:{value}',
143 'issue.filter.chip.assignee': '负责人:{value}',
144 'issue.filter.unknownLabel': '未知标签',
145 'issue.filter.noMilestoneShort': '未设',
146 'issue.filter.unknownMilestone': '未知',
147 'issue.filter.remove': '移除筛选 {text}',
148 'issue.board.manageTitle': '问题集设置、标签与里程碑',
149 'issue.board.manageButton': '管理',
150 'issue.board.errorList': '工单列表',
151 'issue.board.pageOutOfRange': '第 {page} 页超出范围:共 {total} 个工单,每页 {limit} 条。',
152 'issue.board.backToFirst': '回到第 1 页',
153 'issue.board.emptyFilteredTitle': '没有符合条件的工单',
154 'issue.board.emptyFilteredHint': '调整或清除上方筛选后重试。',
155 'issue.board.emptyTitle': '这个问题集还没有工单',
156 'issue.board.emptyHint': '把想法变成清晰的下一步,从一个工单开始。',
157 'issue.board.milestoneClipped':
158 '「未设里程碑」按当前页筛选(后端不支持该条件),共 {total} 个工单中另有 {hidden} 条被隐藏。',
159 'issue.board.refreshing': '正在刷新…',
160 'issue.board.moveFailed': '移动到「{priority}」失败:{reason}',
161 'issue.board.columnAria': '{priority}优先级,{count} 个工单',
162 'issue.board.addInColumn': '在「{column}」新建工单',
163 'issue.board.addCard': '添加一个工单',
164 'issue.board.addCardHint': '也可以将卡片拖到这里',
165 'issue.board.cardAria': '工单 {number} {title}',
166 'issue.board.milestoneTitle': '里程碑:{name}',
167 'issue.board.tableCaption': '工单列表,可选择标题查看详情',
168 'issue.board.col.state': '状态',
169 'issue.board.col.number': '编号',
170 'issue.board.col.title': '标题',
171 'issue.board.col.priority': '优先级',
172 'issue.board.col.milestone': '里程碑',
173 'issue.board.col.assignee': '负责人',
174 'issue.board.col.updated': '更新',
175 'issue.board.changePriority': '修改优先级',
176 'issue.board.pager': '第 {page} / {pages} 页 · 共 {total} 个工单',
177 'issue.board.prev': '上一页',
178 'issue.board.next': '下一页',
179 'issue.board.loading': '正在加载工单',
180 'issue.board.backToBoard': '返回看板',
181 'issue.board.errorIssue': '工单',
182 'issue.board.loadingIssue': '正在加载工单…',
183 'issue.board.actionFailed': '操作失败:{reason}',
184 'issue.board.actionFailedGeneric': '操作失败,请稍后重试。',
185 'issue.board.edit': '编辑',
186 'issue.board.closeIssue': '关闭工单',
187 'issue.board.reopen': '重新打开',
188 'issue.board.closedIssue': '已关闭该工单。',
189 'issue.board.reopenedIssue': '已重新打开。',
190 'issue.board.noBody': '该工单没有正文描述。',
191 'issue.board.unknownAuthor': '未知作者',
192 'issue.board.createdAt': '{author} 创建于 {time}',
193 'issue.board.updatedAt': '更新于 {time}',
194 'issue.board.closedAt': '关闭于 {time}',
195 'issue.board.activity': '活动记录',
196 'issue.board.activityCount': '{count} 条',
197 'issue.board.collapse': '收起',
198 'issue.board.expand': '展开',
199 'issue.board.properties': '属性',
200 'issue.board.showEarlier': '显示更早的 {count} 条动态',
201 'issue.board.hideEarlier': '收起更早的动态',
202 'issue.board.unassigned': '未指派',
203 'issue.board.send': '发送',
204 'issue.board.priorityField': '优先级',
205 'issue.board.milestoneField': '里程碑',
206 'issue.board.dueField': '截止日期',
207 'issue.board.unset': '未设置',
208 'issue.board.clearDate': '清除日期',
209 'issue.board.assigneeField': '负责人',
210 'issue.board.labelsField': '标签',
211 'issue.board.labelsFailed': '标签加载失败',
212 'issue.board.noLabels': '未打标签',
213 'issue.board.linkedPulls': '关联合并请求',
214 'issue.board.notifyField': '通知',
215 'issue.board.milestoneUnknown': '已关闭或已删除',
216 'issue.board.deleteFailed': '删除失败:{reason}',
217 'issue.board.linkFailed': '关联失败',
218 'issue.board.unlink': '解除关联',
219 'issue.board.stateOpen': '进行中',
220 'issue.board.noPullLinks': '尚未关联合并请求。',
221 'issue.board.repoPlaceholder': '仓库名',
222 'issue.board.noRepos': '这个社区还没有仓库',
223 'issue.board.pullPlaceholder': 'PR 编号',
224 'issue.board.pickPull': '选择 PR',
225 'issue.board.pickRepoFirst': '先选仓库',
226 'issue.board.noOpenPulls': '这个仓库没有待合并的 PR',
227 'issue.board.link': '关联',
228 'issue.board.noMembers': '没有可选成员',
229 'issue.board.assignAria': '指派给 {name}',
230 'issue.board.done': '完成',
231 'issue.board.addLabel': '添加标签',
232 'issue.board.allLabelsBound': '所有标签都已绑定。',
233 'issue.board.comments': '评论',
234 'issue.board.loadingComments': '加载评论…',
235 'issue.board.commentsFailed': '评论加载失败,重新打开工单可重试。',
236 'issue.board.noComments': '还没有评论。',
237 'issue.board.commentSendFailed': '评论发送失败。',
238 'issue.board.commentPlaceholder': '添加评论…(Ctrl+Enter 发送)',
239 'issue.board.newCommentAria': '新评论',
240 'issue.board.submitComment': '发表评论',
241 'issue.board.unknownUser': '未知用户',
242 'issue.board.edited': '已编辑',
243 'issue.board.editCommentAria': '编辑评论',
244 'issue.board.replyFailed': '回复失败。',
245 'issue.board.replyPlaceholder': '回复…(Ctrl+Enter 发送,Esc 收起)',
246 'issue.board.reply': '回复',
247 'issue.board.deleteIssue': '删除工单',
248 'issue.board.linkPull': '关联 MR',
249 'issue.board.pickRepo': '选择仓库',
250 'issue.board.event.runExpand': '共 {count} 次改动,展开(最近 {time})',
251 'issue.board.event.runCollapse': '收起改动',
252 'issue.board.replyAria': '回复 {name}',
253 'issue.board.commentFallback': '评论',
254 'issue.board.loadingActivity': '加载活动…',
255 'issue.board.activityFailed': '活动记录加载失败。',
256 'issue.board.noActivity': '暂无活动记录。',
257 'issue.board.system': '系统',
258 'issue.board.form.createTitle': '新建工单',
259 'issue.board.form.editTitle': '编辑工单 {number}',
260 'issue.board.form.editDescription': '让信息保持清晰,协作就更快。',
261 'issue.board.form.validationTitle': '请填写标题。',
262 'issue.board.form.validationTitleMax': '标题最长 {max} 个字符。',
263 'issue.board.form.saveFailed': '保存失败,请稍后重试。',
264 'issue.board.form.attach': '添加附件',
265 'issue.board.form.removeAttachment': '移除附件 {name}',
266 'issue.board.form.attachmentTooLarge': '文件过大,上限 {max}',
267 'issue.board.form.uploadFailed': '附件上传失败',
268 'issue.board.form.preview': '预览 Markdown',
269 'issue.board.form.editSource': '继续编辑',
270 'issue.board.attachment.open': '打开附件 {name}',
271 'issue.board.form.title': '标题',
272 'issue.board.form.titlePlaceholder': '给这个工单一个清晰的标题',
273 'issue.board.form.summary': '摘要',
274 'issue.board.form.summaryOptional': '(选填)',
275 'issue.board.form.summaryPlaceholder': '一句话说明影响范围',
276 'issue.board.form.content': '详细描述',
277 'issue.board.form.contentPlaceholder': '补充详细描述(复现步骤、期望结果…)',
278 'issue.board.form.noLabels': '这个问题集还没有标签,可在「管理 · 标签」中创建。',
279 'issue.board.form.noAssignees': '暂时没有可指派的成员,可以稍后再添加负责人。',
280 'issue.board.manage.title': '问题集管理',
281 'issue.board.manage.description': '重命名、维护标签与里程碑,或删除整个问题集。',
282 'issue.board.manage.general': '常规',
283 'issue.board.manage.labels': '标签',
284 'issue.board.manage.milestones': '里程碑',
285 'issue.board.manage.runFailed': '操作失败,请稍后重试。',
286 'issue.board.manage.processing': '处理中…',
287 'issue.board.manage.name': '名称',
288 'issue.board.manage.prefix': '编号前缀',
289 'issue.board.manage.prefixHint':
290 '工单编号会带上该前缀,例如 CAD-12;留空则使用 #1 形式。字母开头,1–10 位字母或数字。',
291 'issue.board.manage.prefixPlaceholder': '例如 CAD',
292 'issue.board.manage.prefixInvalid': '前缀需以字母开头,仅含字母和数字,最多 10 位。',
293 'issue.board.manage.descriptionField': '描述',
294 'issue.board.manage.stats': '统计:未关闭 {open} · 已关闭 {closed}',
295 'issue.board.manage.deleteBoard': '删除问题集',
296 'issue.board.manage.labelsFailed': '标签列表加载失败。',
297 'issue.board.manage.noLabels': '还没有标签。',
298 'issue.board.manage.newLabel': '新标签名称',
299 'issue.board.manage.useColor': '使用颜色 {color}',
300 'issue.board.manage.selectColor': '选择颜色',
301 'issue.board.manage.labelName': '标签名称',
302 'issue.board.manage.saving': '保存中…',
303 'issue.board.manage.saved': '已保存',
304 'issue.board.manage.milestonesFailed': '里程碑列表加载失败。',
305 'issue.board.manage.noMilestones': '还没有里程碑。',
306 'issue.board.manage.milestoneName': '里程碑名称',
307 'issue.board.manage.due': '截止',
308 'issue.board.manage.close': '关闭',
309 'issue.board.manage.newMilestone': '新里程碑名称',
310 'issue.board.manage.milestoneDue': '里程碑截止日期',
311 'issue.board.manage.add': '添加',
312 'issue.board.closeDialog': '关闭',
313 'issue.board.noOptions': '暂无可选项',
314 'issue.board.confirmDiscard': '再次点击确认',
315 'pull.detail.tab.conversation': '会话',
316 'pull.detail.tab.checks': '检查',
317 'pull.detail.loading': '加载合并请求…',
318 'pull.detail.errorPull': '合并请求',
319 'pull.detail.notFound': '合并请求不存在。',
320 'pull.detail.crumb': '合并请求',
321 'pull.detail.requestsMerge': '请求将 {head} 合并到 {base}',
322 'pull.detail.updatedAt': '更新于 {time}',
323 'pull.detail.locked': '已锁定',
324 'pull.detail.autoMergeOn': '自动合并已开',
325 'pull.detail.complete': '{label}完成。',
326 'pull.detail.failed': '{label}失败:{reason}',
327 'pull.detail.actionFailed': '{label}失败:{reason}',
328 'pull.detail.close': '关闭',
329 'pull.detail.reopen': '重开',
330 'pull.detail.unlock': '解锁',
331 'pull.detail.lock': '锁定',
332 'pull.detail.unlockDiscussion': '解锁讨论',
333 'pull.detail.lockDiscussion': '锁定讨论',
334 'pull.detail.refreshSummary': '刷新摘要',
335 'pull.detail.patch': '补丁',
336 'pull.detail.deleteConfirm': '删除 #{number}?该操作不可恢复。',
337 'pull.detail.deleteFailed': '删除失败:{reason}',
338 'pull.detail.actions': '操作',
339 'pull.detail.reviewers': '评审人',
340 'pull.detail.required': '必需',
341 'pull.detail.noReviewers': '尚未请求评审。',
342 'pull.detail.reviewRequestPlaceholder': '搜索成员,请求评审',
343 'pull.detail.assignees': '受理人',
344 'pull.detail.noAssignees': '还没有受理人。',
345 'pull.detail.assigneePlaceholder': '搜索成员,添加受理人',
346 'pull.detail.labels': '标签',
347 'pull.detail.noLabels': '没有标签。',
348 'pull.detail.labelPlaceholder': '搜索标签,附加到本 PR',
349 'pull.detail.pickNoMatch': '没有匹配的选项',
350 'pull.detail.analyzingMerge': '正在分析合并冲突…',
351 'pull.detail.stats': '统计',
352 'pull.detail.aheadBehind': '领先 / 落后',
353 'pull.detail.filesChanged': '文件变更',
354 'pull.detail.lines': '增删行',
355 'pull.detail.created': '创建',
356 'pull.detail.mergedAt': '合并于',
357 'pull.detail.mergeCommit': '合并提交',
358 'pull.detail.merge': '合并',
359 'pull.detail.draftMergeHint': '这是草稿 PR——点击右上角「编辑」将其转为正式后再评审合并。',
360 'pull.detail.conflicts': '存在 {count} 个冲突文件,无法直接合并:',
361 'pull.detail.blockedByRules': '被保护规则拦截:{reasons}',
362 'pull.detail.needsApprovals': '需要 {required} 个批准(当前 {current})',
363 'pull.detail.canBypass': '(你可绕过)',
364 'pull.detail.staleBranch': '源分支落后于目标分支 {count} 个提交。',
365 'pull.detail.readyToMerge': '满足合并条件',
366 'pull.detail.approvals': '(批准 {current}/{required})',
367 'pull.detail.syncBranch': '同步目标分支到源分支',
368 'pull.detail.syncFailed': '同步分支失败:{reason}',
369 'pull.detail.mergeTitlePlaceholder': '合并标题(默认 PR 标题)',
370 'pull.detail.mergeMessagePlaceholder': '合并说明(默认取提交信息)',
371 'pull.detail.deleteBranchAfter': '合并后删除源分支',
372 'pull.detail.merging': '合并中…',
373 'pull.detail.autoMerge': '满足条件后自动合并',
374 'pull.detail.autoMergeFailed': '自动合并设置失败:{reason}',
375 'pull.detail.mergeSucceeded': '合并成功:{sha}',
376 'pull.detail.mergeFailed': '合并失败:{reason}',
377 'pull.detail.viewConflict': '点击查看冲突内容',
378 'pull.detail.noConflictText': '(无冲突文本可展示)',
379 'pull.detail.conflictContent': '冲突内容 · {path}',
380 'pull.detail.loadingConflict': '加载冲突文本…',
381 'pull.detail.updateFailed': '更新失败:{reason}',
382 'pull.detail.editNumber': '编辑 #{number}',
383 'pull.detail.draftCheckbox': '草稿(不接收评审与合并)',
384 'pull.detail.requestReviewFailed': '请求评审失败:{reason}',
385 'pull.detail.remove': '移除 {name}',
386 'pull.detail.addAssigneeFailed': '添加受理人失败:{reason}',
387 'pull.detail.attachLabelFailed': '附加标签失败:{reason}',
388 'pull.conv.locked': '该 PR 讨论已锁定,写操作可能被服务端拒绝。',
389 'pull.conv.saveFailed': '保存失败',
390 'pull.conv.deleteConfirm': '删除这条评论?',
391 'pull.conv.edited': '(已编辑)',
392 'pull.conv.edit': '编辑',
393 'pull.conv.delete': '删除',
394 'pull.conv.inline': '行内评审评论',
395 'pull.conv.thread': '会话评论',
396 'pull.conv.resolved': '已解决',
397 'pull.conv.markResolved': '标记解决',
398 'pull.conv.replyPlaceholder': '回复这条评论…',
399 'pull.conv.send': '发送',
400 'pull.conv.reply': '回复',
401 'pull.conv.official': '正式',
402 'pull.conv.inlineCount': '{count} 条行内评论',
403 'pull.conv.publishApprove': '批准并发布',
404 'pull.conv.publishRequestChanges': '请求修改并发布',
405 'pull.conv.publishComment': '仅评论发布',
406 'pull.conv.dismiss': '撤销评审',
407 'pull.conv.loadDetails': '加载评审详情(含 {count} 条行内意见的作者与状态)',
408 'pull.conv.mode.comment': '普通评论',
409 'pull.conv.mode.approve': '批准',
410 'pull.conv.mode.requestChanges': '请求修改',
411 'pull.conv.empty': '内容不能为空。',
412 'pull.conv.submitFailed': '提交失败',
413 'pull.conv.append': '追加记录',
414 'pull.conv.write': '写评论 / 提交评审',
415 'pull.conv.commentPlaceholder': '评论这个合并请求…',
416 'pull.conv.reviewPlaceholder': '附上评审意见…',
417 'pull.conv.draftReviewHint': '草稿 PR 通常不接受评审,服务端可能拒绝。',
418 'pull.conv.submitting': '提交中…',
419 'pull.conv.post': '发表评论',
420 'pull.conv.submitReview': '提交评审',
421 'pull.conv.clear': '清空',
422 'pull.files.commentFailed': '评论失败',
423 'pull.files.reviewFailed': '评审提交失败',
424 'pull.files.loading': '加载变更文件…',
425 'pull.files.errorFiles': '变更文件',
426 'pull.files.empty': '没有文件变更。',
427 'pull.files.summary': '{count} 个文件改动',
428 'pull.files.compareVersion': '对比版本',
429 'pull.files.linePlaceholder': '评论 {path}:{line}({side})…',
430 'pull.files.sideOld': '旧行',
431 'pull.files.sideNew': '新行',
432 'pull.files.postLine': '直接评论此行',
433 'pull.files.queueReview': '加入待提交评审',
434 'pull.files.pendingTitle': '待提交的评审({count} 条行内意见)',
435 'pull.files.removeDraft': '移除该条意见',
436 'pull.files.reviewSummaryPlaceholder': '评审总结(可选)…',
437 'pull.files.approve': '批准',
438 'pull.files.requestChanges': '请求修改',
439 'pull.files.commentOnly': '仅评论',
440 'pull.files.saveAsPending': '存为待提交评审',
441 'pull.files.discard': '丢弃',
442 'pull.files.status.added': '新增',
443 'pull.files.status.deleted': '删除',
444 'pull.files.status.renamed': '重命名',
445 'pull.files.status.copied': '复制',
446 'pull.files.status.binary': '二进制',
447 'pull.files.status.modified': '修改',
448 'pull.mergeable.clean': '可合并',
449 'pull.mergeable.blocked': '被门禁阻止',
450 'pull.mergeable.dirty': '存在冲突',
451 'pull.mergeable.unstable': '检查未通过',
452 'pull.mergeable.hasHooks': '等待钩子',
453 'pull.mergeable.unknown': '状态未知',
454 'pull.parts.binaryDiff': '二进制文件,不显示差异内容。',
455 'pull.parts.metaOnly': '仅元数据变更。',
456 'pull.parts.addComment': '在此行添加评审评论',
457 'pull.parts.truncated': '差异过长,已截断显示前 1500 行。',
458 'pull.checks.pending': '排队中',
459 'pull.checks.running': '运行中',
460 'pull.checks.success': '通过',
461 'pull.checks.failed': '失败',
462 'pull.checks.cancelled': '已取消',
463 'pull.checks.waiting': '等待',
464 'pull.checks.error': '错误',
465 'pull.checks.loading': '加载检查…',
466 'pull.checks.errorChecks': '检查',
467 'pull.checks.empty': '没有检查数据。',
468 'pull.checks.versionEmpty': '该版本({sha})没有流水线运行或提交状态。',
469 'pull.checks.version': '版本',
470 'pull.checks.details': '详情',
471 'pull.commits.loading': '加载提交…',
472 'pull.commits.errorList': '提交列表',
473 'pull.commits.empty': '该合并请求没有提交。',
474 'pull.commits.noSubject': '(无标题)',
475 'pull.commits.unknownAuthor': '未知',
476 'pull.commits.mergeCommit': '合并提交({count} 个父提交)',
477 'pull.commits.view': '查看提交',
478 'pull.list.scope.created': '我发起的',
479 'pull.list.scope.reviewRequested': '待我评审',
480 'pull.list.scope.reviewed': '我评审过的',
481 'pull.list.repo': '本仓库',
482 'pull.list.mine': '我的 PR',
483 'pull.list.searchPlaceholder': '搜索标题/描述',
484 'pull.list.new': '新建合并请求',
485 'pull.list.loading': '加载合并请求…',
486 'pull.list.errorList': '合并请求列表',
487 'pull.list.emptyInbox': '这个收件箱是空的。',
488 'pull.list.empty': '没有符合条件的合并请求。',
489 'pull.list.pager': '共 {total} 条',
490 'pull.list.total': '共 {total} 条',
491 'pull.form.title': '新建合并请求',
492 'pull.form.description': '{repo} · 将源分支合入目标分支',
493 'pull.form.missingFields': '标题、源分支和目标分支都不能为空。',
494 'pull.form.sameBranch': '源分支和目标分支不能相同。',
495 'pull.form.createFailed': '创建失败',
496 'pull.form.creating': '创建中…',
497 'pull.form.createDraft': '创建草稿',
498 'pull.form.create': '创建合并请求',
499 'pull.form.head': '源分支(head)',
500 'pull.form.base': '目标分支(base)',
501 'pull.form.comparing': '正在比较 {base} … {head}…',
502 'pull.form.fastForward': '可以快进合并,无冲突',
503 'pull.form.willMergeCommit': '(将生成合并提交)',
504 'pull.form.conflicts': '存在 {count} 处冲突',
505 'pull.form.conflictsHint': ',仍可创建 PR,但需要先解决冲突才能合并。',
506 'pull.form.titlePlaceholder': '合并 {head} 到 {base}',
507 'pull.form.titlePlaceholderFallback': '简短描述这次改动',
508 'pull.form.bodyPlaceholder': '说明改动动机、验证方式与风险点…',
509 'pull.form.draftCheckbox': '以草稿创建(标记为 Draft,ready 后再请求评审)',
510 'pull.form.descriptionField': '描述(支持换行,纯文本渲染)',
511 'issue.board.removeLabel': '移除标签 {name}',
512 'issue.board.filterLabelPrefix': '优先级',
513 'issue.board.form.createDescription': '记录一个问题,让协作有据可循。',
514 'issue.board.filter.due': '截止日期',
515 'issue.board.milestoneLabel': '里程碑 {name}',
516 'issue.board.pullState': '({state})',
517 'issue.filter.triggerAria': '{label}:{value}',
518 'pull.reviewState.approved': '已批准',
519 'pull.reviewState.changesRequested': '请求修改',
520 'pull.reviewState.commented': '评论',
521 'pull.reviewState.pending': '待提交',
522 'pull.reviewState.dismissed': '已撤销',
523 'pull.reviewerState.requested': '待评审',
524 'pull.reviewerState.approved': '已批准',
525 'pull.reviewerState.changesRequested': '请求修改',
526 'pull.reviewerState.commented': '已评论',
527 'pull.reviewerState.dismissed': '已撤销',
528 'pull.event.created': '创建了本合并请求',
529 'pull.event.updated': '更新了合并请求',
530 'pull.event.titleChanged': '修改了标题',
531 'pull.event.bodyEdited': '编辑了描述',
532 'pull.event.baseChanged': '改动了目标分支',
533 'pull.event.markedReady': '转为正式合并请求',
534 'pull.event.markedDraft': '转为草稿',
535 'pull.event.closed': '关闭了合并请求',
536 'pull.event.reopened': '重新打开了合并请求',
537 'pull.event.merged': '合并了本合并请求',
538 'pull.event.reviewRequested': '请求了评审',
539 'pull.event.reviewRequestRemoved': '移除了评审请求',
540 'pull.event.reviewSubmitted': '提交了评审',
541 'pull.event.approved': '批准了本合并请求',
542 'pull.event.changesRequested': '请求修改',
543 'pull.event.comment': '发表了评论',
544 'pull.event.labeled': '添加了标签',
545 'pull.event.unlabeled': '移除了标签',
546 'pull.event.assigned': '添加了受理人',
547 'pull.event.unassigned': '移除了受理人',
548 'pull.event.locked': '锁定了讨论',
549 'pull.event.unlocked': '解锁了讨论',
550 'pull.event.forcePush': '向源分支强推了提交',
551 'pull.event.branchUpdated': '同步了目标分支',
552 'pull.event.headRefUpdated': '更新了源分支',
553 'pull.event.autoMergeEnabled': '开启了自动合并',
554 'pull.event.autoMergeDisabled': '关闭了自动合并',
555 'pull.badge.draft': '草稿',
556 'pull.files.summary_one': '{count} 个文件改动',
557 'pull.files.summary_other': '{count} 个文件改动',
558 'issue.board.pager_one': '第 {page} / {pages} 页 · 共 {total} 个工单',
559 'issue.board.pager_other': '第 {page} / {pages} 页 · 共 {total} 个工单',
560 'issue.board.activityCount_one': '{count} 条',
561 'issue.board.activityCount_other': '{count} 条',
562 'issue.board.columnAria_one': '{priority}优先级,{count} 个工单',
563 'issue.board.columnAria_other': '{priority}优先级,{count} 个工单',
564 'issue.board.pageOutOfRange_one': '第 {page} 页超出范围:共 {total} 个工单,每页 {limit} 条。',
565 'issue.board.pageOutOfRange_other': '第 {page} 页超出范围:共 {total} 个工单,每页 {limit} 条。',
566 'pull.conv.inlineCount_one': '{count} 条行内评论',
567 'pull.conv.inlineCount_other': '{count} 条行内评论',
568 'pull.list.total_one': '共 {total} 条',
569 'pull.list.total_other': '共 {total} 条',
570} as const
Added view/app/src/i18n/messages/zh-CN/labels.ts View view/app/src/i18n/messages/zh-CN/labels.ts
@@ -0,0 +1,44 @@
1/** Domain label tables: status/priority/role names reused by tables and filters. */
2export const labels = {
3 'label.priority.urgent': '紧急',
4 'label.priority.high': '高',
5 'label.priority.medium': '中',
6 'label.priority.low': '低',
7 'label.priority.none': '无优先级',
8 'label.issueState.open': '未关闭',
9 'label.issueState.closed': '已关闭',
10 'label.issueState.all': '全部',
11 'label.issueSort.created': '最新创建',
12 'label.issueSort.updated': '最近更新',
13 'label.issueSort.due': '截止日期',
14 'label.issueSort.priority': '优先级',
15 'label.issueSort.number': '编号',
16 'label.issueView.board': '看板视图',
17 'label.issueView.list': '列表视图',
18 'label.role.owner': '所有者',
19 'label.role.admin': '管理员',
20 'label.role.member': '成员',
21 'label.role.guest': '访客',
22 'label.visibility.public': '公开',
23 'label.visibility.registered': '仅注册用户',
24 'label.visibility.members': '仅成员',
25 'label.visibility.private': '私有',
26 'label.audience.everyone': '所有人',
27 'label.audience.following': '仅我关注的人',
28 'label.audience.nobody': '没有人',
29 'label.runStatus.queued': '排队中',
30 'label.runStatus.running': '运行中',
31 'label.runStatus.success': '成功',
32 'label.runStatus.failed': '失败',
33 'label.runStatus.cancelled': '已取消',
34 'label.runStatus.skipped': '已跳过',
35 'label.authEvent.push': '推送',
36 'label.authEvent.pullRequest': '拉取请求',
37 'label.authEvent.comment': '评论',
38 'label.authEvent.issue': '工单',
39 'label.presence.online': '在线',
40 'label.presence.away': '离开',
41 'label.presence.busy': '忙碌',
42 'label.presence.offline': '离线',
43 'label.presence.invisible': '隐身',
44} as const
Added view/app/src/i18n/messages/zh-CN/livekit.ts View view/app/src/i18n/messages/zh-CN/livekit.ts
@@ -0,0 +1,21 @@
1/** Copy owned by the vendored LiveKit component sources in shared/livekit. */
2export const livekit = {
3 'livekit.microphone': '麦克风',
4 'livekit.camera': '摄像头',
5 'livekit.shareScreen': '共享屏幕',
6 'livekit.stopScreenShare': '停止共享',
7 'livekit.chat': '聊天',
8 'livekit.settings': '设置',
9 'livekit.leave': '离开',
10 'livekit.audioInputs': '音频输入',
11 'livekit.videoInputs': '视频输入',
12 'livekit.allowAudio': '允许播放声音',
13 'livekit.startAudio': '开启声音',
14 'livekit.startVideo': '开启视频',
15 'livekit.reconnecting': '正在重新连接…',
16 'livekit.connecting': '正在连接…',
17 'livekit.disconnected': '连接已断开',
18 'livekit.chatPlaceholder': '输入消息…',
19 'livekit.screenSuffix': '的屏幕共享',
20 'livekit.participantPlaceholder': '暂无画面',
21}
Added view/app/src/i18n/messages/zh-CN/misc.ts View view/app/src/i18n/messages/zh-CN/misc.ts
@@ -0,0 +1,39 @@
1/** Notifications, search results and miscellany that lacks a home domain. */
2export const misc = {
3 'notifications.title': '通知',
4 'notifications.empty': '还没有通知',
5 'notifications.markAllRead': '全部标为已读',
6 'notifications.markedAll': '已全部标为已读。',
7 'notifications.unread': '未读',
8 'notifications.filter.all': '全部',
9 'notifications.filter.unread': '未读',
10 'notifications.filter.mentions': '提及',
11 'notifications.delete': '删除通知',
12 'notifications.deleted': '通知已删除。',
13 'notifications.sub': '邀请、指派与系统提醒留在这里;提及和回复直接回到所在的频道与工单。',
14 'notifications.filter': '通知筛选',
15 'notifications.emptyUnread': '没有未读通知。',
16 'notifications.emptyAll': '暂时没有需要你处理的通知。',
17 'notifications.inviteByEmail': '这条邀请请在邮件中处理。',
18 'notifications.inviteVoid': '这条邀请已失效:可能已在别处接受、被撤销或已过期。',
19 'notifications.joined': '已加入',
20 'notifications.reject': '拒绝',
21 'notifications.accept': '接受邀请',
22 'notifications.type.mention': '提及',
23 'notifications.type.reply': '回复',
24 'notifications.type.follow': '关注',
25 'notifications.type.invite': '邀请',
26 'notifications.type.assignment': '指派',
27 'notifications.type.system': '系统',
28 'notifications.target.issue': '查看工单 #{number}',
29 'notifications.target.room': '打开频道',
30 'notifications.target.space': '进入社区',
31 'notifications.target.joinRequests': '处理加入申请',
32 'notifications.joinRequestSubmitted': '有新的加入申请',
33 'notifications.joinRequestApproved': '申请已通过',
34 'notifications.joinRequestRejected': '申请未通过',
35 'notifications.bell': '通知',
36 'notifications.bellUnread': '通知,{count} 条未读',
37 'notifications.region': '系统通知',
38 'notifications.closeToast': '关闭通知',
39} as const
Added view/app/src/i18n/messages/zh-CN/nav.ts View view/app/src/i18n/messages/zh-CN/nav.ts
@@ -0,0 +1,13 @@
1/** Workspace navigation: sidebar rows, drag-and-drop notices, empty states. */
2export const nav = {
3 'nav.empty': '这里还没有内容。使用空间菜单创建频道,开始一起协作。',
4 'nav.emptyDms': '还没有私聊。从空间的成员资料发起一个私聊吧。',
5 'nav.dropHere': '放到这里:移到根目录',
6 'nav.savingPosition': '正在保存位置…',
7 'nav.moveFailed': '移动失败,请稍后重试。',
8 'nav.moveIntoSelf': '不能把文件夹移动到它自己或它的子文件夹里。',
9 'nav.unsupportedFolderMove': '后端未接受频道的文件夹归属(需更新并重启 apihost)。',
10 'nav.favorite': '已收藏',
11 'nav.inCall': '{count} 人在通话',
12 'nav.unreadCount': '未读数 {count}',
13} as const
Added view/app/src/i18n/messages/zh-CN/repo.ts View view/app/src/i18n/messages/zh-CN/repo.ts
@@ -0,0 +1,561 @@
1/** Repository surfaces: browse, history, branches, releases, settings. */
2export const repo = {
3 'repo.tabs.code': '代码',
4 'repo.tabs.commits': '提交',
5 'repo.tabs.branches': '分支',
6 'repo.tabs.tags': '标签',
7 'repo.tabs.releases': '发布',
8 'repo.tabs.settings': '设置',
9 'repo.tabs.pulls': '拉取请求',
10 'repo.unknownPath': '(未知路径)',
11 'repo.empty': '仓库中没有文件',
12 'repo.loading': '正在加载仓库…',
13 'repo.notFound': '仓库不存在或已删除。',
14 'repo.branch.default': '默认分支',
15 'repo.branch.switch': '切换分支',
16 'repo.branch.create': '新建分支',
17 'repo.branch.name': '分支名称',
18 'repo.branch.created': '分支已创建。',
19 'repo.branch.deleted': '分支已删除。',
20 'repo.branch.deleteConfirm': '确定删除分支 {branch} 吗?',
21 'repo.branch.empty': '没有分支',
22 'repo.tag.create': '新建标签',
23 'repo.tag.name': '标签名称',
24 'repo.tag.created': '标签已创建。',
25 'repo.tag.deleted': '标签已删除。',
26 'repo.tag.empty': '没有标签',
27 'repo.file.history': '文件历史',
28 'repo.file.download': '下载文件',
29 'repo.file.raw': '原始内容',
30 'repo.file.blame': '追溯',
31 'repo.file.copyPath': '复制路径',
32 'repo.file.tooLarge': '文件过大,无法预览。',
33 'repo.file.binary': '二进制文件不支持预览。',
34 'repo.clone.title': '克隆仓库',
35 'repo.clone.https': 'HTTPS',
36 'repo.clone.ssh': 'SSH',
37 'repo.clone.copied': '克隆地址已复制。',
38 'repo.clone.button': '克隆/代码',
39 'repo.clone.withSsh': '使用 SSH 克隆',
40 'repo.clone.withHttps': '使用 HTTPS 克隆',
41 'repo.clone.download': '下载源代码',
42 'repo.clone.downloadFailed': '打包失败,请重试。',
43 'repo.commit.message': '提交信息',
44 'repo.commit.author': '作者',
45 'repo.commit.date': '日期',
46 'repo.commit.sha': '提交哈希',
47 'repo.commit.parent': '父提交',
48 'repo.commit.empty': '没有提交记录',
49 'repo.commit.diffEmpty': '没有可显示的差异',
50 'repo.commit.unified': '统一视图',
51 'repo.commit.split': '分栏视图',
52 'repo.commit.whitespace': '忽略空白',
53 'repo.commit.filesChanged': '{count} 个文件变更',
54 'repo.commit.insights': '提交洞察',
55 'repo.release.create': '新建发布',
56 'repo.release.title': '发布标题',
57 'repo.release.tag': '标签',
58 'repo.release.notes': '发布说明',
59 'repo.release.draft': '草稿',
60 'repo.release.prerelease': '预发布',
61 'repo.release.published': '发布已创建。',
62 'repo.release.deleted': '发布已删除。',
63 'repo.release.empty': '还没有发布',
64 'repo.release.downloads': '下载量',
65 'repo.settings.general': '常规',
66 'repo.settings.name': '仓库名称',
67 'repo.settings.description': '仓库描述',
68 'repo.settings.visibility': '可见性',
69 'repo.settings.defaultBranch': '默认分支',
70 'repo.settings.saved': '仓库设置已保存。',
71 'repo.settings.danger': '危险操作',
72 'repo.settings.deleteRepo': '删除仓库',
73 'repo.settings.deleteConfirm': '删除后无法恢复,请输入仓库名称确认:{name}',
74 'repo.settings.deleted': '仓库已删除。',
75 'repo.settings.sections.protection': '分支保护',
76 'repo.settings.sections.archives': '归档',
77 'repo.settings.sections.remotes': '远程镜像',
78 'repo.settings.sections.locks': '文件锁',
79 'repo.settings.sections.lfs': 'LFS',
80 'repo.settings.sections.ci': 'CI 配置',
81 'repo.settings.sections.webhooks': 'Webhook',
82 'repo.settings.sections.collaborators': '协作者',
83 'repo.lfs.enabled': 'LFS 已启用',
84 'repo.lfs.patterns': '跟踪模式',
85 'repo.lfs.objects': 'LFS 对象',
86 'repo.action.add': '添加',
87 'repo.action.browseCode': '浏览代码',
88 'repo.action.confirmDelete': '确认删除',
89 'repo.action.details': '详情',
90 'repo.action.refreshing': '刷新中…',
91 'repo.action.rename': '重命名',
92 'repo.action.saving': '保存中…',
93 'repo.action.setDefault': '设为默认',
94 'repo.action.viewChanges': '查看改动',
95 'repo.error.actionFailed': '操作失败,请稍后重试。',
96 'repo.error.actionFailedShort': '操作失败',
97 'repo.error.loadFailed': '加载失败',
98 'repo.error.saveFailed': '保存失败',
99 'repo.view.loadFailed': '仓库加载失败',
100 'repo.view.notFound': '仓库不存在或你没有访问权限',
101 'repo.view.archived': '该仓库已归档(只读)。可在「设置」中恢复。',
102 'repo.clone.notConfigured': '克隆地址未配置',
103 'repo.clone.notConfiguredHint': '后端未配置 [git].base_url / http_clone_base_url,无法生成克隆地址',
104 'repo.clone.sshHint': 'SSH 克隆需先在 用户设置 → SSH 公钥 中登记公钥',
105 'repo.clone.copiedLabel': '克隆地址已复制',
106 'repo.clone.copy': '复制克隆地址',
107 'repo.clone.copyFailedHint': '复制失败,请选择左侧地址后手动复制',
108 'repo.clone.copyManually': '请手动复制',
109 'repo.clone.copiedStatus': '克隆地址已复制到剪贴板',
110 'repo.clone.copyFailedStatus': '复制失败,请选择克隆地址手动复制',
111 'repo.branch.defaultBadge': '默认',
112 'repo.branch.current': '当前浏览',
113 'repo.branch.protected': '受保护',
114 'repo.branch.protectedNote': '受保护分支不可删除,改名也需管理员',
115 'repo.branch.protectedRenameBlocked': '受保护分支不可重命名',
116 'repo.branch.deleteBlocked': '默认分支与受保护分支不可删除',
117 'repo.branch.alreadyDefault': '已经是默认分支',
118 'repo.branch.makeDefaultHint': '把仓库默认分支设为该分支',
119 'repo.branch.defaultSet': '已将「{name}」设为默认分支。',
120 'repo.branch.deletedName': '已删除分支「{name}」。',
121 'repo.branch.updated': '更新于 {time}',
122 'repo.branch.createHint': '分支名允许字母、数字与 . - _ /(不能出现 .. // 空格等),最长 250 字符。',
123 'repo.branch.createSubmit': '创建分支',
124 'repo.branch.startPoint': '起点',
125 'repo.branch.startPointHint': '可以是分支名、标签或提交号,默认从当前修订创建。',
126 'repo.branch.createdName': '已创建分支「{name}」。',
127 'repo.branch.rename': '重命名分支',
128 'repo.branch.renameHint': '改名只影响该仓库的分支引用,已克隆到本地的分支需要重新 fetch。',
129 'repo.branch.renameSubmit': '确认重命名',
130 'repo.branch.newName': '新的分支名',
131 'repo.branch.sameName': '新名称与原名称相同。',
132 'repo.branch.renamed': '分支「{from}」已改名为「{next}」。',
133 'repo.branch.validate.empty': '请填写分支名。',
134 'repo.branch.validate.tooLong': '分支名最多 {count} 个字符。',
135 'repo.branch.validate.startsWithDash': '分支名不能以 - 开头。',
136 'repo.branch.validate.endsWithSlash': '分支名不能以 / 结尾。',
137 'repo.branch.validate.endsWithLock': '分支名不能以 .lock 结尾。',
138 'repo.branch.validate.doubleDot': '分支名不能包含连续两个点 ..。',
139 'repo.branch.validate.atBrace': '分支名不能包含 @{。',
140 'repo.branch.validate.doubleSlash': '分支名不能包含连续两个斜杠 //。',
141 'repo.branch.validate.backslash': '分支名不能包含反斜杠。',
142 'repo.branch.validate.whitespace': '分支名不能包含空格或控制字符。',
143 'repo.branches.title': '分支',
144 'repo.branches.total': '共 {count}',
145 'repo.branches.filter': '筛选分支',
146 'repo.branches.filterPlaceholder': '筛选分支…',
147 'repo.branches.listName': '分支列表',
148 'repo.branches.loading': '加载分支…',
149 'repo.branches.noMatch': '没有匹配的分支。',
150 'repo.branches.none': '该仓库还没有分支。',
151 'repo.branches.limitNote': '已载入前 {count} 个分支,筛选仅针对已载入的分支。',
152 'repo.tags.title': '标签',
153 'repo.tags.total': '共 {count}',
154 'repo.tags.listName': '标签列表',
155 'repo.tags.loading': '加载标签…',
156 'repo.tags.none': '该仓库还没有标签。',
157 'repo.tags.filterPlaceholder': '筛选标签…',
158 'repo.tag.switch': '切换标签',
159 'repo.tag.annotated': '附注标签',
160 'repo.tag.lightweight': '轻量标签',
161 'repo.tag.deletedName': '已删除标签「{name}」。',
162 'repo.tag.createdName': '已创建标签「{name}」。',
163 'repo.tag.createHint': '附注标签会记录打标人与说明(annotated tag);轻量标签仅指向修订。',
164 'repo.tag.createSubmit': '创建标签',
165 'repo.tag.target': '目标修订',
166 'repo.tag.targetHint': '分支名、提交号或其它标签。',
167 'repo.tag.message': '说明',
168 'repo.tag.messageLabel': '说明(附注标签)',
169 'repo.tag.messagePlaceholder': '本次发布的主要内容…',
170 'repo.tag.nameRequired': '标签名不能为空。',
171 'repo.tag.detailTitle': '标签详情 · {name}',
172 'repo.tag.type': '类型',
173 'repo.tag.targetCommit': '指向提交',
174 'repo.tag.tagger': '打标人',
175 'repo.tag.createdAt': '创建时间',
176 'repo.submodules.title': '子模块',
177 'repo.submodules.resolve': '解析仓库',
178 'repo.submodules.unresolved': '无法解析(子仓库可能不存在或不可见)',
179 'repo.submodules.loadFailed': '子模块列表加载失败。',
180 'repo.breadcrumb.label': '仓库路径',
181 'repo.breadcrumb.root': '仓库根目录',
182 'repo.commit.history': '提交历史',
183 'repo.commit.backToHistory': '返回提交历史',
184 'repo.commit.browseRevision': '浏览该修订',
185 'repo.commit.loading': '加载提交内容…',
186 'repo.commit.what': '提交',
187 'repo.commit.missing': '提交不存在或已被改写。',
188 'repo.commit.noMessage': '(无提交信息)',
189 'repo.commit.unknownAuthor': '未知作者',
190 'repo.commit.committer': '提交者',
191 'repo.commit.root': '首个提交(对比空树)',
192 'repo.commit.compareTo': '对比 {sha}',
193 'repo.commit.parentChip': '父 {sha}',
194 'repo.commit.parents': '{count} 个父提交',
195 'repo.commit.parents_one': '{count} 个父提交',
196 'repo.commit.parents_other': '{count} 个父提交',
197 'repo.commit.latest': '最新提交',
198 'repo.commit.collapsedHint': '改动较多,默认折叠,点击文件展开。',
199 'repo.commit.noFiles': '该提交没有文件改动(可能是空提交或合并提交)。',
200 'repo.commit.status.added': '新增',
201 'repo.commit.status.deleted': '删除',
202 'repo.commit.status.renamed': '重命名',
203 'repo.commit.status.copied': '复制',
204 'repo.commit.status.modified': '修改',
205 'repo.commit.status.binary': '二进制',
206 'repo.commit.filesChanged_one': '{count} 个文件变更',
207 'repo.commit.filesChanged_other': '{count} 个文件变更',
208 'repo.commits.total': '共 {count} 次',
209 'repo.commits.viewAll': '查看全部历史',
210 'repo.commits.firstParent': '只看第一父提交',
211 'repo.commits.loading': '加载提交历史…',
212 'repo.commits.missing': '该修订可能已被删除或改写。',
213 'repo.commits.none': '该修订没有提交。',
214 'repo.commits.pageEmpty': '这一页没有内容,请翻回上一页。',
215 'repo.diff.followAppearance': '当前跟随「用户设置 · 外观」',
216 'repo.diff.unified': '统一',
217 'repo.diff.split': '分栏',
218 'repo.diff.binary': '二进制文件,不显示差异内容。',
219 'repo.diff.metaOnly': '仅权限或元数据变更。',
220 'repo.diff.lineComments': '该行有 {count} 条评论',
221 'repo.diff.lineComments_one': '该行有 {count} 条评论',
222 'repo.diff.lineComments_other': '该行有 {count} 条评论',
223 'repo.diff.commentOnLine': '对该行发表评论',
224 'repo.diff.truncated': '差异过长,已截断显示前 {count} 行。',
225 'repo.pager.prev': '上一页',
226 'repo.pager.next': '下一页',
227 'repo.pager.page': '第 {current} / {pages} 页',
228 'repo.tree.loading': '加载目录…',
229 'repo.tree.what': '目录',
230 'repo.tree.empty': '此目录为空。',
231 'repo.tree.tableLabel': '仓库文件',
232 'repo.tree.name': '名称',
233 'repo.tree.size': '大小',
234 'repo.tree.parent': '返回上级目录',
235 'repo.file.backToList': '返回文件列表',
236 'repo.file.loading': '加载文件…',
237 'repo.file.what': '文件',
238 'repo.file.blameHint': '显示每行的最后修改提交(blame)',
239 'repo.file.blameJsonHint': '私有仓库 JSON 模式下可用',
240 'repo.file.missingOid': '缺少文件标识,无法在私有仓库模式下预览。',
241 'repo.file.binaryDetail': '二进制文件 · {size} · 暂不支持预览',
242 'repo.file.truncated': '文件较大,服务端已截断返回。',
243 'repo.file.markdownView': 'Markdown 显示方式',
244 'repo.file.preview': '预览',
245 'repo.file.source': '源码',
246 'repo.file.unavailable': '该文件暂不支持预览。',
247 'repo.file.truncatedDetail': '文件过大({size}),服务端已截断,无法预览。',
248 'repo.file.pdfLoading': '正在栅格化首页…',
249 'repo.file.pdfStreaming': '已渲染 {done}/{total} 页,其余页面随后到达…',
250 'repo.file.pdfTruncated': '文档页数超过 400 页,仅呈现前 400 页。',
251 'repo.file.pdfPageAlt': '第 {page} 页',
252 'repo.file.pdfPageFailed': '第 {page} 页渲染失败。',
253 'repo.file.pdfCounter': '{page} / {total}',
254 'repo.file.pdfPrev': '上一页',
255 'repo.file.pdfNext': '下一页',
256 'repo.file.pdfZoomIn': '放大',
257 'repo.file.pdfZoomOut': '缩小',
258 'repo.file.pdfFailed': '无法读取该 PDF 文件。',
259 'repo.file.pdfUnreadable': '该 PDF 无法解析,可能已加密或损坏。',
260 'repo.empty.startTitle': '从第一个提交开始',
261 'repo.empty.startNote': '仓库已经准备好了。添加项目文件,让想法在这里慢慢成形。',
262 'repo.empty.addFiles': '# 添加文件',
263 'repo.empty.cloneNotConfigured': '克隆地址暂未配置。配置完成后,就可以在这里查看推送指引。',
264 'repo.blame.loading': '计算逐行归属…',
265 'repo.blame.unsupported': '该文件不支持 blame(二进制或暂不可读)。',
266 'repo.blame.empty': '后端未返回归属信息(文件可能为空或不在该修订下)。',
267 'repo.blame.what': '逐行归属',
268 'repo.blame.hint': '点击左侧归属跳转到提交详情。',
269 'repo.revision': '修订 {rev}',
270 'repo.release.loading': '加载发布记录…',
271 'repo.release.what': '发布记录',
272 'repo.release.backToList': '返回列表',
273 'repo.release.savedDraft': '已保存草稿。',
274 'repo.release.publishedNow': '已发布。',
275 'repo.release.prereleaseUpdated': '已更新预发布标记。',
276 'repo.release.deletedNow': '已删除发布记录。',
277 'repo.release.notFound': '发布记录不存在。',
278 'repo.release.created': '已创建发布记录。',
279 'repo.release.none': '还没有发布记录。推送标签会自动创建草稿,也可以手动新建。',
280 'repo.release.publishedBadge': '已发布',
281 'repo.release.commitCount': '{count} 个提交',
282 'repo.release.commitCount_one': '{count} 个提交',
283 'repo.release.commitCount_other': '{count} 个提交',
284 'repo.release.system': '系统',
285 'repo.release.body': '正文',
286 'repo.release.saveDraft': '保存草稿',
287 'repo.release.publish': '发布',
288 'repo.release.markPrerelease': '标记预发布',
289 'repo.release.unmarkPrerelease': '取消预发布',
290 'repo.release.noBody': '(没有正文)',
291 'repo.release.publishedImmutable': '已发布的发布记录正文不可再修改。',
292 'repo.release.new': '新建发布记录',
293 'repo.release.pickTagNote': '选择一个已存在的标签,发布记录会指向该标签对应的提交。',
294 'repo.release.tagsFailed': '标签列表加载失败,请稍后重试。',
295 'repo.release.noTags': '没有可用的标签。请先在「分支与标签」中创建标签,或推送一个新标签。',
296 'repo.release.selectTag': '请选择标签…',
297 'repo.release.tagAge': '({age})',
298 'repo.release.titleOptional': '标题(可选)',
299 'repo.release.saveAsDraft': '先保存为草稿',
300 'repo.comment.unknownUser': '未知用户',
301 'repo.comment.resolved': '已解决',
302 'repo.comment.outdated': '已过期',
303 'repo.comment.resolve': '标记解决',
304 'repo.comment.unresolve': '取消解决',
305 'repo.comment.updateFailed': '更新评论状态失败',
306 'repo.comment.deleteFailed': '删除评论失败',
307 'repo.comment.delete': '删除评论',
308 'repo.comment.createFailed': '评论失败',
309 'repo.comment.placeholder': '对该行发表评论…',
310 'repo.comment.submit': '发表评论',
311 'repo.refs.selectBranch': '选择分支',
312 'repo.refs.commit': '{sha} (提交)',
313 'repo.refs.current': '当前修订 {rev},选择分支',
314 'repo.refs.branchLabel': '分支',
315 'repo.refs.tagLabel': '标签',
316 'repo.refs.commitLabel': '提交',
317 'repo.refs.searchBranches': '搜索分支',
318 'repo.refs.searchTags': '搜索标签',
319 'repo.refs.noMatch': '没有匹配的结果,试试其他关键词。',
320 'repo.refs.noBranches': '还没有分支',
321 'repo.refs.noTags': '还没有标签',
322 'repo.aside.starFailed': '暂时无法更新收藏,请稍后重试。',
323 'repo.aside.overview': '仓库概览',
324 'repo.aside.noDescription': '这个仓库的故事,正在从代码开始。',
325 'repo.aside.website': '网站',
326 'repo.aside.openWebsite': '在新窗口打开 {url}',
327 'repo.aside.size': '仓库大小',
328 'repo.aside.license': '许可证',
329 'repo.aside.starBusy': '更新中…',
330 'repo.aside.starred': '已收藏 · {count}',
331 'repo.aside.star': '收藏仓库 · {count}',
332 'repo.aside.languages': '开发语言',
333 'repo.aside.primaryLanguage': '主要语言:{language}',
334 'repo.settings.nav.general': '常规',
335 'repo.settings.nav.features': '页签',
336 'repo.featuresSettings.sub': '控制仓库标签条里有哪些页签、以及它们的顺序。仅空间所有者可修改。',
337 'repo.featuresSettings.allowPr': '允许合并请求',
338 'repo.featuresSettings.allowPrHint': '关闭后「合并请求」页签对所有人隐藏,创建合并请求会被拒绝。',
339 'repo.featuresSettings.orderTitle': '页签顺序',
340 'repo.featuresSettings.orderHint': '调整「提交 / 合并请求 / 分支 / 流水线 / 发布」的顺序;代码与设置固定不动。',
341 'repo.featuresSettings.moveUp': '上移 {name}',
342 'repo.featuresSettings.moveDown': '下移 {name}',
343 'repo.featuresSettings.saved': '页签设置已保存。',
344 'repo.settings.nav.topics': '主题',
345 'repo.settings.nav.remotes': '远程',
346 'repo.settings.nav.import': '导入',
347 'repo.settings.nav.archives': '归档与下载',
348 'repo.settings.nav.insights': '分析与维护',
349 'repo.settings.nav.locks': 'LFS 锁',
350 'repo.settings.nav.protection': '保护规则',
351 'repo.settings.nav.ci': 'CI/CD',
352 'repo.settings.navLabel': '仓库设置分类',
353 'repo.settings.generalSub': '仓库描述、主页、可见性、Git LFS 与默认分支。仅空间所有者可修改。',
354 'repo.settings.field.description': '描述',
355 'repo.settings.field.website': '网站',
356 'repo.settings.field.websiteHint': '对外主页,留空表示未设置。',
357 'repo.settings.field.defaultBranchLoadFailed': '分支列表拉取失败,当前只显示已保存的默认分支。',
358 'repo.settings.field.privateLabel': '私有仓库',
359 'repo.settings.field.private': '私有仓库(仅成员可读)',
360 'repo.settings.field.enableLfs': '启用 Git LFS',
361 'repo.settings.generalSaved': '常规设置已保存。',
362 'repo.settings.saveSettings': '保存设置',
363 'repo.settings.unsaved': '有未保存的修改',
364 'repo.settings.readonlyInfo': '仓库信息(只读)',
365 'repo.settings.fact.id': '仓库 ID',
366 'repo.settings.fact.objectFormat': '对象格式',
367 'repo.settings.fact.createdAt': '创建时间',
368 'repo.settings.fact.updatedAt': '最近更新',
369 'repo.settings.fact.lastPushed': '最近推送',
370 'repo.settings.fact.never': '从未',
371 'repo.settings.fact.sizes': 'Git / LFS 大小',
372 'repo.settings.fact.storageLimit': '存储上限',
373 'repo.settings.fact.unlimited': '不限',
374 'repo.settings.fact.counts': '分支 / 标签 / 提交',
375 'repo.settings.fact.social': 'PR / 加星 / 关注',
376 'repo.settings.fact.license': '许可证',
377 'repo.settings.fact.licenseUnknown': '未识别',
378 'repo.settings.fact.primaryLanguage': '主语言',
379 'repo.settings.fact.languageUnknown': '未统计',
380 'repo.settings.topicAdded': '已添加主题「{name}」。',
381 'repo.settings.topicRemoved': '已移除主题「{name}」。',
382 'repo.settings.topicPlaceholder': '如 devops、cli-tool',
383 'repo.settings.topicWillSavePrefix': '将保存为',
384 'repo.settings.topicsSub': '给仓库打标签便于发现。仅允许小写字母、数字与连字符,服务端会在标签不存在时自动创建。',
385 'repo.settings.topicsLoading': '加载主题…',
386 'repo.settings.topicsEmpty': '该仓库还没有主题。',
387 'repo.protection.sub': '分支保护策略约束推送、强推与合并门禁;白名单成员可以绕过对应限制。',
388 'repo.protection.orderNote': '匹配顺序:分支名命中多条策略时,服务端取更具体的模式。',
389 'repo.protection.create': '新建分支保护策略',
390 'repo.protection.loading': '加载保护策略…',
391 'repo.protection.what': '保护策略',
392 'repo.protection.empty': '还没有保护规则。默认分支建议至少配置一条。',
393 'repo.protection.deleteConfirm': '删除对 “{pattern}” 的保护?',
394 'repo.protection.deleteLabel': '删除策略',
395 'repo.protection.doneSuffix': '完成。',
396 'repo.protection.failedSuffix': '失败:',
397 'repo.protection.unknownError': '未知错误',
398 'repo.protection.chip.approvals': '需 {count} 个批准',
399 'repo.protection.chip.noApprovals': '不要求批准',
400 'repo.protection.chip.restrictPush': '限制推送',
401 'repo.protection.chip.noForcePush': '禁止强推',
402 'repo.protection.chip.pushWhitelist': '推送白名单',
403 'repo.protection.chip.mergeWhitelist': '合并白名单',
404 'repo.protection.chip.approvalWhitelist': '审批白名单',
405 'repo.protection.chip.dismissStale': '新提交作废旧批准',
406 'repo.protection.chip.blockRejected': '拒绝评审阻止合并',
407 'repo.protection.chip.blockChangeRequested': '请求修改阻止合并',
408 'repo.protection.chip.blockOutdated': '落后分支阻止合并',
409 'repo.protection.chip.signedCommits': '要求签名提交',
410 'repo.protection.chip.protectedFiles': '保护文件:{patterns}',
411 'repo.protection.chip.unprotectedFiles': '豁免文件:{patterns}',
412 'repo.protection.edit': '编辑策略',
413 'repo.protection.deleteTitle': '删除策略',
414 'repo.protection.hideWhitelist': '收起白名单',
415 'repo.protection.manageWhitelist': '管理白名单',
416 'repo.protection.kind.push': '推送',
417 'repo.protection.kind.merge': '合并',
418 'repo.protection.kind.approval': '审批绕过',
419 'repo.protection.addFailed': '添加失败',
420 'repo.protection.removeFailed': '移除失败',
421 'repo.protection.selectUser': '选择用户…',
422 'repo.protection.userOption': '{name}(@{username})',
423 'repo.protection.groupLabel': '(组)',
424 'repo.protection.whitelistEmpty': '该策略暂无白名单成员。',
425 'repo.protection.whitelistDisabledNote': '对应白名单开关未在策略中启用,添加后不会生效。',
426 'repo.protection.tagWhitelistTitle': '标签保护白名单',
427 'repo.protection.tagWhitelistNote': '后端尚未提供标签保护策略的列表/创建接口,此处按策略 UUID 维护其用户白名单。',
428 'repo.protection.tagPolicyIdPlaceholder': '标签保护策略 UUID',
429 'repo.protection.branchRequired': '分支模式不能为空。',
430 'repo.protection.updated': '策略已更新。',
431 'repo.protection.created': '策略已创建。',
432 'repo.protection.editTitle': '编辑保护策略',
433 'repo.protection.formDesc': '分支模式支持 glob(如 main、release/*、*)。',
434 'repo.protection.branchPattern': '分支模式',
435 'repo.protection.requiredApprovals': '所需批准数(0–10)',
436 'repo.protection.field.enablePush': '限制推送(默认允许)',
437 'repo.protection.field.enableForcePush': '禁止强推(force push)',
438 'repo.protection.field.enablePushWhitelist': '推送启用白名单',
439 'repo.protection.field.enableMergeWhitelist': '合并启用白名单',
440 'repo.protection.field.enableApprovalsWhitelist': '审批启用白名单',
441 'repo.protection.field.blockOnRejected': '被拒绝的评审阻止合并',
442 'repo.protection.field.blockOnChangeRequested': '请求修改的评审阻止合并',
443 'repo.protection.field.blockOnOutdated': '源分支落后时阻止合并',
444 'repo.protection.field.dismissStale': '新提交后作废旧批准',
445 'repo.protection.field.requireSigned': '要求签名提交',
446 'repo.protection.protectedPatterns': '受保护文件模式(逗号分隔 glob,可选)',
447 'repo.protection.unprotectedPatterns': '豁免文件模式(逗号分隔 glob,可选)',
448 'repo.locks.sub': '锁定大文件路径避免并发覆盖。他人持有的锁需要管理员用 force 释放。',
449 'repo.locks.disabledNote': '该仓库当前未启用 Git LFS,服务端会拒绝任何加锁请求。启用后即可创建与释放锁。',
450 'repo.locks.enabled': '已启用 Git LFS,现在可以创建锁了。',
451 'repo.locks.summary': '我持有 {ours} 个锁,他人持有 {theirs} 个。',
452 'repo.locks.pathPlaceholder': '要锁定的路径,如 assets/video.mp4',
453 'repo.locks.pickFile': '选择文件',
454 'repo.locks.submitting': '提交中…',
455 'repo.locks.create': '新建锁',
456 'repo.locks.unlocked': '已释放 {path}(若此前未加锁则会提示失败)。',
457 'repo.locks.pathHint': '路径相对仓库根目录;按 Enter 或点击「新建锁」提交,锁按路径唯一。',
458 'repo.locks.loading': '加载锁…',
459 'repo.locks.empty': '当前没有 LFS 锁。',
460 'repo.locks.locked': '已锁定 {path}。',
461 'repo.locks.released': '已释放 {path}。',
462 'repo.locks.forceReleased': '已强制释放 {path}。',
463 'repo.locks.confirmRelease': '确认释放',
464 'repo.locks.forceRelease': '强制释放',
465 'repo.locks.release': '释放',
466 'repo.locks.releasePath': '释放该路径',
467 'repo.locks.loadMore': '加载更多',
468 'repo.locks.firstPage': '回到第一页',
469 'repo.locks.treeFailed': '读取仓库目录失败:{reason} 也可以直接输入路径。',
470 'repo.locks.parentDir': '上一级',
471 'repo.locks.validate.required': '需要先指定要锁定的文件路径(相对仓库根目录)。',
472 'repo.locks.validate.nul': '路径中不能包含空字符。',
473 'repo.locks.validate.tooLong': '路径最长 {count} 个字符。',
474 'repo.locks.validate.trailingSlash': '锁针对单个文件,路径不应以 / 结尾。',
475 'repo.locks.validate.dotdot': '路径不能包含 .. 目录跳转。',
476 'repo.locks.validate.empty': '路径不能为空。',
477 'repo.locks.validate.tooLongDetail': '路径过长:{detail}。',
478 'repo.locks.validate.badOid': 'oid 需为 `sha256:<64 位十六进制>`。',
479 'repo.locks.explanation.401': '登录状态已失效,请重新登录后再试。',
480 'repo.locks.explanation.403': '当前账号没有该仓库的写入权限(需要 Write/Admin 或空间所有者)。',
481 'repo.locks.explanation.404': '仓库不存在,或未启用 Git LFS。启用后才允许加锁。',
482 'repo.locks.explanation.409': '该路径已存在锁,或与其他锁冲突。',
483 'repo.locks.explanation.500': '后端处理失败,请稍后重试或换一条路径。',
484 'repo.locks.rejected': '服务端拒绝了该路径:{detail}',
485 'repo.locks.invalid': '提交的内容不符合服务端校验。',
486 'repo.locks.explainedWithDetail': '{explanation}({detail})',
487 'repo.locks.failedWithCode': '操作失败({code})。',
488 'repo.archives.formatTar': 'tar(不压缩)',
489 'repo.archives.archivedNote': '仓库已归档,写入会被拒绝。',
490 'repo.archives.restored': '仓库已恢复为可写。',
491 'repo.archives.sub': '归档状态、按需生成快照归档、全引用 bundle 打包。',
492 'repo.archives.status': '归档状态:{state}',
493 'repo.archives.statusArchived': '已归档(只读)',
494 'repo.archives.statusWritable': '正常(可写)',
495 'repo.archives.archiveNote': '归档后仓库不再是常规写入目标,推送会被拒绝;随时可以恢复。',
496 'repo.archives.restore': '恢复仓库',
497 'repo.archives.confirmArchive': '确认归档',
498 'repo.archives.archive': '归档仓库',
499 'repo.archives.snapshotTitle': '生成快照归档',
500 'repo.archives.revision': '修订',
501 'repo.archives.format': '格式',
502 'repo.archives.pathPrefix': '路径前缀(可选)',
503 'repo.archives.pathPrefixHint': '只打包该目录下的内容,例如 app/。',
504 'repo.archives.created': '归档已生成。',
505 'repo.archives.createAndLink': '生成并取下载地址',
506 'repo.archives.reuseHint':
507 '不重新打包,只为已存在的归档换一个新的短期链接;该修订尚未生成时会失败,此时请直接点「生成并取下载地址」',
508 'repo.archives.linkRefreshed': '已换取新的下载链接。',
509 'repo.archives.reuse': '复用已有归档',
510 'repo.archives.bundleLabel': 'git bundle(全部引用)',
511 'repo.archives.bundleCreated': 'bundle 已打包。',
512 'repo.archives.bundle': '打包全部引用为 bundle',
513 'repo.archives.factContent': '内容',
514 'repo.archives.factSize': '大小',
515 'repo.archives.openDownload': '在新标签页下载',
516 'repo.archives.linkNote': '链接为短期有效,失效后点「复用已有归档」重新获取。',
517 'repo.remotes.sub': '仓库配置的 git remote。地址必须是 https:// / ssh:// / git://,且不能指向 localhost 或内网地址。',
518 'repo.remotes.loading': '加载远程…',
519 'repo.remotes.empty': '尚未配置远程。',
520 'repo.remotes.fetchResult': '抓取:新增 {added} 个引用,更新 {updated} 个。',
521 'repo.remotes.fetched': '已从「{name}」抓取。',
522 'repo.remotes.fetch': '抓取',
523 'repo.remotes.pushResult': '推送:成功 {pushed},被拒 {rejected}。',
524 'repo.remotes.pushed': '已推送到「{name}」。',
525 'repo.remotes.push': '推送',
526 'repo.remotes.deleted': '已删除远程「{name}」。',
527 'repo.remotes.upsertTitle': '新增或更新远程',
528 'repo.remotes.name': '名称',
529 'repo.remotes.nameHint': '不能以 - 开头,不能含空格,最长 255 字符。',
530 'repo.remotes.url': '地址',
531 'repo.remotes.tokenPlaceholder': '令牌(可选,仅本次请求)',
532 'repo.remotes.refspecPlaceholder': 'refspec(推送用,留空为默认)',
533 'repo.remotes.saved': '已保存远程「{name}」。',
534 'repo.remotes.save': '保存远程',
535 'repo.remotes.mirrorResult': '镜像导入:{commits} 个提交,默认分支 {branch}。',
536 'repo.remotes.mirrorDone': '镜像导入完成。',
537 'repo.remotes.mirrorHint': '以 mirror 方式把外部仓库的全部引用导入当前仓库',
538 'repo.remotes.mirror': '镜像导入此地址',
539 'repo.remotes.note':
540 '令牌只在当次请求中使用,不会被保存;镜像导入会覆盖同名引用,请确认地址无误。要从 GitHub / GitLab / Gitea 新建并迁移一个仓库,请用社区菜单的「导入仓库」,任务进度在设置的「导入」分区查看。',
541 'repo.insights.sub': '语言统计、贡献者排行与提交活动。统计是持久化的缓存,改动后需要重新扫描。',
542 'repo.insights.rescanned': '已重新扫描语言统计。',
543 'repo.insights.scanning': '扫描中…',
544 'repo.insights.rescan': '重新扫描语言',
545 'repo.insights.rescanNote': '扫描会遍历当前修订的目录树,仓库较大时耗时较长。',
546 'repo.insights.languages': '语言构成({count})',
547 'repo.insights.languagesEmpty': '还没有语言统计,点上方「重新扫描语言」生成。',
548 'repo.insights.primary': '主',
549 'repo.insights.total': '合计 {size}',
550 'repo.insights.contributors': '贡献者({count})',
551 'repo.insights.contributorsEmpty': '暂无贡献者统计。',
552 'repo.insights.commits': '{count} 提交',
553 'repo.insights.commits_one': '{count} 提交',
554 'repo.insights.commits_other': '{count} 提交',
555 'repo.insights.activity': '提交活动',
556 'repo.insights.branchPlaceholder': '分支,留空为默认',
557 'repo.insights.activityTotal': '共 {count} 次提交',
558 'repo.insights.activityEmpty': '该分支暂无活动统计。',
559 'repo.release.titleField': '标题',
560 'repo.branch.updatedAt': '更新 {time}',
561} as const
Added view/app/src/i18n/messages/zh-CN/settings.ts View view/app/src/i18n/messages/zh-CN/settings.ts
@@ -0,0 +1,145 @@
1/** User settings modal: section list, form labels, notices and errors. */
2export const settings = {
3 'settings.title': '用户设置',
4 'settings.section.account': '我的账户',
5 'settings.group.account': '账号',
6 'settings.group.preferences': '偏好',
7 'settings.group.advanced': '高级',
8 'settings.section.profile': '公开资料',
9 'settings.section.security': '密码与安全',
10 'settings.section.sessions': '设备与会话',
11 'settings.section.emails': '邮箱',
12 'settings.section.privacy': '隐私设置',
13 'settings.section.notifications': '通知',
14 'settings.section.appearance': '外观',
15 'settings.section.keys': '访问密钥',
16 'settings.section.sshKeys': 'SSH 公钥',
17 'settings.section.status': '账户状态',
18 'settings.account.title': '我的账户',
19 'settings.account.sub': '账号身份、语言区域与危险操作。',
20 'settings.account.uploadAvatar': '上传头像',
21 'settings.account.removeAvatar': '移除头像',
22 'settings.account.avatarUpdated': '头像已更新。',
23 'settings.account.avatarRemoved': '头像已移除。',
24 'settings.account.displayName': '显示名称',
25 'settings.account.username': '用户名',
26 'settings.account.usernameHint': '小写字母、数字与下划线;修改后立即生效。',
27 'settings.account.language': '语言',
28 'settings.account.locale': '地区',
29 'settings.account.timezone': '时区',
30 'settings.account.saved': '账户信息已保存。',
31 'settings.account.userId': '用户 ID',
32 'settings.account.createdAt': '注册时间',
33 'settings.account.status': '状态',
34 'settings.account.active': '正常',
35 'settings.account.inactive': '已停用',
36 'settings.account.restricted': '受限',
37 'settings.account.danger': '危险操作',
38 'settings.account.revokeNote': '撤销除当前会话外的所有登录会话。',
39 'settings.account.revokeAll': '注销所有会话',
40 'settings.account.revoked': '其他会话已全部注销。',
41 'settings.error.generic': '操作失败,请稍后重试。',
42 'settings.profile.title': '公开资料',
43 'settings.profile.sub': '其他用户在你的主页上看到的信息。',
44 'settings.profile.realName': '真实姓名',
45 'settings.profile.bio': '个人简介',
46 'settings.profile.company': '公司',
47 'settings.profile.location': '所在地',
48 'settings.profile.website': '个人网站',
49 'settings.profile.pronouns': '代词',
50 'settings.profile.dateOfBirth': '出生日期',
51 'settings.profile.showEmail': '公开邮箱',
52 'settings.profile.showLocation': '公开所在地',
53 'settings.profile.showRealName': '公开真实姓名',
54 'settings.profile.showActivity': '公开动态',
55 'settings.profile.saved': '公开资料已保存。',
56 'settings.security.title': '密码与安全',
57 'settings.security.sub': '密码、两步验证与恢复码。',
58 'settings.security.currentPassword': '当前密码',
59 'settings.security.newPassword': '新密码',
60 'settings.security.confirmPassword': '确认新密码',
61 'settings.security.changePassword': '修改密码',
62 'settings.security.passwordChanged': '密码已修改。',
63 'settings.security.totp': '两步验证',
64 'settings.security.totpEnabled': '两步验证已启用。',
65 'settings.security.totpDisabled': '两步验证已关闭。',
66 'settings.security.enableTotp': '启用两步验证',
67 'settings.security.disableTotp': '关闭两步验证',
68 'settings.security.recoveryCodes': '恢复码',
69 'settings.security.regenerateCodes': '重新生成恢复码',
70 'settings.security.codesRegenerated': '恢复码已重新生成。',
71 'settings.security.scanQr': '用验证器应用扫描二维码,然后输入 6 位验证码。',
72 'settings.security.verifyCode': '验证码',
73 'settings.privacy.title': '隐私设置',
74 'settings.privacy.sub': '个人账号的私信与提及权限,对全部社区生效。',
75 'settings.privacy.allowDms': '允许私信',
76 'settings.privacy.allowMentions': '允许提及',
77 'settings.privacy.showOnline': '展示在线状态',
78 'settings.privacy.saved': '隐私设置已保存。',
79 'settings.notifications.title': '通知',
80 'settings.notifications.sub': '选择通过站内、邮件与推送接收哪些事件。',
81 'settings.notifications.inApp': '站内通知',
82 'settings.notifications.email': '邮件通知',
83 'settings.notifications.push': '推送通知',
84 'settings.notifications.digest': '邮件摘要',
85 'settings.notifications.saved': '通知设置已保存。',
86 'settings.notifications.pushEnabled': '此设备的系统通知',
87 'settings.notifications.pushEnable': '开启',
88 'settings.notifications.pushGranted': '此设备会收到系统通知;关闭即退订浏览器订阅。',
89 'settings.notifications.pushDenied': '该站点已被禁止显示通知,请在地址栏重新允许后再试。',
90 'settings.notifications.pushUnsupported': '此浏览器不支持系统通知。',
91 'settings.notifications.pushTypes': '推送到系统的类型',
92 'settings.notifications.pushMention': '提及',
93 'settings.notifications.pushReply': '回复',
94 'settings.notifications.pushDm': '私信',
95 'settings.notifications.pushReminder': '消息提醒',
96 'settings.notifications.pushFailed': '系统通知设置未保存,请重试。',
97 'settings.appearance.title': '外观',
98 'settings.appearance.sub': '配色、主题模式与代码差异展示方式。',
99 'settings.appearance.themeMode': '主题模式',
100 'settings.appearance.themeSystem': '跟随系统',
101 'settings.appearance.themeLight': '浅色',
102 'settings.appearance.themeDark': '深色',
103 'settings.appearance.diffStyle': '差异展示',
104 'settings.appearance.diffDefault': '默认(跟随服务端设置)',
105 'settings.appearance.diffDefaultNote': '不写入 diff_view_style,由服务端决定',
106 'settings.appearance.diffUnified': '统一 · 增删同列上下排列',
107 'settings.appearance.diffUnifiedNote': '保存值 unified',
108 'settings.appearance.diffSplit': '分栏 · 左删右增两列对照',
109 'settings.appearance.diffSplitNote': '保存值 split',
110 'settings.appearance.diffHintSuffix': '服务端仅识别 split,其它值一律按统一渲染',
111 'settings.appearance.boardView': '问题集默认视图',
112 'settings.appearance.boardViewHint': '打开问题集时使用看板或列表;带 ?view= 的链接仍以链接为准。',
113 'userSettings.appearance.themeSystem': '跟随系统',
114 'settings.appearance.saved': '外观设置已保存。',
115 'settings.keys.title': '访问密钥',
116 'settings.keys.sub': '用于 Git 与 API 的个人访问令牌。',
117 'settings.keys.create': '新建访问密钥',
118 'settings.keys.name': '名称',
119 'settings.keys.created': '访问密钥已创建,请立即保存。',
120 'settings.keys.revoked': '访问密钥已吊销。',
121 'settings.keys.empty': '还没有访问密钥。',
122 'settings.ssh.title': 'SSH 公钥',
123 'settings.ssh.sub': '用于通过 SSH 推送仓库的公钥。',
124 'settings.ssh.add': '添加 SSH 公钥',
125 'settings.ssh.key': '公钥',
126 'settings.ssh.added': 'SSH 公钥已添加。',
127 'settings.ssh.removed': 'SSH 公钥已删除。',
128 'settings.ssh.empty': '还没有 SSH 公钥。',
129 'settings.sessions.title': '设备与会话',
130 'settings.sessions.sub': '当前账号的所有登录会话。',
131 'settings.sessions.revoke': '注销',
132 'settings.sessions.revoked': '会话已注销。',
133 'settings.sessions.empty': '没有其他活跃会话。',
134 'settings.emails.title': '邮箱',
135 'settings.emails.sub': '登录与通知使用的邮箱地址。',
136 'settings.emails.add': '添加邮箱',
137 'settings.emails.primary': '主邮箱',
138 'settings.emails.setPrimary': '设为主邮箱',
139 'settings.emails.verified': '已验证',
140 'settings.emails.unverified': '未验证',
141 'settings.emails.removed': '邮箱已移除。',
142 'settings.status.title': '账户状态',
143 'settings.status.sub': '账号限制、封禁与删除申请。',
144 'settings.status.empty': '账号状态正常,无生效中的限制。',
145} as const
Added view/app/src/i18n/messages/zh-CN/shell.ts View view/app/src/i18n/messages/zh-CN/shell.ts
@@ -0,0 +1,111 @@
1/** Workspace frame: rail, sidebar, route titles and account menu. */
2export const shell = {
3 'shell.rail.personal': '个人',
4 'shell.rail.createSpace': '新建空间',
5 'shell.nav.chat': '聊天',
6 'shell.nav.board': '问题集',
7 'shell.nav.repo': '仓库',
8 'shell.nav.notifications': '通知',
9 'shell.nav.settings': '设置',
10 'shell.nav.backToWorkspace': '返回工作台',
11 'shell.sidebar.rooms': '频道',
12 'shell.sidebar.directMessages': '私信',
13 'shell.sidebar.repositories': '仓库',
14 'shell.sidebar.boards': '问题集',
15 'shell.sidebar.searchRooms': '搜索频道',
16 'shell.sidebar.emptyRooms': '还没有频道',
17 'shell.sidebar.emptyRoomsNote': '创建一个频道开始协作。',
18 'shell.sidebar.emptyRepos': '还没有仓库',
19 'shell.sidebar.emptyBoards': '还没有问题集',
20 'shell.sidebar.newRoom': '新建频道',
21 'shell.account.menu': '账号菜单',
22 'shell.account.profile': '公开资料',
23 'shell.account.settings': '用户设置',
24 'shell.account.logout': '退出登录',
25 'shell.account.logoutFailed': '退出登录失败,请重试。',
26 'shell.search.placeholder': '搜索仓库、问题集、频道…',
27 'shell.search.empty': '没有匹配结果',
28 'shell.search.hint': '输入关键词开始搜索',
29 'shell.title': 'GitDataAI',
30 'shell.titleSpace': '{space} · GitDataAI',
31 'shell.titleRepo': '{repo} · {space} · GitDataAI',
32 'shell.titleBoard': '{board} · {space} · GitDataAI',
33 'shell.titleChat': '{room} · {space} · GitDataAI',
34 'shell.board.title': '问题集',
35 'shell.board.pick': '从左侧选择一个问题集。',
36 'shell.board.notFound': '问题集不存在或已被删除',
37 'shell.board.forbidden': '你没有权限查看这个问题集',
38 'shell.board.loadFailed': '问题集加载失败',
39 'shell.board.deletedHint': '该问题集可能已被删除,或你的账号没有读取权限。',
40 'shell.board.return': '返回社区',
41 'shell.board.newIssue': '新建工单',
42 'shell.board.colPending': '待处理',
43 'shell.board.colInProgress': '进行中',
44 'shell.board.colReview': '待验收',
45 'shell.board.colDone': '已完成',
46 'shell.repo.overview': '仓库概览',
47 'shell.repo.tabs.code': '代码',
48 'shell.repo.tabs.commits': '提交',
49 'shell.repo.tabs.pulls': '合并请求',
50 'shell.repo.tabs.branches': '分支',
51 'shell.repo.tabs.releases': '发布',
52 'shell.repo.tabs.settings': '设置',
53 'shell.repo.tabs.more': '更多',
54 'shell.repo.loading': '正在加载仓库…',
55 'shell.repo.notFound': '仓库不存在或你没有访问权限',
56 'shell.repo.loadFailed': '仓库加载失败',
57 'shell.repo.archived': '该仓库已归档(只读)。可在「设置」中恢复。',
58 'shell.repo.cloneUnconfigured': '后端未配置 [git].base_url / http_clone_base_url,无法生成克隆地址',
59 'shell.repo.cloneUnconfiguredShort': '克隆地址未配置',
60 'shell.repo.sshHint': 'SSH 克隆需先在 用户设置 → SSH 公钥 中登记公钥',
61 'shell.repo.cloneCopied': '克隆地址已复制',
62 'shell.repo.copyClone': '复制克隆地址',
63 'shell.repo.copyFailed': '复制失败,请选择左侧地址后手动复制',
64 'shell.repo.copyManually': '请手动复制',
65 'shell.repo.clipboardOk': '克隆地址已复制到剪贴板',
66 'shell.repo.clipboardFail': '复制失败,请选择克隆地址手动复制',
67 'shell.repo.starFailed': '暂时无法更新收藏,请稍后重试。',
68 'shell.repo.descriptionFallback': '这个仓库的故事,正在从代码开始。',
69 'shell.repo.website': '网站',
70 'shell.repo.openWebsite': '在新窗口打开 {url}',
71 'shell.repo.license': '许可证',
72 'shell.repo.starUpdating': '更新中…',
73 'shell.repo.starred': '已收藏 {count}',
74 'shell.repo.star': '收藏 {count}',
75 'shell.repo.watch': '关注 {count}',
76 'shell.repo.watching': '已关注 {count}',
77 'shell.repo.watchFailed': '暂时无法更新关注,请稍后重试。',
78 'shell.repo.latestRelease': '最近发布',
79 'shell.repo.noRelease': '暂无发布',
80 'shell.repo.latestCommit': '最近提交',
81 'shell.repo.contributors': '贡献者',
82 'shell.repo.languages': '开发语言',
83 'shell.repo.primaryLanguage': '主要语言:{language}',
84 'shell.rail.home': '主页',
85 'shell.rail.addSpace': '添加空间',
86 'shell.rail.discover': '发现',
87 'shell.pending.title': '让协作从这里开始',
88 'shell.pending.body': '{label},继续讨论、分享想法,或查看团队的最新进展。',
89 'shell.pending.hint': '在工作区导航中选择内容',
90 'shell.pending.pickChannel': '从左侧选择一个频道',
91 'shell.user.settings': '用户设置',
92 'shell.user.mic': '麦克风',
93 'shell.user.mute': '静音',
94 'shell.user.unmute': '取消静音',
95 'shell.user.headphones': '听筒',
96 'shell.user.deafen': '关闭听筒',
97 'shell.user.undeafen': '打开听筒',
98 'shell.user.card': '个人信息',
99 'shell.user.openCard': '查看个人信息',
100 'shell.user.online': '在线',
101 'shell.user.copyUsername': '复制用户名',
102 'shell.user.copyId': '复制用户 ID',
103 'shell.user.copied': '已复制',
104 'shell.user.editProfile': '编辑个人资料',
105 'shell.chat.unknownUser': '未知用户',
106 'shell.chat.me': '我',
107 'shell.chat.userFallback': '用户 {id}',
108 'shell.chat.dm': '私信',
109 'shell.chat.channel': '频道',
110 'shell.titleNotifications': '通知 · GitDataAI',
111} as const
Added view/app/src/i18n/messages/zh-CN/space.ts View view/app/src/i18n/messages/zh-CN/space.ts
@@ -0,0 +1,509 @@
1/** Space management: creation, members, roles, invites, settings. */
2export const space = {
3 'space.create': '新建空间',
4 'space.create.name': '空间名称',
5 'space.create.slug': 'URL 标识',
6 'space.create.description': '空间描述',
7 'space.settings.title': '空间设置',
8 'space.settings.general': '常规',
9 'space.settings.members': '成员',
10 'space.settings.roles': '角色',
11 'space.settings.invites': '邀请',
12 'space.settings.channelPermissions': '频道权限',
13 'space.settings.name': '空间名称',
14 'space.settings.description': '空间描述',
15 'space.settings.avatar': '空间头像',
16 'space.settings.saved': '空间设置已保存。',
17 'space.settings.danger': '危险操作',
18 'space.settings.deleteSpace': '删除空间',
19 'space.settings.deleteConfirm': '删除后无法恢复,请输入空间名称确认:{name}',
20 'space.settings.deleted': '空间已删除。',
21 'space.members.title': '成员',
22 'space.members.sub': '查看并搜索这个社区里的成员。',
23 'space.members.recent': '最近加入的成员',
24 'space.members.shown': '已显示 {shown} 位,共 {total} 位成员',
25 'space.members.sort': '排序',
26 'space.members.sortJoined': '最近加入',
27 'space.members.sortName': '按名称',
28 'space.members.rowsPerPage': '每页行数',
29 'space.members.col.name': '名称',
30 'space.members.col.joined': '加入时间',
31 'space.members.col.method': '加入方式',
32 'space.members.col.status': '状态',
33 'space.members.owner': '社区创建者',
34 'space.members.invitedBy': '由 {name} 邀请',
35 'space.members.joinedSelf': '自行加入',
36 'space.members.noMatch': '没有匹配的成员',
37 'space.members.refresh': '刷新成员列表',
38 'space.members.open': '查看成员列表',
39 'space.members.invite': '邀请成员',
40 'space.members.remove': '移除成员',
41 'space.members.removed': '成员已移除。',
42 'space.members.empty': '还没有成员',
43 'space.members.role': '角色',
44 'space.members.joinedAt': '加入时间',
45 'space.roles.title': '角色',
46 'space.roles.groupCaption': '身份组',
47 'space.roles.back': '返回',
48 'space.roles.editorSub': '配置身份组设置和权限,保存后立即生效。',
49 'space.roles.settingsGroup': '显示',
50 'space.roles.editTitle': '编辑“{name}”',
51 'space.roles.permissionSearch': '搜索权限',
52 'space.roles.scopeCommunity': '社区范围',
53 'space.roles.clearPermissions': '清除权限',
54 'space.roles.colorHint': '直接改这个值,支持 hex / rgb / hsl 或颜色名。',
55 'space.roles.emptyHint': '还没有角色,点上面的「创建身份组」新建一个。',
56 'space.roles.cancel': '取消',
57 'space.roles.noPermissionMatch': '没有匹配的权限',
58 'space.roles.create': '新建角色',
59 'space.roles.name': '角色名称',
60 'space.roles.permissions': '权限',
61 'space.roles.created': '角色已创建。',
62 'space.roles.deleted': '角色已删除。',
63 'space.invites.title': '邀请',
64 'space.invites.create': '创建邀请链接',
65 'space.invites.expiresAt': '过期时间',
66 'space.invites.maxUses': '最大使用次数',
67 'space.invites.created': '邀请链接已创建。',
68 'space.invites.revoked': '邀请链接已撤销。',
69 'space.invites.empty': '还没有邀请链接',
70 'space.invites.copyLink': '复制邀请链接',
71 'space.invites.used': '已使用 {used}/{max}',
72 'space.channelPermissions.title': '频道权限',
73 'space.channelPermissions.sub': '按成员或角色覆盖频道级别的读写权限。',
74 'space.channelPermissions.saved': '频道权限已保存。',
75 'space.leave': '退出空间',
76 'space.leave.confirm': '确定退出空间 {name} 吗?',
77 'space.left': '已退出空间。',
78 'space.join': '加入空间',
79 'space.joined': '已加入空间。',
80 'space.create.nameRequired': '空间名称 · 必填',
81 'space.create.nameHint': '小写字母、数字与连字符,最长 64 字符',
82 'space.create.nameInvalid': '名称只能包含小写字母、数字和连字符,长度 1-64。',
83 'space.create.failed': '创建失败,请稍后重试。',
84 'space.create.descriptionPlaceholder': '这个空间用来做什么?',
85 'space.create.private': '私有空间',
86 'space.create.creating': '创建中…',
87 'space.menu.invite': '邀请朋友',
88 'space.menu.createRepo': '创建仓库',
89 'space.menu.createBoard': '创建问题集',
90 'space.menu.createRoom': '创建聊天室',
91 'space.menu.createDir': '创建文件夹',
92 'space.menu.settings': '社区设置',
93 'space.menu.privacy': '隐私设置',
94 'space.menu.leave': '退出社区',
95 'space.invite.title': '邀请朋友',
96 'space.invite.sub': '邀请 {space} 的新成员。对方接受后会自动获得所选角色。',
97 'space.invite.submit': '发出邀请',
98 'space.invite.email': '邮箱',
99 'space.invite.emailHint': '站外朋友填这里,会收到邀请邮件',
100 'space.invite.username': '或用户名',
101 'space.invite.usernameHint': '已注册的朋友填这里,会同时收到站内通知',
102 'space.invite.expires': '过期时间',
103 'space.invite.expiresHint': '留空表示长期有效',
104 'space.invite.groups': '加入后授予角色',
105 'space.invite.groupsEmpty': '这个社区还没有角色,请先到 社区设置 → 角色 创建一个。',
106 'space.invite.groupsHint': '至少选择一个角色',
107 'space.invite.missingTarget': '请填写邮箱或用户名,至少一个。',
108 'space.invite.sent': '邀请已发出:{target} 接受后即可加入。',
109 'space.invite.accept.crumb': '社区邀请',
110 'space.invite.accept.hint': '来自邀请邮件的专属链接',
111 'space.invite.accept.heading': '接受邀请',
112 'space.invite.accept.headingSpace': '加入 {space}',
113 'space.invite.accept.sub': '接受后即可成为成员,并获得邀请时选定的角色。',
114 'space.invite.accept.joining': '正在加入…',
115 'space.invite.accept.signInNote': '这个邀请是发给某个账号的:请先登录(或注册)后再打开邮件里的链接。',
116 'space.invite.accept.login': '去登录',
117 'space.invite.accept.signup': '注册新账号',
118 'space.invite.accept.invalid': '链接里缺少邀请码,请回到邮件重新打开。',
119 'space.invite.accept.notFound': '邀请不存在,或已被撤销。',
120 'space.invite.accept.mismatch': '当前登录账号不是被邀请的账号,请换用收到邀请的邮箱登录后重试。',
121 'space.invite.accept.used': '邀请已被使用或已过期。',
122 'space.invite.accept.failed': '加入失败,请稍后再试。',
123 'space.invite.accept.retry': '重试',
124 'space.invite.accept.home': '返回首页',
125 'space.repo.title': '创建仓库',
126 'space.repo.name': '仓库名称',
127 'space.repo.nameHint': '小写字母、数字与连字符',
128 'space.repo.description': '描述 · 可选',
129 'space.repo.dir': '所在文件夹',
130 'space.repo.defaultBranch': '默认分支 · 可选',
131 'space.repo.license': '许可证 · 可选',
132 'space.repo.initMessage': '初始提交信息 · 可选',
133 'space.repo.enableLfs': '启用 Git LFS',
134 'space.repo.private': '私有仓库',
135 'space.board.create.title': '创建问题集',
136 'space.board.nameField': '名称',
137 'space.board.description': '描述 · 可选',
138 'space.board.icon': '图标 · 可选',
139 'space.board.dir': '所在文件夹',
140 'space.room.title': '创建聊天室',
141 'space.room.name': '频道名称',
142 'space.room.nameHint': '小写字母、数字与连字符',
143 'space.room.kind': '类型',
144 'space.room.kind.text': '文字频道',
145 'space.room.kind.voice': '语音频道',
146 'space.room.kind.stage': '舞台频道',
147 'space.room.dir': '所在文件夹',
148 'space.room.topic': '主题 · 可选',
149 'space.room.userLimit': '语音人数上限 · 可选',
150 'space.room.rateLimit': '发言冷却(秒) · 可选',
151 'space.room.noLimit': '0 = 不限制',
152 'space.room.private': '私有频道(仅受邀可见)',
153 'space.dir.title': '创建文件夹',
154 'space.dir.description': '文件夹用于在空间目录树中归档仓库与问题集。',
155 'space.dir.name': '文件夹名称',
156 'space.dir.parent': '父级文件夹',
157 'space.dir.root': '根目录',
158 'space.privacy.title': '隐私设置',
159 'space.privacy.description': '个人账号的私信与提及权限,对全部社区生效。',
160 'space.privacy.dms': '允许私信',
161 'space.privacy.dmsHint': '允许他人给你发私信',
162 'space.privacy.mentions': '允许提及',
163 'space.privacy.mentionsHint': '允许他人在消息里提及你',
164 'space.privacy.online': '展示在线状态',
165 'space.privacy.onlineHint': '在成员列表里展示你的在线状态',
166 'space.privacy.saved': '隐私设置已保存。',
167 'space.leave.title': '退出社区',
168 'space.leave.sub': '退出后需要重新受邀才能回到 {space}。',
169 'space.userLabel': '{name}(@{username})',
170 'space.permission.viewSpace': '浏览社区',
171 'space.permission.read': '读取内容',
172 'space.permission.message': '发送消息',
173 'space.permission.manageMembers': '管理成员',
174 'space.permission.manageRoles': '管理角色',
175 'space.permission.manageInvites': '管理邀请',
176 'space.permission.manageBans': '管理封禁',
177 'space.permission.manageChannels': '管理频道',
178 'space.permission.manageOverwrites': '管理权限覆写',
179 'space.discover.title': '发现社区',
180 'space.discover.sub': '浏览公开社区,浏览简介与成员规模,申请加入。',
181 'space.discover.search': '搜索社区名称或简介',
182 'space.discover.sort.members': '推荐',
183 'space.discover.sort.newest': '最新',
184 'space.discover.cardSize': '卡片大小',
185 'space.discover.cardSize.large': '大',
186 'space.discover.cardSize.medium': '中',
187 'space.discover.cardSize.small': '小',
188 'space.discover.resultCount': '共 {count} 个公开社区',
189 'space.discover.loading': '正在加载社区…',
190 'space.discover.empty': '没有匹配的社区',
191 'space.discover.emptyHint': '换个关键词试试,或稍后再来看看。',
192 'space.discover.loadFailed': '社区列表加载失败',
193 'space.discover.loadMore': '加载更多',
194 'space.discover.members': '{count} 位成员',
195 'space.discover.joinLevel': '加入门槛:{level}',
196 'space.discover.joinLevel.none': '无额外要求',
197 'space.discover.enter': '进入社区',
198 'space.discover.requested': '已提交申请',
199 'space.discover.pending': '待审批',
200 'space.discover.withdraw': '撤回申请',
201 'space.discover.withdrawn': '已撤回申请。',
202 'space.discover.cooldown': '冷却中',
203 'space.discover.cooldownUntil': '{time} 后可再次申请',
204 'space.discover.apply': '申请加入',
205 'space.discover.closed': '暂不开放',
206 'space.discover.rejected': '上次申请未通过',
207 'space.discover.dialogTitle': '申请加入 {space}',
208 'space.discover.note': '申请附言(可选)',
209 'space.discover.noteHint': '简要说明你想加入的原因,最多 512 字。',
210 'space.discover.notePlaceholder': '我想加入这个社区,因为…',
211 'space.discover.submit': '提交申请',
212 'space.discover.submitted': '申请已提交,等待管理员审批。',
213 'space.discover.submitFailed': '申请提交失败',
214 'space.discover.withdrawFailed': '撤回失败',
215 'space.discover.cooldownError': '上次申请刚被驳回,请稍后再试。',
216 'space.discover.requirementError': '你还不满足该社区的加入门槛。',
217 'space.discover.notAllowedError': '该社区当前不接受申请。',
218 'space.discover.docTitle': '发现社区 · GitDataAI',
219 'space.joinRequests.title': '加入申请',
220 'space.joinRequests.sub': '审批想加入本社区的申请人,通过后他们会自动成为成员。',
221 'space.joinRequests.filter.pending': '待审批',
222 'space.joinRequests.filter.approved': '已通过',
223 'space.joinRequests.filter.rejected': '已拒绝',
224 'space.joinRequests.filter.all': '全部',
225 'space.joinRequests.empty': '还没有加入申请',
226 'space.joinRequests.emptyHint': '当有人申请加入时,你会在这里看到并收到通知。',
227 'space.joinRequests.loadFailed': '加入申请加载失败',
228 'space.joinRequests.note': '附言',
229 'space.joinRequests.noNote': '未填写附言',
230 'space.joinRequests.approve': '通过',
231 'space.joinRequests.reject': '拒绝',
232 'space.joinRequests.approved': '已通过 {name} 的申请。',
233 'space.joinRequests.rejected': '已拒绝 {name} 的申请。',
234 'space.joinRequests.status.pending': '待审批',
235 'space.joinRequests.status.approved': '已通过',
236 'space.joinRequests.status.rejected': '已拒绝',
237 'space.joinRequests.status.withdrawn': '已撤回',
238 'space.joinRequests.decidedBy': '审批人:{name}',
239 'space.joinRequests.requestedAt': '申请于 {time}',
240 'space.joinRequests.loadMore': '加载更多',
241 'space.joinRequests.count': '共 {count} 条',
242 'space.joinLevel.none': '无额外要求',
243 'space.joinLevel.mail': '需验证邮箱',
244 'space.joinLevel.mailDay': '需验证邮箱且账号满一天',
245 'space.joinLevel.mailHour': '需验证邮箱且账号满一小时',
246 'space.joinLevel.mailPhone': '需验证邮箱与手机号',
247 'space.notify.all': '全部消息',
248 'space.notify.mentions': '仅提及',
249 'space.notify.nothing': '不通知',
250 'space.memberStatus.active': '活跃',
251 'space.memberStatus.inactive': '不活跃',
252 'space.memberStatus.left': '已离开',
253 'space.memberStatus.removed': '已移除',
254 'space.joinTrigger.issueReaction': '问题集表情回应',
255 'space.joinTrigger.commentReaction': '评论表情回应',
256 'space.image.avatar': '社区图标',
257 'space.image.avatarHint': '建议正方形,至少 128×128',
258 'space.image.banner': '社区横幅',
259 'space.image.bannerHint': '列表与详情页顶部展示',
260 'space.image.inviteBanner': '邀请卡片图',
261 'space.image.inviteBannerHint': '分享邀请时预览使用',
262 'space.image.embedSplash': '嵌入背景图',
263 'space.image.embedSplashHint': '外部嵌入页面使用',
264 'space.settings.sections.mine': '我在本社区',
265 'space.settings.group.community': '社区',
266 'space.settings.group.members': '成员与权限',
267 'space.settings.sections.general': '社区资料',
268 'space.settings.sections.images': '图标与横幅',
269 'space.settings.sections.join': '加入规则',
270 'space.settings.sections.joinRequests': '加入申请',
271 'space.settings.sections.bans': '封禁',
272 'space.settings.sections.cleanup': '对象清理',
273 'space.cleanup.ownerSub': '批量清理空间里的目录与对象,仅空间所有者可访问。',
274 'space.cleanup.notOwner': '对象清理只对空间所有者开放。',
275 'space.cleanup.sub':
276 '快速删除或调整可见性:仓库可切换公开/私有并归档,频道可切换公开/私有或删除,问题集可直接删除,空目录可删除,带可见性覆盖的节点可一键恢复默认。',
277 'space.cleanup.filterPlaceholder': '按名称 / 路径 / 类型过滤',
278 'space.cleanup.empty': '没有匹配的对象。',
279 'space.cleanup.children': '子节点 {count}',
280 'space.cleanup.overwrites': '可见性覆盖 {count}',
281 'space.cleanup.archived': '已归档',
282 'space.cleanup.makePrivate': '设为私有',
283 'space.cleanup.makePublic': '设为公开',
284 'space.cleanup.archive': '归档',
285 'space.cleanup.unarchive': '取消归档',
286 'space.cleanup.clearOverwrites': '清除可见性覆盖',
287 'space.cleanup.deleteBlocked': '还有子节点,先清空再删除。',
288 'space.cleanup.confirmDelete': '确定删除「{name}」?该操作不可撤销。',
289 'space.cleanup.confirmClearOverwrites': '清除「{name}」的全部可见性覆盖,恢复为默认可见性?',
290 'space.cleanup.updated': '已更新。',
291 'space.cleanup.deleted': '已删除。',
292 'space.cleanup.overwritesCleared': '已清除可见性覆盖。',
293 'space.cleanup.kind.dir': '目录',
294 'space.cleanup.kind.object': '对象',
295 'space.cleanup.kind.repo': '仓库',
296 'space.cleanup.kind.board': '问题集',
297 'space.cleanup.kind.text': '文字频道',
298 'space.cleanup.kind.voice': '语音频道',
299 'space.cleanup.kind.stage': '舞台频道',
300 'space.cleanup.kind.category': '分类',
301 'space.cleanup.kind.news': '公告频道',
302 'space.cleanup.kind.guildRoom': '频道',
303 'space.cleanup.kind.thread': '线程',
304 'space.settings.fullLoadFailed': '读取完整社区资料失败,以下表单基于列表缓存,部分字段可能不是最新。',
305 'space.general.title': '社区资料',
306 'space.general.sub': '名称、介绍与加入方式。',
307 'space.general.ownerOnly': '只有社区拥有者可以修改这些内容,你当前只能查看。',
308 'space.general.name': '社区名称',
309 'space.general.nameHint': '仅限小写字母、数字与连字符,最长 64 个字符',
310 'space.general.description': '简介',
311 'space.general.descriptionHint': '展示在社区卡片与目录页,最长 512 个字符',
312 'space.general.welcome': '新人欢迎语',
313 'space.general.welcomeHint': '成员加入后出现在频道里的一条提示',
314 'space.general.welcomePlaceholder': '欢迎回到社区,请先阅读频道主题再发言。',
315 'space.general.access': '访问与加入',
316 'space.general.private': '私有社区(仅成员可见)',
317 'space.general.allowJoin': '允许主动申请加入',
318 'space.general.joinLevel': '加入门槛',
319 'space.general.allowInvite': '允许成员发出邀请',
320 'space.general.require2fa': '成员必须开启两步验证',
321 'space.general.save': '保存资料',
322 'space.general.saved': '社区资料已保存。',
323 'space.general.readonly': '只读信息',
324 'space.general.id': '社区 ID',
325 'space.general.owner': '拥有者',
326 'space.general.members': '成员数',
327 'space.general.status': '状态',
328 'space.general.createdUpdated': '创建 / 更新',
329 'space.general.active': '正常',
330 'space.general.inactive': '已停用',
331 'space.danger.ownerSub': '删除社区等不可逆操作只对拥有者开放。',
332 'space.danger.notOwner': '你不是这个社区的拥有者,无法删除它。',
333 'space.danger.sub': '删除后社区立即对外不可见,恢复需要联系平台管理员。',
334 'space.danger.deleteName': '删除 {name}',
335 'space.danger.description': '仓库、问题集与频道会一并从这个地址下线。成员数 {count}。',
336 'space.danger.confirmLabel': '输入社区名称 {name} 以确认',
337 'space.danger.deleting': '删除中…',
338 'space.danger.confirm': '确认删除',
339 'space.images.sub': '图片按原始字节直传对象存储,单个文件不超过 5 MB。',
340 'space.images.loadFailed': '读取图片失败,可能没有管理频道的权限。',
341 'space.images.tooLarge': '图片超过 5 MB,请压缩后再上传。',
342 'space.images.updated': '{label}已更新。',
343 'space.images.deleted': '{label}已删除。',
344 'space.images.loading': '读取中',
345 'space.images.unset': '未设置',
346 'space.images.replace': '更换',
347 'space.membership.sub': '这些设置只影响你在这个社区里的体验。',
348 'space.membership.subJoined': '加入于 {date}。这些设置只影响你在这个社区里的体验。',
349 'space.membership.save': '保存我的设置',
350 'space.membership.saved': '我的社区设置已保存。',
351 'space.alias.label': '社区备注名',
352 'space.alias.hint': '留空则使用全局昵称,最长 64 个字符',
353 'space.alerts.label': '消息提醒',
354 'space.alerts.mobilePush': '接收移动端推送',
355 'space.alerts.suppressEveryone': '忽略 @everyone 提及',
356 'space.alerts.suppressRoles': '忽略角色提及',
357 'space.alerts.hideMuted': '隐藏被静音的频道',
358 'space.members.count': '共 {count} 人。备注名与提醒设置只在本社区生效。',
359 'space.members.searchPlaceholder': '搜索成员…',
360 'space.members.searchAria': '搜索成员',
361 'space.members.usernamePlaceholder': '用户名',
362 'space.members.usernameAria': '要添加的用户名',
363 'space.members.add': '添加成员',
364 'space.members.added': '成员已添加。',
365 'space.members.loading': '正在读取成员…',
366 'space.members.emptyFiltered': '没有匹配的成员。',
367 'space.members.muted': '已禁言',
368 'space.members.collapse': '收起',
369 'space.members.expand': '编辑',
370 'space.members.mute': '禁言该成员',
371 'space.members.muteUntil': '禁言至',
372 'space.members.muteForever': '留空表示永久',
373 'space.members.allowPush': '允许移动端推送',
374 'space.members.save': '保存成员设置',
375 'space.members.saved': '成员设置已保存。',
376 'space.members.missingUser': '该成员缺少用户标识',
377 'space.roles.sub': '角色把成员分组,并批量授予权限。顺序决定成员列表中的展示次序。',
378 'space.roles.namePlaceholder': '角色名称',
379 'space.roles.nameAria': '新角色名称',
380 'space.roles.color': '颜色',
381 'space.roles.colorAria': '新角色颜色',
382 'space.roles.colorAria2': '角色颜色',
383 'space.roles.emojiAria': '新角色表情',
384 'space.roles.loading': '正在读取角色…',
385 'space.roles.empty': '还没有角色。新建一个后,在成员行里勾选权限即可授权。',
386 'space.roles.noPermissions': '尚无权限',
387 'space.roles.createdAt': '创建于 {date}',
388 'space.roles.permissionCount': '{count} 项权限',
389 'space.roles.emoji': '表情图标',
390 'space.roles.emojiHint': '可选,显示在角色名前',
391 'space.roles.position': '排序',
392 'space.roles.positionHint': '数字越小越靠前',
393 'space.roles.save': '保存角色',
394 'space.roles.saved': '角色已更新。',
395 'space.roles.dirty': '有未保存的修改',
396 'space.roles.selectMember': '选择成员',
397 'space.roles.selectMemberPlaceholder': '选择成员…',
398 'space.roles.granted': '已授予该角色。',
399 'space.roles.grant': '授予',
400 'space.roles.revoked': '已收回该角色。',
401 'space.roles.revoke': '收回',
402 'space.roles.joined': '已加入该角色。',
403 'space.roles.join': '我加入',
404 'space.roles.left': '已退出该角色。',
405 'space.roles.leave': '我退出',
406 'space.roles.noReadback':
407 '服务端只提供角色的写入接口,没有“成员当前拥有哪些角色”的读取接口,因此这里无法回显已授予的成员。',
408 'space.roles.delete': '删除角色',
409 'space.invites.sub': '接受后自动成为成员并获得所选角色;角色至少要有一个是必须的。',
410 'space.invites.missingTarget': '请填写邮箱或用户名。',
411 'space.invites.missingRole': '请至少选择一个要授予的角色。',
412 'space.invites.mode': '邀请方式',
413 'space.invites.byEmail': '按邮箱',
414 'space.invites.byUsername': '按用户名',
415 'space.invites.email': '邮箱地址',
416 'space.invites.username': '用户名',
417 'space.invites.oneOf': '两者只需填写一个',
418 'space.invites.expiresHint': '留空表示长期有效',
419 'space.invites.grantLabel': '接受后自动授予角色 · 必选',
420 'space.invites.send': '发送邀请',
421 'space.invites.activeTitle': '进行中的邀请',
422 'space.invites.loading': '正在读取邀请…',
423 'space.invites.historyTitle': '历史邀请',
424 'space.invites.noEmail': '未填写邮箱',
425 'space.invites.expiresOn': '过期于 {date}',
426 'space.invites.acceptedOn': '接受于 {date}',
427 'space.invites.revoke': '撤销',
428 'space.invites.redeemTitle': '使用邀请码',
429 'space.invites.redeemNote':
430 '接受码只写在邀请邮件里,接口不会返回它。若对方没有已验证邮箱,收信人会拿不到链接,这时改用「按用户名」邀请,对方会在站内通知里看到。',
431 'space.invites.code': '邀请码',
432 'space.invites.redeemed': '邀请码已使用。',
433 'space.invites.state.pending': '待接受',
434 'space.invites.state.accepted': '已接受',
435 'space.invites.state.revoked': '已撤销',
436 'space.invites.state.expired': '已过期',
437 'space.invites.nonePending': '没有待接受的邀请。',
438 'space.joinRules.sub': '满足条件的人自动获得角色,用于把问题集的热心参与者升级为成员。',
439 'space.joinRules.deletedRole': '已删除的角色',
440 'space.joinRules.needRole': '先在「角色」里创建一个角色,才能配置规则。',
441 'space.joinRules.grantRole': '授予角色',
442 'space.joinRules.selectRole': '选择角色…',
443 'space.joinRules.trigger': '触发方式',
444 'space.joinRules.matcher': '匹配内容',
445 'space.joinRules.matcherHint': '表情名称,例如 thumbs_up',
446 'space.joinRules.created': '规则已创建。',
447 'space.joinRules.create': '新建规则',
448 'space.joinRules.loading': '正在读取规则…',
449 'space.joinRules.empty': '还没有加入规则。',
450 'space.joinRules.enabled': '生效中',
451 'space.joinRules.disabled': '已停用',
452 'space.joinRules.enable': '启用该规则',
453 'space.joinRules.updated': '规则已更新。',
454 'space.joinRules.save': '保存规则',
455 'space.joinRules.deleted': '规则已删除。',
456 'space.invites.expires': '过期时间',
457 'space.bans.sub': '被封禁的用户无法加入,除非规则中放行了单项能力。',
458 'space.bans.expires': '到期时间',
459 'space.bans.expiresHint': '留空表示永久封禁',
460 'space.bans.reason': '原因',
461 'space.bans.reasonHint': '仅管理员可见,会记录在封禁行上',
462 'space.bans.banned': '用户已封禁。',
463 'space.bans.banUser': '封禁用户',
464 'space.bans.loading': '正在读取封禁列表…',
465 'space.bans.empty': '没有封禁记录。',
466 'space.bans.unbanAt': '解封于 {date}',
467 'space.bans.permanent': '永久',
468 'space.bans.by': '由 {by} 于 {date} 执行',
469 'space.bans.unbanned': '已解除封禁。',
470 'space.bans.unban': '解除封禁',
471 'space.bans.missingUser': '该封禁记录缺少用户标识',
472 'space.bans.saved': '封禁设置已更新。',
473 'space.bans.saveExceptions': '保存例外',
474 'space.bans.allowSpeak': '允许在语音频道发言',
475 'space.bans.allowPushRepo': '允许推送仓库',
476 'space.bans.allowCommentAny': '允许评论任意问题',
477 'space.channelPermissions.sub2': '对单个文件夹或频道隐藏、或强制显示给特定角色与成员。',
478 'space.channelPermissions.stance.deny': '隐藏',
479 'space.channelPermissions.stance.inherit': '跟随',
480 'space.channelPermissions.stance.allow': '可见',
481 'space.channelPermissions.selectNode': '选择节点',
482 'space.channelPermissions.emptyTree': '目录为空',
483 'space.channelPermissions.repaired': '目录结构已检查:扫描 {scanned} 个节点,修正 {fixed} 个。',
484 'space.channelPermissions.repair': '修复目录结构',
485 'space.channelPermissions.emptyNote': '这个社区还没有文件夹或频道。',
486 'space.channelPermissions.folder': '文件夹',
487 'space.channelPermissions.entry': '条目',
488 'space.channelPermissions.position': '排序 {position}',
489 'space.channelPermissions.archived': '已归档',
490 'space.channelPermissions.noTopic': '无主题',
491 'space.channelPermissions.byRole': '按角色',
492 'space.channelPermissions.byMember': '按成员',
493 'space.channelPermissions.defaultAlias': '默认昵称',
494 'space.channelPermissions.rename': '重命名',
495 'space.channelPermissions.renameHint': '服务端只允许修改名称',
496 'space.channelPermissions.renamed': '节点已重命名。',
497 'space.channelPermissions.saveName': '保存名称',
498 'space.channelPermissions.visibility': '{title} 可见性',
499 'space.channelPermissions.addByUsername': '按用户名添加例外',
500 'space.channelPermissions.addByUsernameHint': '社区成员列表不可用时,直接输入用户名',
501 'space.channelPermissions.alreadyExceptional': '该成员已经在例外列表里。',
502 'space.channelPermissions.exceptionAdded': '已添加成员例外。',
503 'space.channelPermissions.addVisible': '添加为可见',
504 'space.roles.emptyShort': '还没有角色。',
505 'space.members.count_one': '共 {count} 人。备注名与提醒设置只在本社区生效。',
506 'space.members.count_other': '共 {count} 人。备注名与提醒设置只在本社区生效。',
507 'space.roles.permissionCount_one': '{count} 项权限',
508 'space.roles.permissionCount_other': '{count} 项权限',
509} as const
Added view/app/src/i18n/messages/zh-CN/ui.ts View view/app/src/i18n/messages/zh-CN/ui.ts
@@ -0,0 +1,49 @@
1/** Shared UI chrome: shell landmarks, modal scaffolding, panel states. */
2export const ui = {
3 'ui.skipToContent': '跳转到主要内容',
4 'ui.openNavigation': '打开工作区导航',
5 'ui.closeNavigation': '收起工作区导航',
6 'ui.collapseNavigation': '收起导航',
7 'ui.navigation': '工作区导航',
8 'ui.spaceList': '空间列表',
9 'ui.currentSpace': '当前空间',
10 'ui.personalSpace': '个人空间',
11 'ui.shortcuts': '快捷入口',
12 'ui.workspace': '工作区',
13 'ui.collapseSidebar': '折叠侧栏',
14 'ui.expandSidebar': '展开侧栏',
15 'ui.navScope': '频道、看板与仓库',
16 'ui.workspaceLabel': '工作空间',
17 'ui.close': '关闭',
18 'ui.loading': '加载中',
19 'ui.loadFailed': '加载失败,请稍后重试。',
20 'ui.actionFailed': '操作失败,请稍后重试。',
21 'ui.fieldRequired': '请填写「{field}」。',
22 'ui.fieldNonNegativeInteger': '「{field}」需要是非负整数。',
23 'ui.noOptions': '暂无可选项',
24 'ui.saveChanges': '保存修改',
25 'ui.saving': '保存中…',
26 'ui.notFoundOnRevision': '{what}不存在于该修订。',
27 'ui.forbidden': '没有权限访问该{what}。',
28 'ui.loadFailedWithNote': '{what}加载失败:{note}',
29 'ui.loadFailedWithStatus': '{what}加载失败({status})。',
30 'ui.readFileFailed': '读取文件失败',
31 'ui.themeContextMissing': 'useThemeContext 必须在 ThemeProvider 内使用',
32 'ui.settingsCategories': '{title}分类',
33 'palette.graphite.name': '石墨',
34 'palette.graphite.origin': 'GitData 默认',
35 'palette.paper.name': '纸质',
36 'palette.paper.origin': '暖色阅读',
37 'palette.mint.name': '薄荷',
38 'palette.mint.origin': '冷色清爽',
39 'palette.github.name': '晴空',
40 'palette.solarized.name': '日曜',
41 'palette.nord.name': '北极',
42 'palette.catppuccin.name': '猫咪',
43 'palette.everforest.name': '常林',
44 'palette.gruvbox.name': '大地',
45 'palette.onedark.name': '一色',
46 'palette.tokyonight.name': '夜色',
47 'palette.dracula.name': '德古拉',
48 'ui.closeSettings': '关闭设置',
49} as const
Added view/app/src/i18n/messages/zh-CN/userSettings.ts View view/app/src/i18n/messages/zh-CN/userSettings.ts
@@ -0,0 +1,130 @@
1/** User settings: modal sections, per-section copy, and validation messages. */
2export const userSettings = {
3 'userSettings.profile.visibility': '资料可见性',
4 'userSettings.profile.bio': '简介',
5 'userSettings.profile.website': '网站',
6 'userSettings.profile.company': '公司',
7 'userSettings.profile.location': '所在地',
8 'userSettings.profile.saved': '资料已保存。',
9 'userSettings.sessions.devices': '设备',
10 'userSettings.sessions.noDevices': '暂无设备记录。',
11 'userSettings.sessions.unknownDevice': '未知设备',
12 'userSettings.sessions.renamePlaceholder': '重命名…',
13 'userSettings.sessions.rename': '重命名',
14 'userSettings.sessions.renamed': '设备已重命名。',
15 'userSettings.sessions.recently': '最近 {when}',
16 'userSettings.emails.primary': '主邮箱',
17 'userSettings.emails.verified': '已验证',
18 'userSettings.emails.pending': '待验证',
19 'userSettings.emails.setPrimary': '设为主要',
20 'userSettings.emails.primaryChanged': '主邮箱已切换。',
21 'userSettings.emails.deleted': '邮箱已删除。',
22 'userSettings.emails.newPlaceholder': '新邮箱地址',
23 'userSettings.emails.add': '添加邮箱',
24 'userSettings.emails.added': '添加请求已提交。',
25 'userSettings.emails.codePlaceholder': '发往 {email} 的验证码',
26 'userSettings.emails.verify': '验证',
27 'userSettings.emails.verifiedNotice': '邮箱已验证。',
28 'userSettings.keys.namePlaceholder': '密钥名称,如 laptop-git',
29 'userSettings.keys.scope': '作用域',
30 'userSettings.keys.create': '创建',
31 'userSettings.keys.newToken': '新密钥(仅显示一次)',
32 'userSettings.keys.empty': '暂无访问密钥。',
33 'userSettings.keys.createdAt': '创建 {created}',
34 'userSettings.keys.lastUsed': '最近使用 {used}',
35 'userSettings.keys.rotate': '轮换',
36 'userSettings.keys.rotated': '已轮换,请复制新密钥。',
37 'userSettings.keys.delete': '删除',
38 'userSettings.keys.deleted': '密钥已删除。',
39 'userSettings.security.changePassword': '修改密码',
40 'userSettings.security.currentPlaceholder': '当前密码',
41 'userSettings.security.newPlaceholder': '新密码',
42 'userSettings.security.confirmPlaceholder': '确认新密码',
43 'userSettings.security.update': '更新密码',
44 'userSettings.security.passwordUpdated': '密码已更新。',
45 'userSettings.security.fillBoth': '请填写当前与新密码。',
46 'userSettings.security.mismatch': '两次输入的新密码不一致。',
47 'userSettings.security.totp': '两步验证',
48 'userSettings.security.recoveryCodes': '恢复码',
49 'userSettings.security.generateCodes': '生成恢复码',
50 'userSettings.security.codesGenerated': '恢复码已生成。',
51 'userSettings.ssh.title': 'SSH 公钥',
52 'userSettings.ssh.sub': '用于 git over SSH 的克隆与推送。公钥指纹全站唯一,重复登记会被拒绝。',
53 'userSettings.ssh.duplicate': '该公钥已被登记(指纹全站唯一,可能已属于其他账户)。',
54 'userSettings.ssh.deleted': '公钥不存在或已被删除,请刷新列表。',
55 'userSettings.ssh.unparsable': '公钥内容无法解析,请确认粘贴的是完整的一行公钥。',
56 'userSettings.ssh.unsupported': '不支持的密钥类型:仅接受 RSA / Ed25519 / ECDSA P-256。',
57 'userSettings.ssh.nameLength': '名称需为 1–64 个字符。',
58 'userSettings.ssh.keyPlaceholder': 'ssh-ed25519 AAAA… user@host',
59 'userSettings.ssh.namePlaceholder': '密钥名称',
60 'userSettings.ssh.add': '添加公钥',
61 'userSettings.ssh.empty': '暂无 SSH 公钥。',
62 'userSettings.sessions.title': '设备与会话',
63 'userSettings.sessions.sub': '登录过的设备与当前活跃会话。',
64 'userSettings.sessions.sessions': '会话',
65 'userSettings.sessions.noSessions': '暂无活跃会话。',
66 'userSettings.sessions.kindWeb': '网页登录',
67 'userSettings.sessions.kindKey': '访问密钥',
68 'userSettings.sessions.current': '当前设备',
69 'userSettings.sessions.revoked': '已注销',
70 'userSettings.sessions.createdAt': '创建于 {when}',
71 'userSettings.sessions.revoke': '注销',
72 'userSettings.sessions.revokeAll': '注销所有其他会话',
73 'userSettings.sessions.deviceDeleted': '设备已删除。',
74 'userSettings.emails.title': '邮箱',
75 'userSettings.emails.sub': '登录邮箱、主邮箱与验证状态。',
76 'userSettings.emails.refresh': '刷新列表',
77 'userSettings.sessions.refresh': '刷新列表',
78 'userSettings.security.sub': '修改密码、两步验证与恢复码。',
79 'userSettings.security.totpHeading': '两步验证',
80 'userSettings.security.status': '当前状态:',
81 'userSettings.security.enabled': '已开启',
82 'userSettings.security.notEnabled': '未开启',
83 'userSettings.security.setupHint': '用验证器应用添加以下密钥后输入 6 位验证码完成开启。',
84 'userSettings.security.enable': '开启两步验证',
85 'userSettings.security.secret': '密钥',
86 'userSettings.security.codePlaceholder': '验证器 6 位代码',
87 'userSettings.security.passwordConfirmPlaceholder': '当前密码(确认用)',
88 'userSettings.security.verifyAndEnable': '验证并开启',
89 'userSettings.security.enabledNotice': '两步验证已开启,请保存下方恢复码。',
90 'userSettings.security.codesOnce': '恢复码(仅显示这一次,请妥善保存)',
91 'userSettings.security.viewCodes': '查看恢复码',
92 'userSettings.security.codesLoaded': '已加载恢复码。',
93 'userSettings.security.regenerate': '重新生成',
94 'userSettings.security.regenPasswordPlaceholder': '当前密码(重新生成需要)',
95 'userSettings.security.code6Placeholder': '6 位验证码',
96 'userSettings.security.disable': '关闭两步验证',
97 'userSettings.ssh.registered': '已登记的公钥({count})',
98 'userSettings.ssh.none': '还没有添加任何 SSH 公钥。',
99 'userSettings.ssh.addedAt': '添加于 {when}',
100 'userSettings.ssh.confirmDelete': '确认删除',
101 'userSettings.ssh.addedNotice': 'SSH 公钥已删除。',
102 'userSettings.ssh.modalTitle': '添加 SSH 公钥',
103 'userSettings.ssh.modalDescription': '仅接受 OpenSSH 单行公钥格式,支持 RSA / Ed25519 / ECDSA P-256。',
104 'userSettings.ssh.generateHint': '还没有密钥?先生成一个',
105 'userSettings.ssh.privateHint': '公钥在 ',
106 'userSettings.ssh.privateHintTail': '(私钥不要告诉任何人)。',
107 'userSettings.ssh.nameLabel': '名称',
108 'userSettings.ssh.nameExample': '如 work-laptop',
109 'userSettings.ssh.nameHint': '1–64 个字符,只用于你自己分辨是哪台设备。',
110 'userSettings.ssh.contentLabel': '公钥内容',
111 'userSettings.ssh.contentHint': '粘贴整行公钥即可,换行与多余空白会自动合并。',
112 'userSettings.ssh.pasteRequired': '请粘贴 SSH 公钥内容。',
113 'userSettings.ssh.badFormat': '无法识别的公钥格式,应形如 ssh-ed25519 AAAAC3Nza… 注释。',
114 'userSettings.ssh.addedFlash': '已添加「{name}」。',
115 'userSettings.notifications.digest': '接收邮件摘要(每日)',
116 'userSettings.notifications.muteUntil': '免打扰截止时间',
117 'userSettings.notifications.muteHint': '留空表示不启用免打扰。',
118 'userSettings.status.sub': '封禁与限制记录(只读)。',
119 'userSettings.status.clean': '没有处罚记录,账户状态良好。',
120 'userSettings.status.permanent': '永久',
121 'userSettings.appearance.example': '示例 · {style}',
122 'userSettings.appearance.splitExample': '分栏(左原内容 / 右新内容)',
123 'userSettings.appearance.unifiedExample': '统一(单列)',
124 'userSettings.appearance.themeColor': '主题配色',
125 'userSettings.appearance.currentPalette': '当前「{name}」· 以{mode}渲染 · 已存入本机',
126 'userSettings.appearance.resetDefault': '恢复默认',
127 'userSettings.privacy.dms': '允许私信',
128 'userSettings.privacy.mentions': '允许提及',
129 'userSettings.privacy.online': '展示在线状态',
130} as const
Added view/app/src/i18n/format.ts View view/app/src/i18n/format.ts
@@ -0,0 +1,98 @@
1import { getLocale, type Locale } from './locale'
2
3/**
4 * Locale-aware display formatters. Every date/number the UI shows goes through
5 * here so that switching the interface language also switches the conventions
6 * (Y/M/D vs D/M/Y, 24h clock, relative-time wording, KiB grouping).
7 *
8 * The `null`/`undefined`/invalid paths keep returning the em dash used
9 * throughout the tables, so callers never need their own placeholder branch.
10 */
11
12export const DASH = '\u2014'
13
14/** Rust's `time` crate emits `… +00:00:00`; `Date` rejects the seconds offset. */
15export function parseDateValue(value: string): Date {
16 return new Date(value.replace(/([+-]\d{2}):(\d{2}):(\d{2})$/, '$1:$2'))
17}
18
19export function formatBytes(value: number | null | undefined): string {
20 if (value === null || value === undefined || !Number.isFinite(value)) return DASH
21 const units = ['B', 'KB', 'MB', 'GB', 'TB']
22 let size = value
23 let unit = 0
24 while (size >= 1024 && unit < units.length - 1) {
25 size /= 1024
26 unit += 1
27 }
28 const formatted =
29 unit === 0
30 ? new Intl.NumberFormat(getLocale()).format(size)
31 : new Intl.NumberFormat(getLocale(), { maximumFractionDigits: 1 }).format(size)
32 return `${formatted} ${units[unit]}`
33}
34
35export function shortSha(sha: string | null | undefined, len = 7): string {
36 return sha ? sha.slice(0, len) : DASH
37}
38
39export function formatDateTime(value: string | number | null | undefined): string {
40 if (value === null || value === undefined) return DASH
41 const date = typeof value === 'number' ? new Date(value * 1000) : parseDateValue(value)
42 if (Number.isNaN(date.getTime())) return typeof value === 'number' ? DASH : value
43 return new Intl.DateTimeFormat(getLocale(), {
44 dateStyle: 'medium',
45 timeStyle: 'short',
46 }).format(date)
47}
48
49/** Wall-clock time of day for chat and activity rows (locale 12h/24h). */
50export function formatTime(value: number | null | undefined): string {
51 if (value === null || value === undefined) return DASH
52 const date = new Date(value * 1000)
53 if (Number.isNaN(date.getTime())) return DASH
54 return new Intl.DateTimeFormat(getLocale(), { hour: '2-digit', minute: '2-digit' }).format(date)
55}
56
57/** Compact relative stamp for commit and comment rows. */
58export function formatRelative(value: number | string | null | undefined): string {
59 const epoch =
60 typeof value === 'number' ? value : value ? Math.floor(parseDateValue(value).getTime() / 1000) : Number.NaN
61 if (!Number.isFinite(epoch)) return DASH
62 const delta = Math.floor(Date.now() / 1000) - epoch
63 // Future-dated commits (clock skew, rebased author time) read as "just now".
64 if (delta < 60) return relativeUnit(0, 'second')
65 const minutes = Math.floor(delta / 60)
66 if (minutes < 60) return relativeUnit(-minutes, 'minute')
67 const hours = Math.floor(minutes / 60)
68 if (hours < 24) return relativeUnit(-hours, 'hour')
69 const days = Math.floor(hours / 24)
70 if (days < 31) return relativeUnit(-days, 'day')
71 const months = Math.floor(days / 30)
72 if (months < 12) return relativeUnit(-months, 'month')
73 return relativeUnit(-Math.floor(days / 365), 'year')
74}
75
76function relativeUnit(amount: number, unit: Intl.RelativeTimeFormatUnit): string {
77 return new Intl.RelativeTimeFormat(getLocale(), { numeric: 'auto' }).format(amount, unit)
78}
79
80/** Long calendar-day label for grouped lists (commit history day headers). */
81export function formatDay(value: string | number | null | undefined): string {
82 if (value === null || value === undefined) return DASH
83 const date = typeof value === 'number' ? new Date(value * 1000) : parseDateValue(value)
84 if (Number.isNaN(date.getTime())) return typeof value === 'number' ? DASH : value
85 return new Intl.DateTimeFormat(getLocale(), { dateStyle: 'long' }).format(date)
86}
87
88/** Medium date (no time) for settings rows and metadata blocks. */
89export function formatDate(value: string | null | undefined, locale?: Locale): string {
90 if (!value) return DASH
91 const date = parseDateValue(value)
92 if (Number.isNaN(date.getTime())) return value
93 return new Intl.DateTimeFormat(locale ?? getLocale(), {
94 year: 'numeric',
95 month: '2-digit',
96 day: '2-digit',
97 }).format(date)
98}
Added view/app/src/i18n/index.ts View view/app/src/i18n/index.ts
@@ -0,0 +1,27 @@
1// Public API of the shared/i18n segment.
2// Consumers import from '@app/i18n', never from deeper files.
3
4export {
5 DASH,
6 formatBytes,
7 formatDate,
8 formatDateTime,
9 formatRelative,
10 formatTime,
11 parseDateValue,
12 shortSha,
13} from './format'
14export {
15 applyAccountLocale,
16 bootLocale,
17 DEFAULT_LOCALE,
18 getLocale,
19 LOCALES,
20 type Locale,
21 localeLabel,
22 normalizeLocale,
23 setLocale,
24 useLocale,
25} from './locale'
26export { isMessageKey, type MessageKey, type MessageParams, t, tCount, translate, translateCount } from './translate'
27export { type I18n, type TranslateCountFn, type TranslateFn, useT } from './use-t'
Added view/app/src/i18n/locale.ts View view/app/src/i18n/locale.ts
@@ -0,0 +1,142 @@
1import { useSyncExternalStore } from 'react'
2
3/**
4 * UI locale store: a module-level singleton exactly like `shared/lib/theme`, so
5 * the boot sequence, plain modules (formatters, `meta.ts` label tables) and
6 * React components all read the same value without a provider in scope.
7 *
8 * Resolution order — account preference, then the device choice, then the
9 * browser, then the reference locale:
10 *
11 * 1. `users.language` (written by the settings modal through `PATCH /me`)
12 * 2. localStorage, the explicit choice made on this device
13 * 3. `navigator.languages`
14 * 4. zh-CN, the reference dictionary every key is authored against
15 *
16 * Only the locales shipped in `messages/` are accepted. A tag such as `ja`
17 * or `zh-Hant` normalizes to `null` and simply loses its turn in the chain, so
18 * unsupported account preferences never blank the interface.
19 */
20
21/** UI locales shipped with this build. */
22export type Locale = 'zh-CN' | 'en'
23
24export const LOCALES: readonly Locale[] = ['zh-CN', 'en']
25export const DEFAULT_LOCALE: Locale = 'zh-CN'
26
27const STORAGE_KEY = 'gitdata-locale'
28
29let accountLocale: Locale | null = null
30let current: Locale = DEFAULT_LOCALE
31const listeners = new Set<() => void>()
32
33/**
34 * Fold a BCP-47 or POSIX tag onto a shipped locale.
35 *
36 * `zh_Hans`, `zh-Hans-CN`, `zh-CN`, `zh` and `en-US` all collapse; `zh-Hant`,
37 * `zh-TW`, `zh-HK` and `zh-MO` deliberately do **not** — rendering Simplified
38 * copy for a Traditional reader is worse than falling back to English.
39 */
40export function normalizeLocale(raw: string | null | undefined): Locale | null {
41 const tag = (raw ?? '').trim().toLowerCase().replace(/_/g, '-')
42 if (!tag) return null
43 if (tag === 'zh' || tag.startsWith('zh-')) {
44 const parts = tag.split('-').slice(1)
45 if (parts.some((part) => part.startsWith('hant'))) return null
46 if (parts.some((part) => part === 'tw' || part === 'hk' || part === 'mo')) return null
47 return 'zh-CN'
48 }
49 if (tag === 'en' || tag.startsWith('en-')) return 'en'
50 return null
51}
52
53function fromNavigator(): Locale | null {
54 if (typeof navigator === 'undefined') return null
55 const candidates = navigator.languages?.length ? navigator.languages : [navigator.language]
56 for (const candidate of candidates) {
57 const locale = normalizeLocale(candidate)
58 if (locale) return locale
59 }
60 return null
61}
62
63function storedLocale(): Locale | null {
64 try {
65 return normalizeLocale(localStorage.getItem(STORAGE_KEY))
66 } catch {
67 return null
68 }
69}
70
71function persist(locale: Locale) {
72 try {
73 localStorage.setItem(STORAGE_KEY, locale)
74 } catch {
75 /* private mode / storage disabled — the session keeps the in-memory value */
76 }
77}
78
79function resolve(): Locale {
80 return accountLocale ?? storedLocale() ?? fromNavigator() ?? DEFAULT_LOCALE
81}
82
83function paint(locale: Locale) {
84 if (typeof document !== 'undefined') document.documentElement.lang = locale
85 for (const listener of listeners) listener()
86}
87
88/** Boot-time entry point: resolve once, before the first paint of localized UI. */
89export function bootLocale() {
90 current = resolve()
91 if (typeof document !== 'undefined') document.documentElement.lang = current
92}
93
94/**
95 * Explicit switch from the settings UI. The choice is remembered on this
96 * device; the caller is responsible for persisting it to the account, and the
97 * server value re-applies on the next `fetchMe`.
98 */
99export function setLocale(locale: Locale) {
100 accountLocale = null
101 persist(locale)
102 if (current === locale) return
103 current = locale
104 paint(locale)
105}
106
107/**
108 * Apply the signed-in account's preference (`users.language` first, then
109 * `users.locale`). Passing values that normalize to `null` clears the override,
110 * which hands control back to the device/browser chain.
111 */
112export function applyAccountLocale(...candidates: (string | null | undefined)[]) {
113 let next: Locale | null = null
114 for (const candidate of candidates) {
115 next = normalizeLocale(candidate)
116 if (next) break
117 }
118 if (next === accountLocale) return
119 accountLocale = next
120 const resolved = resolve()
121 if (resolved === current) return
122 current = resolved
123 paint(resolved)
124}
125
126export function getLocale(): Locale {
127 return current
128}
129
130/** Human-readable locale name for pickers, in the locale's own language. */
131export function localeLabel(locale: Locale): string {
132 return locale === 'en' ? 'English' : '简体中文'
133}
134
135function subscribe(listener: () => void): () => void {
136 listeners.add(listener)
137 return () => listeners.delete(listener)
138}
139
140export function useLocale(): Locale {
141 return useSyncExternalStore(subscribe, getLocale)
142}
Added view/app/src/i18n/translate.ts View view/app/src/i18n/translate.ts
@@ -0,0 +1,81 @@
1import { DEFAULT_LOCALE, getLocale, type Locale } from './locale'
2import { en } from './messages/en'
3import { type MessageKey, zhCN } from './messages/zh-CN'
4
5/**
6 * Translation core. The zh-CN dictionary is the reference: it defines the key
7 * set and every caller is type-checked against it, while the English dictionary
8 * is declared as a total `Record<MessageKey, string>` so a missing or renamed
9 * key fails `tsc` instead of silently falling back at runtime.
10 */
11
12export type { MessageKey }
13
14/** Membership test against the reference (zh-CN) dictionary. */
15const MESSAGE_KEYS: ReadonlySet<string> = new Set(Object.keys(zhCN))
16
17export function isMessageKey(key: string): key is MessageKey {
18 return MESSAGE_KEYS.has(key)
19}
20
21/** Values accepted by `{placeholder}` interpolation. */
22export type MessageParams = Record<string, string | number>
23
24const DICTIONARIES: Record<Locale, Record<string, string>> = {
25 'zh-CN': zhCN,
26 en,
27}
28
29function interpolate(template: string, params?: MessageParams): string {
30 if (!params) return template
31 return template.replace(/\{(\w+)\}/g, (match, name: string) => (name in params ? String(params[name]) : match))
32}
33
34/**
35 * Look up `key` in the active locale, then in the reference dictionary, then
36 * give up and return the key itself. The last resort is deliberate: a raw
37 * `repo.tabs.files` on screen is an obvious bug report, whereas an empty string
38 * is invisible.
39 */
40export function translate(locale: Locale, key: string, params?: MessageParams): string {
41 const hit = DICTIONARIES[locale][key] ?? DICTIONARIES[DEFAULT_LOCALE][key]
42 if (hit === undefined) {
43 if (import.meta.env.DEV) console.warn(`[i18n] missing message: ${key}`)
44 return key
45 }
46 return interpolate(hit, params)
47}
48
49/** Imperative lookup for non-component code (formatters, stores, `meta.ts`). */
50export function t(key: MessageKey, params?: MessageParams): string {
51 return translate(getLocale(), key, params)
52}
53
54const PLURAL_RULES: Partial<Record<Locale, Intl.PluralRules>> = {}
55
56function pluralRules(locale: Locale): Intl.PluralRules {
57 PLURAL_RULES[locale] ??= new Intl.PluralRules(locale)
58 return PLURAL_RULES[locale]
59}
60
61/**
62 * Plural-aware lookup. Dictionaries carry `_one` / `_other` suffixed pairs and
63 * the category is chosen by `Intl.PluralRules`, so languages with richer plural
64 * sets can add `_few` / `_many` entries later without changing call sites.
65 * A missing suffixed entry degrades to `_other`, then to the bare key.
66 */
67export function translateCount(locale: Locale, key: string, count: number, params?: MessageParams): string {
68 const category = pluralRules(locale).select(count)
69 const withCategory = `${key}_${category}`
70 const suffix = withCategory in DICTIONARIES[locale] || withCategory in DICTIONARIES[DEFAULT_LOCALE]
71 return translate(locale, suffix ? withCategory : `${key}_other`, { count, ...params })
72}
73
74export function tCount(key: MessageKey, count: number, params?: MessageParams): string {
75 return translateCount(getLocale(), key, count, params)
76}
77
78/** Same lookup as `t`, bound to an explicit locale (SSR/tests/tooling). */
79export function tIn(locale: Locale, key: MessageKey, params?: MessageParams): string {
80 return translate(locale, key, params)
81}
Added view/app/src/i18n/use-t.ts View view/app/src/i18n/use-t.ts
@@ -0,0 +1,31 @@
1import { useCallback } from 'react'
2import { DEFAULT_LOCALE, type Locale, useLocale } from './locale'
3import { type MessageKey, type MessageParams, translate, translateCount } from './translate'
4
5/**
6 * React-facing translation hooks. `useLocale` is the reactivity edge: any
7 * component that renders translated copy subscribes to it, so a language switch
8 * re-renders exactly the tree that depends on the locale (see `locale.ts`).
9 *
10 * `t`/`tc` resolve the dictionary at call time, which keeps them correct inside
11 * event handlers and memoised callbacks as well as during render.
12 */
13
14export type TranslateFn = (key: MessageKey, params?: MessageParams) => string
15
16export type TranslateCountFn = (key: MessageKey, count: number, params?: MessageParams) => string
17
18export interface I18n {
19 locale: Locale
20 fallback: Locale
21 t: TranslateFn
22 tc: TranslateCountFn
23}
24
25/** Hook form. Use it in every component that renders localized copy. */
26export function useT(): I18n {
27 const locale = useLocale()
28 const t = useCallback<TranslateFn>((key, params) => translate(locale, key, params), [locale])
29 const tc = useCallback<TranslateCountFn>((key, count, params) => translateCount(locale, key, count, params), [locale])
30 return { locale, fallback: DEFAULT_LOCALE, t, tc }
31}
Added view/app/src/lib/api-envelope.ts View view/app/src/lib/api-envelope.ts
@@ -0,0 +1,42 @@
1export interface ApiEnvelope<T> {
2 code: number
3 message: string
4 data?: T | null
5 error?: string | null
6 error_code?: string | null
7 request_id?: string
8}
9
10export class ApiFailure extends Error {
11 readonly code: number
12 readonly errorCode?: string | null
13 readonly requestId?: string | null
14
15 constructor(message: string, code: number, errorCode?: string | null, requestId?: string | null) {
16 super(message)
17 this.name = 'ApiFailure'
18 this.code = code
19 this.errorCode = errorCode
20 this.requestId = requestId
21 }
22}
23
24export async function unwrapApi<T>(
25 method: PromiseLike<
26 | ApiEnvelope<unknown>
27 | {
28 code: number
29 message: string
30 data?: unknown
31 error?: string | null
32 error_code?: string | null
33 request_id?: string
34 }
35 >,
36): Promise<T> {
37 const res = (await method) as ApiEnvelope<unknown>
38 if (res.code !== 200) {
39 throw new ApiFailure(res.error || res.message || 'request failed', res.code, res.error_code, res.request_id)
40 }
41 return res.data as T
42}
Added view/app/src/lib/cache-keys.ts View view/app/src/lib/cache-keys.ts
@@ -0,0 +1,101 @@
1/**
2 * Cache-key conventions for the local-first store (`@/shared/storage`).
3 *
4 * One key per (scope, payload): the key is the contract between a `useAsync`
5 * consumer, a hover `prefetch` and any mutation that invalidates — building
6 * them here keeps the three from drifting apart. Pure string arithmetic, no
7 * business logic, which is why this lives in shared.
8 *
9 * Scopes nest by prefix so `invalidateCache('space:foo:')` drops everything
10 * belonging to one space. Always invalidate with the trailing separator.
11 *
12 * The member list intentionally has three keys: three fetchers post-process
13 * the same endpoint into three different payload shapes (admin rows with
14 * limit 200, chat's flattened picker rows, the issue board's page object),
15 * and a shared key would serve one consumer another's shape.
16 */
17
18/* ---------------------------------------------------------- global identity */
19
20export const meKey = () => 'me'
21export const spacesKey = () => 'spaces'
22export const dmsKey = () => 'dms'
23
24/* ------------------------------------------------------- space-scoped lists */
25
26export const spaceRoomsKey = (space: string) => `space:${space}:rooms`
27export const spaceDirKey = (space: string) => `space:${space}:dir`
28export const spaceReposKey = (space: string) => `space:${space}:repos`
29/**
30 * The full name-sorted repo list behind the repo pickers (link a PR, …).
31 *
32 * Deliberately separate from `spaceReposKey`: the sidebar fetches 50 rows in
33 * default order, this one up to 100 in name order, and `useAsync` caches by key
34 * alone — one key for both would let whichever loaded first decide the other's
35 * ordering and truncation.
36 */
37export const spaceReposPickKey = (space: string) => `space:${space}:repos:pick`
38export const spaceBoardsKey = (space: string) => `space:${space}:boards`
39export const spaceEmojisKey = (space: string) => `space:${space}:emojis`
40
41/** features/space-management rows (limit 200). */
42export const spaceAdminMembersKey = (space: string) => `space:${space}:admin:members`
43/** entities/chat flattened picker rows (limit 100). */
44export const spaceMembersFlatKey = (space: string) => `space:${space}:members:flat`
45/** entities/issue page object (limit 100). */
46export const spaceMembersPageKey = (space: string) => `space:${space}:members:page`
47
48/**
49 * Space settings/admin sections: `invites`, `groups`, `bans`, `join-rules`,
50 * `membership`, `images`, `settings`, `overwrites:{nodeId}`, …
51 */
52export const spaceAdminKey = (space: string, part: string) => `space:${space}:admin:${part}`
53
54/**
55 * One uploaded space image (`banner`, `icon`, …) as a single `SpaceImage` row.
56 *
57 * Distinct from `spaceAdminKey(space, 'images')`, which caches the *record of
58 * all kinds* the settings section renders: sharing a key would serve one
59 * consumer the other's shape. The sidebar reads this key so a community banner
60 * is on screen from the first frame instead of arriving after a refetch.
61 */
62export const spaceImageKey = (space: string, kind: string) => `space:${space}:image:${kind}`
63
64/** Whole-space invalidation prefix (note the trailing separator). */
65export const spacePrefix = (space: string) => `space:${space}:`
66
67/* --------------------------------------------------------------------- chat */
68
69/** Room side data: `room`, `members`, `pins`, `threads`. */
70export const roomKey = (roomId: string, part: string) => `room:${roomId}:${part}`
71
72/** Transcript seed page of one scope (`threadId === null` is the main stream). */
73export const chatSeedKey = (roomId: string, threadId: string | null) => `chat:${roomId}:${threadId ?? 'main'}:seed`
74
75/* ------------------------------------------------------------------- issues */
76
77/** Board payloads: `board`, `labels`, `milestones`, `issues:{queryKey}`. */
78export const boardKey = (space: string, board: string, part: string) => `board:${space}/${board}:${part}`
79
80/** Issue payloads: `detail`, `labels`, `comments`, `events`. */
81export const issueKey = (space: string, board: string, issueId: string, part: string) =>
82 `issue:${space}/${board}/${issueId}:${part}`
83
84/* -------------------------------------------------------------------- repos */
85
86/**
87 * Repo payloads: `detail`, `tags`, `branches:{limit}`, `tree:{rev}:{path}`,
88 * `readme:{rev}:{path}`, `commits:…`, `diff:…`, `remotes`, `topics`,
89 * `insights:…`, `lfs-locks:…`.
90 */
91export const repoKey = (space: string, repo: string, part: string) => `repo:${space}/${repo}:${part}`
92
93/* ------------------------------------------------------------ notifications */
94
95/** `list:{filter}`, `unread`. */
96export const notifyKey = (part: string) => `notify:${part}`
97
98/* ----------------------------------------------------------- user settings */
99
100/** `preferences`, `emails`, `sessions`, `totp`, `access-keys`, `ssh-keys`, `suspensions`. */
101export const settingsKey = (part: string) => `settings:${part}`
Added view/app/src/lib/config.ts View view/app/src/lib/config.ts
@@ -0,0 +1,24 @@
1const DEFAULTS = {
2 region: 'EU-CENTRAL-1',
3 minAge: 18,
4} as const
5
6function readMeta(name: string): string | null {
7 const meta = document.querySelector<HTMLMetaElement>(`meta[name="${name}"]`)
8 return meta?.content?.trim() || null
9}
10
11/**
12 * Deployment region injected via `<meta name="app:region">` in index.html,
13 * so the value can be swapped server-side without rebuilding the bundle.
14 */
15export function getRegion(): string {
16 return readMeta('app:region') ?? DEFAULTS.region
17}
18
19/** Minimum registration age injected via `<meta name="app:min-age">` in index.html. */
20export function getMinAge(): number {
21 const raw = readMeta('app:min-age')
22 const parsed = raw ? Number.parseInt(raw, 10) : NaN
23 return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULTS.minAge
24}
Added view/app/src/lib/format.ts View view/app/src/lib/format.ts
@@ -0,0 +1,17 @@
1/**
2 * Generic display formatters shared across slices.
3 *
4 * The implementations live in `shared/i18n/format` so that locale is resolved
5 * in one place; this module keeps the historical names (and the `shared/lib`
6 * public API) intact for the call sites that already import them.
7 */
8export {
9 formatBytes,
10 formatDate,
11 formatDateTime,
12 formatDateTime as fmtTime,
13 formatDay as fmtDay,
14 formatRelative,
15 formatRelative as fmtRelative,
16 shortSha,
17} from '@app/i18n/format'
Added view/app/src/lib/index.ts View view/app/src/lib/index.ts
@@ -0,0 +1,42 @@
1// Public API of the shared/lib segment — generic infrastructure without
2// business logic. Consumers import from '@app/lib', never from deeper files.
3
4export * from './api-envelope'
5export * from './cache-keys'
6
7export { getMinAge, getRegion } from './config'
8export { fmtDay, fmtRelative, fmtTime, formatBytes, shortSha } from './format'
9export {
10 DEFAULT_PALETTE_ID,
11 decodeCss,
12 encodeCss,
13 findPalette,
14 getPalette,
15 getPaletteId,
16 initPalette,
17 PALETTES,
18 type Palette,
19 type PaletteCore,
20 type PaletteMode,
21 type PaletteSnapshot,
22 paletteCss,
23 paletteVariants,
24 previewColors,
25 resetPalette,
26 resolveVariant,
27 sanitizeCss,
28 setPalette,
29 usePalette,
30} from './palettes'
31export { PREFETCH_FRESH_MS, type PrefetchOptions, prefetch, runShared } from './prefetch'
32export { useSpaLinkClick } from './spa-links'
33export {
34 getTheme,
35 initTheme,
36 onThemeChange,
37 setTheme,
38 type Theme,
39 toggleTheme,
40 useTheme,
41} from './theme'
42export { type AsyncCacheOptions, type AsyncState, useAsync } from './use-async'
Added view/app/src/lib/palettes.ts View view/app/src/lib/palettes.ts
@@ -0,0 +1,568 @@
1/**
2 * Palette store: named colour schemes that override the `--shell-*` tokens.
3 *
4 * The selected palette is persisted as **base64 CSS**: `paletteCss()` renders a
5 * stylesheet, `encodeCss()` puts it in localStorage, and on the next boot
6 * `decodeCss()` reverses the path — decode, sanitise (only `--custom: value`
7 * declarations survive), then inject. A corrupt or hand-edited payload
8 * therefore degrades to "no palette injected" instead of becoming an XSS or a
9 * broken page.
10 *
11 * A palette is orthogonal to light/dark: `Theme` decides the mode, and each
12 * palette ships whichever variants it has. A dark-only scheme (Dracula) simply
13 * pulls the mode with it, which `initPalette` keeps in sync.
14 */
15
16import { t } from '@app/i18n'
17import { useSyncExternalStore } from 'react'
18import { getTheme, onThemeChange, setTheme, type Theme } from './theme'
19
20export type PaletteMode = Theme
21
22export interface PaletteCore {
23 /** Sidebar / app background. */
24 base: string
25 /** Inputs, chips, secondary surfaces. */
26 panel: string
27 /** Cards and content columns. */
28 surface: string
29 /** Body text. */
30 text: string
31 /** Secondary text; must stay legible on `base` and `surface`. */
32 muted: string
33 /** Hairlines. */
34 line: string
35 /** The single solid action colour; one button per screen. */
36 accent: string
37 /** Keyboard focus ring; defaults to `muted`. */
38 focus?: string
39}
40
41export interface Palette {
42 id: string
43 name: string
44 /** Origin of the scheme, shown under the name. */
45 origin: string
46 light?: PaletteCore
47 dark?: PaletteCore
48}
49
50const PALETTE_ID_KEY = 'kernel-palette'
51const PALETTE_CSS_KEY = 'kernel-palette-css'
52const STYLE_ID = 'gitdata-palette'
53
54export const DEFAULT_PALETTE_ID = 'graphite'
55
56export const PALETTES: Palette[] = [
57 // Names/origins resolve lazily so the picker follows the active language.
58
59 {
60 id: 'graphite',
61 get name() {
62 return t('palette.graphite.name')
63 },
64 get origin() {
65 return t('palette.graphite.origin')
66 },
67 light: {
68 base: '#fafafa',
69 panel: '#f5f5f4',
70 surface: '#ffffff',
71 text: '#23272b',
72 muted: '#687078',
73 line: '#e1e3e3',
74 accent: '#2f3438',
75 },
76 dark: {
77 base: '#1a1c1e',
78 panel: '#282a2d',
79 surface: '#202224',
80 text: '#eceeef',
81 muted: '#b1b5ba',
82 line: '#3a3d40',
83 accent: '#565e67',
84 },
85 },
86 {
87 id: 'paper',
88 get name() {
89 return t('palette.paper.name')
90 },
91 get origin() {
92 return t('palette.paper.origin')
93 },
94 light: {
95 base: '#faf7f2',
96 panel: '#f2ede4',
97 surface: '#fffdf9',
98 text: '#2f2a26',
99 muted: '#7d7367',
100 line: '#e6ded2',
101 accent: '#847459',
102 },
103 },
104 {
105 id: 'mint',
106 get name() {
107 return t('palette.mint.name')
108 },
109 get origin() {
110 return t('palette.mint.origin')
111 },
112 light: {
113 base: '#f4f9f7',
114 panel: '#e8f1ed',
115 surface: '#ffffff',
116 text: '#1f2d2a',
117 muted: '#5f7a73',
118 line: '#d9e7e2',
119 accent: '#2f6f5f',
120 },
121 },
122 {
123 id: 'github',
124 get name() {
125 return t('palette.github.name')
126 },
127 origin: 'GitHub',
128 light: {
129 base: '#f6f8fa',
130 panel: '#eaeef2',
131 surface: '#ffffff',
132 text: '#1f2328',
133 muted: '#59636e',
134 line: '#d1d9e0',
135 accent: '#215a8e',
136 },
137 dark: {
138 base: '#0d1117',
139 panel: '#161b22',
140 surface: '#11161d',
141 text: '#e6edf3',
142 muted: '#9198a1',
143 line: '#30363d',
144 accent: '#3d6a94',
145 },
146 },
147 {
148 id: 'solarized',
149 get name() {
150 return t('palette.solarized.name')
151 },
152 origin: 'Solarized',
153 light: {
154 base: '#fdf6e3',
155 panel: '#eee8d5',
156 surface: '#fffdf1',
157 text: '#073642',
158 muted: '#657b83',
159 line: '#e3dcc5',
160 accent: '#268bd2',
161 },
162 dark: {
163 base: '#002b36',
164 panel: '#0a3d4a',
165 surface: '#06333f',
166 text: '#e8e5d8',
167 muted: '#93a1a1',
168 line: '#1b4a56',
169 accent: '#2aa198',
170 },
171 },
172 {
173 id: 'nord',
174 get name() {
175 return t('palette.nord.name')
176 },
177 origin: 'Nord',
178 light: {
179 base: '#eceff4',
180 panel: '#e5e9f0',
181 surface: '#f7f9fc',
182 text: '#2e3440',
183 muted: '#4c566a',
184 line: '#d8dee9',
185 accent: '#5e81ac',
186 },
187 dark: {
188 base: '#2e3440',
189 panel: '#3b4252',
190 surface: '#353c4a',
191 text: '#eceff4',
192 muted: '#c8d1de',
193 line: '#434c5e',
194 accent: '#88c0d0',
195 },
196 },
197 {
198 id: 'catppuccin',
199 get name() {
200 return t('palette.catppuccin.name')
201 },
202 origin: 'Catppuccin',
203 light: {
204 base: '#eff1f5',
205 panel: '#e6e9ef',
206 surface: '#f7f8fc',
207 text: '#4c4f69',
208 muted: '#6c6f85',
209 line: '#ccd0da',
210 accent: '#7287fd',
211 },
212 dark: {
213 base: '#1e1e2e',
214 panel: '#313244',
215 surface: '#24243a',
216 text: '#cdd6f4',
217 muted: '#a6adc8',
218 line: '#45475a',
219 accent: '#89b4fa',
220 },
221 },
222 {
223 id: 'everforest',
224 get name() {
225 return t('palette.everforest.name')
226 },
227 origin: 'Everforest',
228 light: {
229 base: '#f0f4ec',
230 panel: '#e4ebdd',
231 surface: '#f9fbf5',
232 text: '#2d353b',
233 muted: '#6c7a80',
234 line: '#d5dfc9',
235 accent: '#7f9e4c',
236 },
237 dark: {
238 base: '#2b3339',
239 panel: '#343f44',
240 surface: '#303a40',
241 text: '#d3c6aa',
242 muted: '#9da9a0',
243 line: '#404d53',
244 accent: '#a7c080',
245 },
246 },
247 {
248 id: 'gruvbox',
249 get name() {
250 return t('palette.gruvbox.name')
251 },
252 origin: 'Gruvbox',
253 light: {
254 base: '#f2e5bc',
255 panel: '#ebdbb2',
256 surface: '#fbf1c7',
257 text: '#3c3836',
258 muted: '#7c6f64',
259 line: '#d5c4a1',
260 accent: '#076678',
261 },
262 dark: {
263 base: '#282828',
264 panel: '#3c3836',
265 surface: '#32302f',
266 text: '#ebdbb2',
267 muted: '#a89984',
268 line: '#504945',
269 accent: '#83a598',
270 },
271 },
272 {
273 id: 'onedark',
274 get name() {
275 return t('palette.onedark.name')
276 },
277 origin: 'One Dark',
278 dark: {
279 base: '#282c34',
280 panel: '#2c313a',
281 surface: '#31363f',
282 text: '#abb2bf',
283 muted: '#8b95a7',
284 line: '#3e4451',
285 accent: '#61afef',
286 },
287 },
288 {
289 id: 'tokyonight',
290 get name() {
291 return t('palette.tokyonight.name')
292 },
293 origin: 'Tokyo Night',
294 dark: {
295 base: '#1a1b26',
296 panel: '#24283b',
297 surface: '#1f2335',
298 text: '#c0caf5',
299 muted: '#9aa5ce',
300 line: '#3b4261',
301 accent: '#7aa2f7',
302 },
303 },
304 {
305 id: 'dracula',
306 get name() {
307 return t('palette.dracula.name')
308 },
309 origin: 'Dracula',
310 dark: {
311 base: '#282a36',
312 panel: '#343746',
313 surface: '#2f3341',
314 text: '#f8f8f2',
315 muted: '#b8c0d0',
316 line: '#44475a',
317 accent: '#bd93f9',
318 },
319 },
320]
321
322/* ------------------------------------------------------------------ maths -- */
323
324function toRgb(hex: string): [number, number, number] {
325 const h = hex.replace('#', '')
326 const full =
327 h.length === 3
328 ? h
329 .split('')
330 .map((c) => c + c)
331 .join('')
332 : h
333 const n = Number.parseInt(full.slice(0, 6), 16)
334 return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
335}
336
337function toHex(rgb: [number, number, number]): string {
338 return `#${rgb
339 .map((c) =>
340 Math.round(Math.min(255, Math.max(0, c)))
341 .toString(16)
342 .padStart(2, '0'),
343 )
344 .join('')}`
345}
346
347/** Linear blend, `t` weighted towards `b`. */
348function mix(a: string, b: string, t: number): string {
349 const [r1, g1, b1] = toRgb(a)
350 const [r2, g2, b2] = toRgb(b)
351 return toHex([r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t])
352}
353
354function rgbOf(hex: string): string {
355 return toRgb(hex).join(' ')
356}
357
358/* ----------------------------------------------------------------- render -- */
359
360/**
361 * One palette variant rendered to token values. Derived tokens (rail, active,
362 * tile, board) come from the core colours so a new scheme needs only seven
363 * hand-picked values.
364 */
365function tokensFor(core: PaletteCore, mode: PaletteMode): Record<string, string> {
366 const sink = mode === 'light' ? core.text : '#000000'
367 return {
368 // The rail recedes from the sidebar in both modes; in dark it goes darker,
369 // not lighter, or pale icons lose their footing.
370 '--shell-rail': mode === 'light' ? mix(core.base, core.text, 0.035) : mix(core.base, sink, 0.3),
371 '--shell-sidebar': core.base,
372 '--shell-surface': core.surface,
373 '--shell-field': core.panel,
374 '--shell-active': mix(core.panel, core.text, mode === 'light' ? 0.07 : 0.18),
375 '--shell-tile': mix(core.panel, core.text, mode === 'light' ? 0.14 : 0.28),
376 '--shell-board': mode === 'light' ? mix(core.base, core.text, 0.04) : mix(core.base, sink, 0.12),
377 '--shell-ink': core.text,
378 '--shell-ink-muted': core.muted,
379 '--shell-hairline': core.line,
380 '--shell-action': core.accent,
381 '--shell-focus': core.focus ?? core.muted,
382 '--shell-shadow': mode === 'light' ? `0 6px 24px rgb(${rgbOf(core.text)} / 6%)` : `0 6px 24px rgb(0 0 0 / 22%)`,
383 }
384}
385
386export function paletteVariants(palette: Palette): PaletteMode[] {
387 return [palette.light ? 'light' : null, palette.dark ? 'dark' : null].filter(Boolean) as PaletteMode[]
388}
389
390export function findPalette(id: string | null | undefined): Palette | undefined {
391 return PALETTES.find((palette) => palette.id === id)
392}
393
394/** Which variant a palette renders in, given the requested mode. */
395export function resolveVariant(palette: Palette, mode: PaletteMode): PaletteMode {
396 if (mode === 'light' && palette.light) return 'light'
397 if (mode === 'dark' && palette.dark) return 'dark'
398 return palette.light ? 'light' : 'dark'
399}
400
401/** `html:root` outranks both `:root` and `.dark`, so one rule wins either mode. */
402export function paletteCss(id: string, mode: PaletteMode): string {
403 const palette = findPalette(id) ?? findPalette(DEFAULT_PALETTE_ID)!
404 const core = palette[resolveVariant(palette, mode)]
405 if (!core) return ''
406 const tokens = tokensFor(core, resolveVariant(palette, mode))
407 return `html:root{${Object.entries(tokens)
408 .map(([k, v]) => `${k}:${v}`)
409 .join(';')}}`
410}
411
412/** Swatches for the picker: card, app, hairline, action, text. */
413export function previewColors(id: string, mode: PaletteMode): string[] {
414 const palette = findPalette(id) ?? findPalette(DEFAULT_PALETTE_ID)!
415 const core = palette[resolveVariant(palette, mode)]
416 return core ? [core.surface, core.base, core.line, core.accent, core.text] : []
417}
418
419/* ------------------------------------------------------- base64 + safety -- */
420
421export function encodeCss(css: string): string {
422 const bytes = new TextEncoder().encode(css)
423 let binary = ''
424 for (const byte of bytes) binary += String.fromCharCode(byte)
425 return btoa(binary)
426}
427
428const FORBIDDEN = /(?:\{|\}|<|>|;|url\(|expression|@|javascript:|behavior)/i
429
430/**
431 * Keep only `--custom-property: value` declarations. Anything else — a nested
432 * rule, a selector, an `@import`, a `url()` — rejects the whole payload, which
433 * the caller treats as "no palette".
434 */
435export function sanitizeCss(css: string): string | null {
436 const body = css.replace(/\/\*[\s\S]*?\*\//g, '').trim()
437 const wrapped = /^html:root\s*\{([\s\S]*)\}$/.exec(body)
438 const declarations = (wrapped ? wrapped[1] : body).split(';')
439 const clean: string[] = []
440 for (const entry of declarations) {
441 const item = entry.trim()
442 if (!item) continue
443 const colon = item.indexOf(':')
444 if (colon < 0) return null
445 const prop = item.slice(0, colon).trim()
446 const value = item.slice(colon + 1).trim()
447 if (!/^--[a-z][a-z0-9-]*$/.test(prop)) return null
448 if (!value || value.length > 200 || FORBIDDEN.test(value)) return null
449 clean.push(`${prop}:${value}`)
450 }
451 return clean.length ? `html:root{${clean.join(';')}}` : null
452}
453
454export function decodeCss(payload: string | null): string | null {
455 if (!payload) return null
456 try {
457 const binary = atob(payload.trim())
458 const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0))
459 return sanitizeCss(new TextDecoder().decode(bytes))
460 } catch {
461 return null
462 }
463}
464
465/* ------------------------------------------------------------------ store -- */
466
467let paletteId = DEFAULT_PALETTE_ID
468let snapshot: PaletteSnapshot | null = null
469const listeners = new Set<() => void>()
470
471export interface PaletteSnapshot {
472 id: string
473 palette: Palette
474 /** Variant actually rendered. */
475 variant: PaletteMode
476 /** Base64 CSS as persisted. */
477 encoded: string
478}
479
480function inject(css: string | null): void {
481 const existing = document.getElementById(STYLE_ID)
482 if (!css) {
483 existing?.remove()
484 return
485 }
486 const tag = existing ?? Object.assign(document.createElement('style'), { id: STYLE_ID })
487 tag.textContent = css
488 if (!existing) document.head.appendChild(tag)
489}
490
491function emit(): void {
492 snapshot = null
493 for (const listener of listeners) listener()
494}
495
496function read(): PaletteSnapshot {
497 if (snapshot) return snapshot
498 const palette = findPalette(paletteId) ?? findPalette(DEFAULT_PALETTE_ID)!
499 const variant = resolveVariant(palette, getTheme())
500 snapshot = { id: palette.id, palette, variant, encoded: encodeCss(paletteCss(palette.id, getTheme())) }
501 return snapshot
502}
503
504/** Re-render the injected stylesheet after the mode or palette changed. */
505function apply(): void {
506 snapshot = null // the cached variant/premium pair is stale once the mode moves
507 const state = read()
508 inject(paletteCss(state.id, getTheme()))
509 emit()
510}
511
512export function getPaletteId(): string {
513 return read().id
514}
515
516export function getPalette(): Palette {
517 return read().palette
518}
519
520/** Persist as base64 CSS and paint it immediately. */
521export function setPalette(id: string): void {
522 const palette = findPalette(id)
523 if (!palette) return
524 paletteId = palette.id
525 const css = paletteCss(palette.id, getTheme())
526 localStorage.setItem(PALETTE_ID_KEY, palette.id)
527 localStorage.setItem(PALETTE_CSS_KEY, encodeCss(css))
528 // A scheme with only one variant also owns the light/dark class.
529 const variants = paletteVariants(palette)
530 if (variants.length === 1 && getTheme() !== variants[0]) setTheme(variants[0])
531 else apply()
532}
533
534/** Back to the built-in tokens: drop the override and both stored values. */
535export function resetPalette(): void {
536 paletteId = DEFAULT_PALETTE_ID
537 localStorage.removeItem(PALETTE_ID_KEY)
538 localStorage.removeItem(PALETTE_CSS_KEY)
539 inject(null)
540 emit()
541}
542
543/**
544 * Bootstrap: prefer the stored base64 payload, fall back to the stored palette
545 * id, and ignore either if it does not decode to safe declarations. Call next
546 * to `initTheme()`, before the first paint.
547 */
548export function initPalette(): void {
549 const fromUrl = new URLSearchParams(window.location.search).get('palette')
550 const stored = findPalette(fromUrl) ?? findPalette(localStorage.getItem(PALETTE_ID_KEY))
551 if (stored) paletteId = stored.id
552 const decoded = decodeCss(localStorage.getItem(PALETTE_CSS_KEY))
553 if (decoded) inject(decoded)
554 else if (paletteId !== DEFAULT_PALETTE_ID) {
555 localStorage.removeItem(PALETTE_CSS_KEY)
556 inject(paletteCss(paletteId, getTheme()))
557 }
558 onThemeChange(apply)
559}
560
561function subscribe(listener: () => void): () => void {
562 listeners.add(listener)
563 return () => listeners.delete(listener)
564}
565
566export function usePalette(): PaletteSnapshot {
567 return useSyncExternalStore(subscribe, read)
568}
Added view/app/src/lib/prefetch.ts View view/app/src/lib/prefetch.ts
@@ -0,0 +1,66 @@
1import { peekCache, readCache, writeCache } from '@app/services/storage'
2
3/**
4 * Shared in-flight registry + prefetch primitive for the local-first cache.
5 *
6 * One promise per cache key, shared by every consumer: a hover prefetch that
7 * is still running when the user clicks becomes the very request the mounting
8 * `useAsync` waits on (no duplicate roundtrip), and two components mounting
9 * onto the same key (e.g. member lists in three panels) collapse into one
10 * fetch even when the underlying alova methods differ.
11 */
12
13const inflight = new Map<string, Promise<unknown>>()
14
15/**
16 * Run `fn` at most once per key while a request is in flight; later callers
17 * receive the same promise (and the same error, if it rejects).
18 */
19export function runShared<T>(cacheKey: string, fn: () => Promise<T>): Promise<T> {
20 const existing = inflight.get(cacheKey)
21 if (existing) return existing as Promise<T>
22 const promise = fn().finally(() => {
23 if (inflight.get(cacheKey) === promise) inflight.delete(cacheKey)
24 })
25 inflight.set(cacheKey, promise)
26 return promise
27}
28
29/** A cache entry younger than this does not justify another prefetch request. */
30export const PREFETCH_FRESH_MS = 30_000
31
32export interface PrefetchOptions<T> {
33 /** Throttle window: skip while the cached entry is younger (default 30s). */
34 freshMs?: number
35 /** Same merge semantics as `useAsync` — kept so hover prefill and mount agree. */
36 merge?: (cached: T, fresh: T) => T
37}
38
39/**
40 * Warm the cache for a key the user is about to open (nav hover, idle
41 * top-N, pagination lookahead). Failures are silent by design: a prefetch
42 * is an optimization, and the real `useAsync` on mount will surface any
43 * genuine error through its own state.
44 */
45export async function prefetch<T>(
46 cacheKey: string,
47 fn: () => Promise<T>,
48 opts?: PrefetchOptions<T>,
49): Promise<T | undefined> {
50 const freshMs = opts?.freshMs ?? PREFETCH_FRESH_MS
51 const running = inflight.get(cacheKey)
52 // Someone (another hover, a mounting hook) is already fetching this key —
53 // ride along, but keep the prefetch contract of never rejecting.
54 if (running) return (running as Promise<T>).catch(() => undefined)
55 const hit = peekCache<T>(cacheKey) ?? (await readCache<T>(cacheKey))
56 if (hit && Date.now() - hit.at < freshMs) return hit.data
57 try {
58 const fresh = await runShared(cacheKey, fn)
59 const cached = peekCache<T>(cacheKey)?.data
60 const merged = cached !== undefined && opts?.merge ? opts.merge(cached, fresh) : fresh
61 writeCache(cacheKey, merged)
62 return merged
63 } catch {
64 return undefined
65 }
66}
Added view/app/src/lib/spa-links.ts View view/app/src/lib/spa-links.ts
@@ -0,0 +1,42 @@
1import { type MouseEvent, useCallback } from 'react'
2import { useNavigate } from 'react-router-dom'
3
4/** 无协议但形如域名(`www.example.com/x`)的 href 按站外链接处理。 */
5const HOST_LIKE = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/i
6
7/**
8 * 富文本(Markdown / 服务端 embed HTML)渲染出的链接元素不允许直接跳转:
9 * 一旦放行就是整页刷新。这个处理器是它们唯一的出口——
10 *
11 * - `#fragment`、`mailto:` 等非 http(s) 与带修饰键的点击仍交给浏览器;
12 * - 站内应用路径走 react-router(SPA 内部切换);
13 * - 站外地址与 `/api/` 下载地址开新标签页。
14 */
15export function useSpaLinkClick() {
16 const navigate = useNavigate()
17 return useCallback(
18 (event: MouseEvent<HTMLElement>) => {
19 if (event.defaultPrevented || event.button !== 0) return
20 // 修饰键是「新标签打开」的浏览器约定:放行即可,当前文档不会刷新。
21 if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
22 const target = event.target instanceof Element ? event.target : null
23 const href = target?.closest('a')?.getAttribute('href')
24 if (!href || href.startsWith('#')) return
25 const external = !href.startsWith('/') && HOST_LIKE.test(href.split(/[/?#]/, 1)[0])
26 let url: URL
27 try {
28 url = new URL(external ? `https://${href}` : href, window.location.href)
29 } catch {
30 return
31 }
32 if (url.protocol !== 'http:' && url.protocol !== 'https:') return
33 event.preventDefault()
34 if (!external && url.origin === window.location.origin && !url.pathname.startsWith('/api/')) {
35 navigate(`${url.pathname}${url.search}${url.hash}`)
36 return
37 }
38 window.open(url.href, '_blank', 'noopener,noreferrer')
39 },
40 [navigate],
41 )
42}
Added view/app/src/lib/theme.ts View view/app/src/lib/theme.ts
@@ -0,0 +1,56 @@
1import { useSyncExternalStore } from 'react'
2
3export type Theme = 'light' | 'dark'
4
5const STORAGE_KEY = 'kernel-theme'
6const listeners = new Set<() => void>()
7
8function readInitial(): Theme {
9 const fromUrl = new URLSearchParams(window.location.search).get('theme')
10 if (fromUrl === 'light' || fromUrl === 'dark') return fromUrl
11 const stored = localStorage.getItem(STORAGE_KEY)
12 if (stored === 'light' || stored === 'dark') return stored
13 return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
14}
15
16let current: Theme = 'light'
17
18function apply() {
19 document.documentElement.classList.toggle('dark', current === 'dark')
20}
21
22/** Call once at app bootstrap, before first paint of themed UI. */
23export function initTheme() {
24 current = readInitial()
25 apply()
26}
27
28export function getTheme(): Theme {
29 return current
30}
31
32export function setTheme(theme: Theme) {
33 current = theme
34 localStorage.setItem(STORAGE_KEY, theme)
35 apply()
36 for (const listener of listeners) listener()
37}
38
39export function toggleTheme() {
40 setTheme(current === 'dark' ? 'light' : 'dark')
41}
42
43/** Subscribe to light/dark changes; the palette store repaints on these. */
44export function onThemeChange(listener: () => void): () => void {
45 listeners.add(listener)
46 return () => listeners.delete(listener)
47}
48
49function subscribe(listener: () => void) {
50 listeners.add(listener)
51 return () => listeners.delete(listener)
52}
53
54export function useTheme(): Theme {
55 return useSyncExternalStore(subscribe, getTheme)
56}
Added view/app/src/lib/use-async.ts View view/app/src/lib/use-async.ts
@@ -0,0 +1,148 @@
1import { peekCache, readCache, writeCache } from '@app/services/storage'
2import { useEffect, useMemo, useRef, useState } from 'react'
3import { runShared } from './prefetch'
4
5export interface AsyncState<T> {
6 data?: T
7 loading: boolean
8 error?: unknown
9 /** 静默重取中:数据仍在屏幕上,不显示任何加载态。 */
10 silent?: boolean
11 /** Data is on screen while a network revalidation is still running. */
12 refreshing?: boolean
13 /** The data on screen came from the local cache, not this run's response. */
14 stale?: boolean
15}
16
17export interface AsyncControls<T> {
18 /** 重新拉取并保留屏幕上的数据(只标记 `refreshing`)。 */
19 reload: () => void
20 /** 静默重新拉取:不切 loading,也不显示刷新态,用于事件驱动的后台校准。 */
21 reloadSilently: () => void
22 /** 本地合并,不发起请求。 */
23 mutate: (updater: (current: T | undefined) => T) => void
24}
25
26export interface AsyncCacheOptions<T> {
27 /**
28 * Enables local-first behaviour for this fetch: paint the stored payload
29 * immediately, revalidate over the network in parallel, merge and persist
30 * the result. `null` keeps the plain promise-state behaviour.
31 */
32 cacheKey?: string | null
33 /** Folds the stored payload into the fresh response; default: fresh wins. */
34 merge?: (cached: T, fresh: T) => T
35}
36
37/**
38 * Promise-state hook with an optional local-first tier.
39 *
40 * Lifecycle when `cacheKey` is set:
41 * 1. first render peeks the memory tier synchronously — stored data paints
42 * without a loading flash (on a hard reload the disk tier hydrates in the
43 * first effect tick instead);
44 * 2. the network fetch always runs in parallel, deduplicated with hover
45 * prefetches through the shared in-flight registry;
46 * 3. a successful response is merged over the cache, persisted, and pushed
47 * into state — the UI updates live;
48 * 4. a failed response keeps the cached payload on screen and only flags
49 * `error`, so a working (stale) view never blanks out.
50 *
51 * `fn` results from superseded runs are ignored via the `active` flag, and a
52 * late disk hydration can never overwrite an already-arrived response.
53 */
54export function useAsync<T>(
55 fn: () => Promise<T>,
56 deps: readonly unknown[],
57 opts?: AsyncCacheOptions<T>,
58): AsyncState<T> & AsyncControls<T> {
59 const cacheKey = opts?.cacheKey ?? null
60 const [nonce, setNonce] = useState(0)
61 const silentMode = useRef(false)
62 const [state, setState] = useState<AsyncState<T>>(() => {
63 if (!cacheKey) return { loading: true }
64 const hit = peekCache<T>(cacheKey)
65 return hit ? { data: hit.data, loading: false, refreshing: true, stale: true } : { loading: true }
66 })
67 // deps is intentionally dynamic: callers choose their own trigger keys.
68 // eslint-disable-next-line react-hooks/exhaustive-deps
69 useEffect(() => {
70 let active = true
71 // A *successful* response outranks any later disk hydration; a failed one
72 // must not — offline, the stored payload arriving after the network error
73 // is exactly what keeps the screen alive.
74 let succeeded = false
75 let failed = false
76 const silentRun = silentMode.current
77 silentMode.current = false
78
79 if (cacheKey) {
80 const hit = peekCache<T>(cacheKey)
81 if (hit) {
82 if (!silentRun) setState({ data: hit.data, loading: false, refreshing: true, stale: true })
83 } else if (silentRun) {
84 setState((prev) => ({ ...prev, silent: true }))
85 } else {
86 // Either a cold key or a scope switch (roomId changed): drop the
87 // previous scope's payload and hydrate from disk while fetching.
88 setState({ loading: true })
89 void readCache<T>(cacheKey).then((envelope) => {
90 if (!active || succeeded || !envelope) return
91 setState((prev) => {
92 if (prev.data !== undefined) return prev
93 return {
94 data: envelope.data,
95 loading: false,
96 refreshing: !failed,
97 stale: true,
98 error: prev.error,
99 }
100 })
101 })
102 }
103 } else if (silentRun) {
104 setState((prev) => ({ ...prev, silent: true }))
105 } else {
106 setState((prev) => ({ data: prev.data, loading: true }))
107 }
108
109 const run = cacheKey ? runShared(cacheKey, fn) : fn()
110 run.then(
111 (fresh) => {
112 if (!active) return
113 succeeded = true
114 if (!cacheKey) {
115 setState({ data: fresh, loading: false, error: undefined })
116 return
117 }
118 const cached = peekCache<T>(cacheKey)?.data
119 const merged = cached !== undefined && opts?.merge ? opts.merge(cached, fresh) : fresh
120 writeCache(cacheKey, merged)
121 setState({ data: merged, loading: false, refreshing: false, stale: false, silent: false, error: undefined })
122 },
123 (error) => {
124 if (!active) return
125 failed = true
126 setState((prev) => ({ ...prev, loading: false, refreshing: false, silent: false, error }))
127 },
128 )
129 return () => {
130 active = false
131 }
132 // eslint-disable-next-line react-hooks/exhaustive-deps
133 }, [...deps, nonce])
134
135 const controls = useMemo<AsyncControls<T>>(
136 () => ({
137 reload: () => setNonce((value) => value + 1),
138 reloadSilently: () => {
139 silentMode.current = true
140 setNonce((value) => value + 1)
141 },
142 mutate: (updater) => setState((prev) => ({ ...prev, data: updater(prev.data) })),
143 }),
144 [],
145 )
146
147 return { ...state, ...controls }
148}