Soundboard: naming a clip used window.prompt, which Electron does not implement — it returned nothing, the flow aborted in silence, and adding a sound worked in the browser while doing nothing at all in the desktop app. Replaced with a two-step field inside the popover, identical in both. Spotify, three separate defects behind the two symptoms reported: Out of sync — a 20s poll stacked on the activity store's 5s debounce left everyone else on the previous track for up to 25s. The next poll is now scheduled just past the current track's end instead of on a fixed interval, and a track change bypasses the debounce (it happens once every few minutes; the debounce exists for chatty producers). Vanishing — a paused track, and the silent gap Spotify reports between two songs, both cleared the activity outright. Pausing is now carried as state rather than absence, and an empty answer is tolerated for 25s before the block comes down. Progress bar — timestamps are computed with the server's clock and were drawn against the viewer's, so any drift displaced the bar; and it kept advancing after a pause until the next poll. The ready payload now carries server time so each client can correct its own offset, and the bar freezes when paused. Tray, native notifications and system audio in screen share were all found already implemented and wired end to end; recorded in the roadmap rather than built again.
142 lines
6.3 KiB
TypeScript
142 lines
6.3 KiB
TypeScript
/**
|
|
* Source dictionary. Every key the app can translate is declared here, and its
|
|
* type is derived from this object — a typo or a missing key fails typecheck
|
|
* rather than silently rendering the raw key at runtime.
|
|
*
|
|
* Keys are flat and dot-namespaced by system (`settings.voice.*`), so a
|
|
* translation pass can take one system at a time.
|
|
*/
|
|
export const en = {
|
|
// Settings — navigation
|
|
'settings.tab.account': 'Account',
|
|
'settings.tab.voice': 'Voice & Video',
|
|
'settings.tab.privacy': 'Privacy',
|
|
'settings.tab.connections': 'Connections',
|
|
'settings.tab.keybinds': 'Keybinds',
|
|
'settings.tab.desktop': 'Desktop',
|
|
'settings.tab.instance': 'Instance',
|
|
'settings.tab.language': 'Language',
|
|
|
|
// Settings — language
|
|
'settings.language.title': 'Language',
|
|
'settings.language.description': 'Choose the language for the interface. Anything not yet translated stays in English.',
|
|
'settings.language.en': 'English',
|
|
'settings.language.ptBR': 'Portuguese (Brazil)',
|
|
|
|
// Settings — voice: input
|
|
'settings.voice.input.title': 'Input Device',
|
|
'settings.voice.input.volume': 'Input Volume',
|
|
'settings.voice.micTest.start': "Let's Check",
|
|
'settings.voice.micTest.stop': 'Stop Testing',
|
|
'settings.voice.micTest.playing': 'Playing your mic back to you — say something.',
|
|
'settings.voice.micTest.inCall': 'The level meter is live while you are in a call.',
|
|
'settings.voice.micTest.idle': 'Test your mic without joining a call.',
|
|
'settings.voice.micTest.failed': 'Could not open the microphone. Check the device and its permission.',
|
|
|
|
// Soundboard
|
|
'soundboard.title': 'Soundboard',
|
|
'soundboard.empty': 'No sounds yet.',
|
|
'soundboard.add': 'Add sound',
|
|
'soundboard.adding': 'Uploading...',
|
|
'soundboard.remove': 'Remove',
|
|
'soundboard.namePrompt': 'Name for this sound',
|
|
'soundboard.confirm': 'Add',
|
|
'soundboard.cancel': 'Cancel',
|
|
'soundboard.joinFirst': 'Join a voice channel to use the soundboard.',
|
|
'soundboard.tooLarge': 'Sound must be under 2 MB and a few seconds long.',
|
|
'soundboard.uploadFailed': 'Could not upload that file. Try a different one.',
|
|
|
|
// Account menu (own name in the user bar)
|
|
'accountMenu.editProfile': 'Edit Profile',
|
|
'accountMenu.status': 'Status',
|
|
'accountMenu.status.online': 'Online',
|
|
'accountMenu.status.idle': 'Idle',
|
|
'accountMenu.status.dnd': 'Do Not Disturb',
|
|
'accountMenu.status.offline': 'Invisible',
|
|
'accountMenu.copyId': 'Copy User ID',
|
|
'accountMenu.copied': 'Copied',
|
|
|
|
// Statistics
|
|
'stats.title': 'Statistics',
|
|
'stats.range.7': 'Last 7 days',
|
|
'stats.range.30': 'Last 30 days',
|
|
'stats.range.365': 'Last year',
|
|
'stats.voice.title': 'Time in voice',
|
|
'stats.messages.title': 'Messages sent',
|
|
'stats.empty': 'Nothing recorded in this period yet.',
|
|
'stats.total.voice': '{value} total',
|
|
'stats.total.messages': '{value} messages in total',
|
|
'stats.hours': '{hours}h {minutes}m',
|
|
'stats.minutes': '{minutes}m',
|
|
'stats.note': 'Counting started when this feature was installed — earlier activity is not included.',
|
|
|
|
// Audit log
|
|
'audit.title': 'Audit Log',
|
|
'audit.empty': 'Nothing recorded yet. Changes to the server show up here.',
|
|
'audit.loadMore': 'Load more',
|
|
'audit.unknownActor': 'Deleted account',
|
|
'audit.action.space.update': '{actor} updated the server settings',
|
|
'audit.action.space.transfer_ownership': '{actor} transferred ownership of the server',
|
|
'audit.action.channel.create': '{actor} created the channel {name}',
|
|
'audit.action.channel.update': '{actor} updated the channel {name}',
|
|
'audit.action.channel.delete': '{actor} deleted the channel {name}',
|
|
'audit.action.member.kick': '{actor} removed a member',
|
|
'audit.action.member.leave': '{actor} left the server',
|
|
'audit.action.member.ban': '{actor} banned a member',
|
|
'audit.action.member.unban': '{actor} unbanned a member',
|
|
'audit.action.role.create': '{actor} created the role {name}',
|
|
'audit.action.role.update': '{actor} updated the role {name}',
|
|
'audit.action.role.delete': '{actor} deleted a role',
|
|
'audit.action.invite.create': '{actor} created an invite',
|
|
'audit.action.message.delete': '{actor} deleted a message',
|
|
'audit.action.unknown': '{actor} performed an action',
|
|
|
|
// GIF picker
|
|
'gif.search': 'Search GIFs',
|
|
'gif.tab.favorites': 'Favorites',
|
|
'gif.tab.trending': 'Trending',
|
|
'gif.empty.search': 'No GIFs found',
|
|
'gif.empty.trending': 'No trending GIFs',
|
|
'gif.empty.favorites': 'No favorites yet — tap the star on any GIF.',
|
|
'gif.favorite.add': 'Add to favorites',
|
|
'gif.favorite.remove': 'Remove from favorites',
|
|
'gif.category.hello': 'hello',
|
|
'gif.category.lol': 'lol',
|
|
'gif.category.love': 'love',
|
|
'gif.category.birthday': 'happy birthday',
|
|
'gif.category.dance': 'dance',
|
|
'gif.category.facepalm': 'facepalm',
|
|
|
|
// Settings — privacy
|
|
'privacy.title': 'Privacy',
|
|
'privacy.discoverable.label': 'Allow others to find my profile',
|
|
'privacy.discoverable.description': 'When enabled, your profile appears in Discover People. Others can always add you by exact username.',
|
|
'privacy.activity.label': 'Share Activity Status',
|
|
'privacy.activity.description': "Allow others to see what you're up to, like games you're playing or music you're listening to.",
|
|
|
|
// Settings — connections
|
|
'connections.title': 'Connections',
|
|
'connections.spotify.description': 'Show what you are listening to on your profile.',
|
|
'connections.spotify.connect': 'Connect Spotify',
|
|
'connections.spotify.disconnect': 'Disconnect',
|
|
'connections.spotify.connected': 'Connected',
|
|
'connections.spotify.notConfigured': 'This instance has no Spotify credentials configured.',
|
|
'connections.spotify.hint': 'Only what is playing is read — playback cannot be controlled.',
|
|
'connections.spotify.error.denied': 'Authorisation was cancelled on Spotify.',
|
|
'connections.spotify.error.invalid_state': 'The authorisation link expired. Try again.',
|
|
'connections.spotify.error.exchange_failed': 'Spotify refused the authorisation. Try again.',
|
|
|
|
// Profile card
|
|
'profile.aboutMe': 'About Me',
|
|
'profile.memberSince': 'Member Since',
|
|
'profile.sendMessage': 'Send Message',
|
|
'profile.activity.playing': 'Playing',
|
|
'profile.activity.listening': 'Listening to',
|
|
'profile.activity.watching': 'Watching',
|
|
'profile.activity.streaming': 'Streaming',
|
|
'profile.activity.elapsed': '{time} elapsed',
|
|
} as const;
|
|
|
|
export type TranslationKey = keyof typeof en;
|
|
export type Dictionary = Record<TranslationKey, string>;
|