feat(i18n): translate five more systems, and check for updates more often
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
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s

Update cadence: the desktop app checked once ten seconds after launch and
then every four hours. A release published while the app sits open went
unnoticed until the next poll — which is exactly what happened with 1.4.0.
It now polls every 30 minutes and also checks when the window regains focus,
throttled to one check per five minutes so alt-tabbing does not hammer the
feed. `lastCheckAt` is stamped from the `checking-for-update` event rather
than by the callers, so a manual check from the settings panel counts too.
The focus listener is on `app`, not on mainWindow, because the window is
destroyed and recreated when closed to tray.

Translations, five whole systems:
  - Settings > Desktop (autostart, updates, instance)
  - Settings > Voice: output device, which completes that tab
  - Settings > Keybinds, including the action names and conflict prompt
  - Settings > Account, the largest — profile, password, detached-instance
    notice, danger zone
  - Explore spaces

Two sentences wrap a literal that must not be translated (the `input` group
name, the `user@instance` example). Those are split around a placeholder
rather than interpolated as text, so a translation keeps control of the word
order on both sides of it. Keybind action labels gained a `labelKey`
alongside the English `label`, which stays for non-React callers.

Verified: web build (tsc + vite), desktop typecheck, 490 web tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 13:38:28 -03:00
co-authored by Claude Opus 5
parent a54d998ba9
commit 115c81cba3
10 changed files with 448 additions and 132 deletions
@@ -8,8 +8,10 @@ import { getSpaceGradient } from '../../utils/gradients';
import { extractDominantColors, colorsToGradient } from '../../utils/colorExtractor';
import { MemberListToggleButton } from '../layout/MemberListToggleButton';
import { useSpaceJoin } from '../../hooks/useSpaceJoin';
import { useT } from '../../i18n';
export function ExplorePage() {
const t = useT();
const navigate = useNavigate();
const setCurrentSpace = useSpaceStore((s) => s.setCurrentSpace);
@@ -73,7 +75,7 @@ export function ExplorePage() {
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-5.5-2.5l7.51-3.49L17.5 6.5 9.99 9.99 6.5 17.5zm5.5-6.6c.61 0 1.1.49 1.1 1.1s-.49 1.1-1.1 1.1-1.1-.49-1.1-1.1.49-1.1 1.1-1.1z" />
</svg>
<span className="font-bold text-txt-primary">Explore</span>
<span className="font-bold text-txt-primary">{t('explore.title')}</span>
</div>
<div className="w-[1px] h-6 bg-surface-elevated mx-2" />
@@ -81,7 +83,7 @@ export function ExplorePage() {
<div className="relative flex-1 max-w-xs ml-2">
<input
type="text"
placeholder="Search spaces..."
placeholder={t('explore.searchPlaceholder')}
value={searchQuery}
onChange={(e) => handleSearchChange(e.target.value)}
className="input-search w-full"
@@ -107,7 +109,7 @@ export function ExplorePage() {
<div className="flex-1 overflow-y-auto">
{!discoveryEnabled && (
<div className="mx-6 mt-4 p-2.5 bg-accent-amber/10 border border-accent-amber/30 rounded text-[13px] text-accent-amber">
Space discovery is disabled by the instance administrator.
{t('explore.disabled')}
</div>
)}
@@ -124,9 +126,7 @@ export function ExplorePage() {
<div className="flex flex-col items-center justify-center h-64 opacity-80">
<Mascot state="lonely" className="w-32 h-32 mb-3" />
<p className="text-txt-tertiary text-sm">
{searchQuery
? 'No spaces match your search.'
: 'No discoverable spaces yet.'}
{searchQuery ? t('explore.empty.search') : t('explore.empty.none')}
</p>
</div>
) : (
@@ -138,7 +138,7 @@ export function ExplorePage() {
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
<span className="text-[13px] text-accent-mint">
You're in all discoverable spaces
{t('explore.allJoined')}
</span>
</div>
)}
@@ -173,7 +173,7 @@ export function ExplorePage() {
<path d="M7 10l5 5 5-5z" />
</svg>
<span className="text-xs font-semibold uppercase tracking-wider text-txt-tertiary group-hover:text-txt-secondary transition-colors">
Joined
{t('explore.joined')}
</span>
<span className="text-xs text-txt-tertiary/60">
{joinedSpaces.length}
@@ -207,6 +207,7 @@ function SpaceCard({
space: TaggedExploreSpace;
onJoinSuccess: (spaceId: string) => void;
}) {
const t = useT();
const {
isJoined,
isPublic,
@@ -297,7 +298,7 @@ function SpaceCard({
? 'bg-accent-mint/20 text-accent-mint'
: 'bg-accent-amber/20 text-accent-amber'
}`}>
{isPublic ? 'Public' : 'Request'}
{isPublic ? t('explore.badge.public') : t('explore.badge.request')}
</span>
</div>
@@ -330,7 +331,7 @@ function SpaceCard({
{space.description}
</p>
) : (
<p className="text-[13px] text-txt-tertiary italic mb-3 flex-1">No description</p>
<p className="text-[13px] text-txt-tertiary italic mb-3 flex-1">{t('explore.noDescription')}</p>
)}
<div className="flex items-center gap-3 text-[12px] text-txt-tertiary mb-3">
@@ -338,7 +339,7 @@ function SpaceCard({
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" className="opacity-60">
<path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z" />
</svg>
{space.memberCount} {space.memberCount === 1 ? 'member' : 'members'}
{space.memberCount} {t(space.memberCount === 1 ? 'explore.member' : 'explore.members')}
</span>
{originLabel && (
<span className="flex items-center gap-1 text-txt-tertiary/70">
@@ -360,7 +361,7 @@ function SpaceCard({
onClick={handleViewSpace}
className="w-full py-2 bg-accent-mint/15 hover:bg-accent-mint/25 text-accent-mint text-sm font-medium rounded transition-colors"
>
View Space
{t('explore.view')}
</button>
) : isPublic ? (
<button
@@ -371,10 +372,10 @@ function SpaceCard({
{joining ? (
<span className="flex items-center justify-center gap-2">
<LoadingSpinner />
Joining...
{t('explore.joining')}
</span>
) : (
'Join Space'
t('explore.join')
)}
</button>
) : isPending ? (
@@ -382,14 +383,14 @@ function SpaceCard({
disabled
className="w-full py-2 bg-interactive-muted text-txt-tertiary text-sm font-medium rounded cursor-default"
>
Request Pending
{t('explore.requestPending')}
</button>
) : showRequestForm ? (
<div className="space-y-2">
<textarea
value={requestMessage}
onChange={(e) => setRequestMessage(e.target.value.slice(0, 200))}
placeholder="Why do you want to join? (optional)"
placeholder={t('explore.request.placeholder')}
rows={2}
className="input-standard w-full resize-none"
/>
@@ -399,13 +400,13 @@ function SpaceCard({
disabled={joining}
className="flex-1 py-1.5 bg-accent-amber hover:bg-accent-amber/80 text-[#13131a] text-sm font-medium rounded transition-colors disabled:opacity-50"
>
{joining ? 'Sending...' : 'Send Request'}
{joining ? t('explore.request.sending') : t('explore.request.send')}
</button>
<button
onClick={cancelRequestForm}
className="px-3 py-1.5 text-sm text-txt-tertiary hover:text-txt-secondary transition-colors"
>
Cancel
{t('explore.request.cancel')}
</button>
</div>
</div>
@@ -414,7 +415,7 @@ function SpaceCard({
onClick={openRequestForm}
className="w-full py-2 bg-accent-amber/20 hover:bg-accent-amber/30 text-accent-amber text-sm font-medium rounded transition-colors"
>
Request to Join
{t('explore.requestToJoin')}
</button>
)}
</div>