Files
backspace/packages/web/src/i18n/locales/en.ts
T
devsyncwrld 1830051732
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
feat(stats): voice-time and message leaderboards per space
Voice stays get their own table rather than joining the audit log: that table
records points in time, a call is an interval, and pairing join/leave point
events would leave every query guessing at joins whose leave never arrived.

Sessions are opened and closed inside joinRoom/leaveCurrentRoom rather than at
the seven call sites that reach them, so no path can be missed, and
destroyRoom closes them too — it bypasses leaveCurrentRoom and would otherwise
leak open rows.

A restart leaves sessions open with an unknowable end time. They are closed at
startedAt, discarding that time rather than inventing it: crediting the gap
would hand someone hours they never spent, and the numbers are the point.
Mirrors the existing users.status sweep on boot.

Only closed sessions count, so a figure does not move on every refresh. Bars
scale to the leader, not the total — with five people every share of a total
looks identical. Statistics are readable by any member, since they are the
group's own numbers; the audit log, which names who did what, stays on
MANAGE_SPACE.
2026-08-31 13:29:59 -03:00

119 lines
5.5 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.',
// 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>;