feat(i18n): language foundation with en and pt-BR
Nothing in the project was translatable — every string sat inline in English. en.ts is the source dictionary and its type is derived from it, so a typo or a missing key fails typecheck instead of rendering the raw key at runtime. pt-BR.ts is deliberately Partial: translation proceeds one system per update and anything absent falls back to English, so a half-migrated interface is never broken, only partly English. Locale is persisted, guessed from the browser on first run, and kept in sync with <html lang> through a subscription — persisted state rehydrates after first paint, so a one-off assignment would miss it. Translates the voice input panel (including the mic test shipped earlier today) and the profile card as this round's system. Language options are labelled in the active language, so a wrong pick can always be undone.
This commit is contained in:
@@ -9,6 +9,8 @@ import { useAuthStore } from '../../stores/authStore';
|
||||
import { AccountPanel } from './settingsPanels/AccountPanel';
|
||||
import { VoicePanel } from './settingsPanels/VoicePanel';
|
||||
import { PrivacyPanel } from './settingsPanels/PrivacyPanel';
|
||||
import { LanguagePanel } from './settingsPanels/LanguagePanel';
|
||||
import { useT } from '../../i18n';
|
||||
import { ConnectionsPanel } from './settingsPanels/ConnectionsPanel';
|
||||
import { DesktopPanel } from './settingsPanels/DesktopPanel';
|
||||
import { InstancePanel } from './settingsPanels/InstancePanel';
|
||||
@@ -16,7 +18,7 @@ import { KeybindsPanel } from './settingsPanels/KeybindsPanel';
|
||||
import { isElectron } from '../../platform/platform';
|
||||
import { SettingsSectionsProvider, useSettingsSectionsContext } from './SettingsSectionsContext';
|
||||
|
||||
type SettingsTab = 'account' | 'voice' | 'privacy' | 'connections' | 'keybinds' | 'desktop' | 'instance';
|
||||
type SettingsTab = 'account' | 'voice' | 'privacy' | 'connections' | 'keybinds' | 'language' | 'desktop' | 'instance';
|
||||
|
||||
function SidebarSubLinks() {
|
||||
const ctx = useSettingsSectionsContext();
|
||||
@@ -60,6 +62,7 @@ export function UserSettingsModal() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
|
||||
const t = useT();
|
||||
const [tab, setTab] = useState<SettingsTab>('account');
|
||||
const [mobileView, setMobileView] = useState<'tabs' | 'content'>('tabs');
|
||||
// AGPL § 13: home-instance source offer. Fetched from the public info endpoint
|
||||
@@ -81,7 +84,7 @@ export function UserSettingsModal() {
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const requested = modalData.tab as SettingsTab | undefined;
|
||||
if (requested && ['account', 'voice', 'privacy', 'connections', 'keybinds', 'instance'].includes(requested)) {
|
||||
if (requested && ['account', 'voice', 'privacy', 'connections', 'keybinds', 'language', 'instance'].includes(requested)) {
|
||||
// Only allow instance tab for admins
|
||||
if (requested === 'instance' && !isAdmin) {
|
||||
setTab('account');
|
||||
@@ -135,14 +138,15 @@ export function UserSettingsModal() {
|
||||
{/* Nav list */}
|
||||
<div className="glass-bubble rounded-lg p-2 flex-1 flex flex-col">
|
||||
<div className="text-[10px] font-semibold text-txt-tertiary uppercase tracking-wider px-3 py-1">User Settings</div>
|
||||
<button onClick={() => handleTabClick('account')} className={tabClass('account')}>Account</button>
|
||||
<button onClick={() => handleTabClick('voice')} className={tabClass('voice')}>Voice & Video</button>
|
||||
<button onClick={() => handleTabClick('privacy')} className={tabClass('privacy')}>Privacy</button>
|
||||
<button onClick={() => handleTabClick('account')} className={tabClass('account')}>{t('settings.tab.account')}</button>
|
||||
<button onClick={() => handleTabClick('voice')} className={tabClass('voice')}>{t('settings.tab.voice')}</button>
|
||||
<button onClick={() => handleTabClick('privacy')} className={tabClass('privacy')}>{t('settings.tab.privacy')}</button>
|
||||
|
||||
<div className="border-t border-white/[0.04] my-2 mx-2" />
|
||||
<div className="text-[10px] font-semibold text-txt-tertiary uppercase tracking-wider px-3 py-1">App Settings</div>
|
||||
<button onClick={() => handleTabClick('connections')} className={tabClass('connections')}>Connections</button>
|
||||
<button onClick={() => handleTabClick('keybinds')} className={tabClass('keybinds')}>Keybinds</button>
|
||||
<button onClick={() => handleTabClick('connections')} className={tabClass('connections')}>{t('settings.tab.connections')}</button>
|
||||
<button onClick={() => handleTabClick('keybinds')} className={tabClass('keybinds')}>{t('settings.tab.keybinds')}</button>
|
||||
<button onClick={() => handleTabClick('language')} className={tabClass('language')}>{t('settings.tab.language')}</button>
|
||||
{isElectron() && <button onClick={() => handleTabClick('desktop')} className={tabClass('desktop')}>Desktop</button>}
|
||||
|
||||
{isAdmin && (
|
||||
@@ -192,14 +196,15 @@ export function UserSettingsModal() {
|
||||
|
||||
<div className="glass-bubble rounded-lg p-2 space-y-0.5">
|
||||
<div className="text-[10px] font-semibold text-txt-tertiary uppercase tracking-wider px-3 py-1">User Settings</div>
|
||||
<button onClick={() => handleTabClick('account')} className={tabClass('account')}>Account</button>
|
||||
<button onClick={() => handleTabClick('voice')} className={tabClass('voice')}>Voice & Video</button>
|
||||
<button onClick={() => handleTabClick('privacy')} className={tabClass('privacy')}>Privacy</button>
|
||||
<button onClick={() => handleTabClick('account')} className={tabClass('account')}>{t('settings.tab.account')}</button>
|
||||
<button onClick={() => handleTabClick('voice')} className={tabClass('voice')}>{t('settings.tab.voice')}</button>
|
||||
<button onClick={() => handleTabClick('privacy')} className={tabClass('privacy')}>{t('settings.tab.privacy')}</button>
|
||||
|
||||
<div className="border-t border-white/[0.04] my-2 mx-2" />
|
||||
<div className="text-[10px] font-semibold text-txt-tertiary uppercase tracking-wider px-3 py-1">App Settings</div>
|
||||
<button onClick={() => handleTabClick('connections')} className={tabClass('connections')}>Connections</button>
|
||||
<button onClick={() => handleTabClick('keybinds')} className={tabClass('keybinds')}>Keybinds</button>
|
||||
<button onClick={() => handleTabClick('connections')} className={tabClass('connections')}>{t('settings.tab.connections')}</button>
|
||||
<button onClick={() => handleTabClick('keybinds')} className={tabClass('keybinds')}>{t('settings.tab.keybinds')}</button>
|
||||
<button onClick={() => handleTabClick('language')} className={tabClass('language')}>{t('settings.tab.language')}</button>
|
||||
{isElectron() && <button onClick={() => handleTabClick('desktop')} className={tabClass('desktop')}>Desktop</button>}
|
||||
|
||||
{isAdmin && (
|
||||
@@ -249,6 +254,7 @@ export function UserSettingsModal() {
|
||||
{tab === 'privacy' && <PrivacyPanel />}
|
||||
{tab === 'connections' && <ConnectionsPanel />}
|
||||
{tab === 'keybinds' && <KeybindsPanel />}
|
||||
{tab === 'language' && <LanguagePanel />}
|
||||
{tab === 'desktop' && <DesktopPanel />}
|
||||
{tab === 'instance' && isAdmin && <InstancePanel />}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user