Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f01b246
feat: add Chinese (简体中文) i18n support
LuoYe17 May 12, 2026
82a078d
fix(i18n): localize hardcoded English strings and fix interpolation p…
LuoYe17 May 12, 2026
7c79f47
fix: localize missed setError call in SettingsHome
LuoYe17 May 12, 2026
1926697
fix: remove redundant ternary in SkillsStep i18n migration
LuoYe17 May 12, 2026
efc3655
fix: make default I18nContext resolve English translations
LuoYe17 May 12, 2026
0975740
fix: wrong i18n keys and interpolation bugs
LuoYe17 May 12, 2026
c3654f0
refactor: extract I18nContextValue interface from inline type
LuoYe17 May 12, 2026
0154784
Merge branch 'main' into claude/agitated-northcutt-ab62aa
LuoYe17 May 12, 2026
22d9430
fix: wrap main safety timeout string in t() and update zh-CN
LuoYe17 May 12, 2026
68b72fd
chore: trigger CI for Prettier re-check
LuoYe17 May 12, 2026
6c7d717
fix: resolve ESM/CJS interop for en import in default context
LuoYe17 May 12, 2026
18a975d
fix: use lazy resolveEn() to handle CJS interop at call time
LuoYe17 May 12, 2026
54d471c
Merge branch 'main' into claude/agitated-northcutt-ab62aa
LuoYe17 May 13, 2026
d2932af
Merge branch 'main' into pr/1518
senamakel May 15, 2026
7b05b4a
fix(merge): hooks rules + JSX cleanup post main-merge
senamakel May 15, 2026
7994e71
chore: apply auto-fixes
senamakel May 15, 2026
a1b6f7e
fix(i18n): resolve English copy drift so all 147 failing unit tests pass
senamakel May 15, 2026
789f0c3
style: apply Prettier formatting after i18n fix commit
senamakel May 15, 2026
69be391
Merge branch 'main' into pr/1518
senamakel May 15, 2026
4a6c315
fix(i18n): localize Saving/Sending labels and yes/no in panels
senamakel May 15, 2026
bf38a71
Merge remote-tracking branch 'upstream/main' into pr/1518
senamakel May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 21 additions & 18 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import PersistRehydrationScreen from './components/PersistRehydrationScreen';
import GlobalUpsellBanner from './components/upsell/GlobalUpsellBanner';
import AppWalkthrough from './components/walkthrough/AppWalkthrough';
import { MascotFrameProducer } from './features/meet/MascotFrameProducer';
import { I18nProvider } from './lib/i18n/I18nContext';
// [#1123] Commented out — welcome-agent onboarding replaced by Joyride walkthrough
// import { isWelcomeLocked } from './lib/coreState/store';
import { startNativeNotificationsService } from './lib/nativeNotifications';
Expand Down Expand Up @@ -51,24 +52,26 @@ function App() {
)}>
<Provider store={store}>
<PersistGate loading={<PersistRehydrationScreen />} persistor={persistor}>
<BootCheckGate>
<CoreStateProvider>
<SocketProvider>
<ChatRuntimeProvider>
<Router>
<CommandProvider>
<ServiceBlockingGate>
<AppShell />
<DictationHotkeyManager />
<LocalAIDownloadSnackbar />
<AppUpdatePrompt />
</ServiceBlockingGate>
</CommandProvider>
</Router>
</ChatRuntimeProvider>
</SocketProvider>
</CoreStateProvider>
</BootCheckGate>
<I18nProvider>
<BootCheckGate>
<CoreStateProvider>
<SocketProvider>
<ChatRuntimeProvider>
<Router>
<CommandProvider>
<ServiceBlockingGate>
<AppShell />
<DictationHotkeyManager />
<LocalAIDownloadSnackbar />
<AppUpdatePrompt />
</ServiceBlockingGate>
</CommandProvider>
</Router>
</ChatRuntimeProvider>
</SocketProvider>
</CoreStateProvider>
</BootCheckGate>
</I18nProvider>
Comment thread
LuoYe17 marked this conversation as resolved.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred — disagree (stale guideline)

CodeRabbit's required provider chain (Redux Provider → PersistGate → UserProvider → SocketProvider → AIProvider → SkillProvider → HashRouter) references providers (UserProvider, AIProvider, SkillProvider) that no longer exist in this codebase. Per current CLAUDE.md, the documented chain is:

Sentry.ErrorBoundary → Redux Provider → PersistGate → BootCheckGate → CoreStateProvider → SocketProvider → ChatRuntimeProvider → HashRouter → CommandProvider → ServiceBlockingGate → AppShell

The current code matches the documented chain. This PR only adds an I18nProvider wrapper; it doesn't reorder anything. No action taken.

</PersistGate>
</Provider>
</Sentry.ErrorBoundary>
Expand Down
121 changes: 56 additions & 65 deletions app/src/components/BootCheckGate/BootCheckGate.tsx

Large diffs are not rendered by default.

25 changes: 14 additions & 11 deletions app/src/components/BottomTabBar.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

import { useT } from '../lib/i18n/I18nContext';
// [#1123] Commented out — welcome-agent onboarding replaced by Joyride walkthrough
// import { isWelcomeLocked } from '../lib/coreState/store';
import { useCoreState } from '../providers/CoreStateProvider';
import { useAppSelector } from '../store/hooks';
import { selectUnreadCount } from '../store/notificationSlice';
import { isAccountsFullscreen } from '../utils/accountsFullscreen';

const tabs = [
const makeTabs = (t: (key: string) => string) => [
{
id: 'home',
label: 'Home',
label: t('nav.home'),
path: '/home',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand All @@ -26,7 +27,7 @@ const tabs = [
},
{
id: 'human',
label: 'Human',
label: t('nav.human'),
path: '/human',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand All @@ -41,7 +42,7 @@ const tabs = [
},
{
id: 'chat',
label: 'Chat',
label: t('nav.chat'),
path: '/chat',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand All @@ -56,7 +57,7 @@ const tabs = [
},
{
id: 'skills',
label: 'Connections',
label: t('nav.connections'),
path: '/skills',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand All @@ -71,7 +72,7 @@ const tabs = [
},
{
id: 'intelligence',
label: 'Memory',
label: t('nav.memory'),
path: '/intelligence',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand All @@ -86,7 +87,7 @@ const tabs = [
},
{
id: 'notifications',
label: 'Alerts',
label: t('nav.alerts'),
path: '/notifications',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand All @@ -101,7 +102,7 @@ const tabs = [
},
{
id: 'rewards',
label: 'Rewards',
label: t('nav.rewards'),
path: '/rewards',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand All @@ -116,7 +117,7 @@ const tabs = [
},
{
id: 'settings',
label: 'Settings',
label: t('nav.settings'),
path: '/settings',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand All @@ -138,6 +139,8 @@ const tabs = [
];

const BottomTabBar = () => {
const { t } = useT();
const tabs = useMemo(() => makeTabs(t), [t]);
const location = useLocation();
const navigate = useNavigate();
const { snapshot } = useCoreState();
Expand Down Expand Up @@ -230,7 +233,7 @@ const BottomTabBar = () => {
}`}
aria-label={
tab.id === 'notifications' && unreadCount > 0
? `${tab.label} (${unreadCount} unread)`
? `${tab.label} (${unreadCount} ${t('alerts.unread')})`
: tab.label
}>
<span className="relative inline-flex flex-shrink-0">
Expand Down
7 changes: 5 additions & 2 deletions app/src/components/channels/ChannelSetupModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';

import { useT } from '../../lib/i18n/I18nContext';
import type { ChannelDefinition, ChannelType } from '../../types/channels';
import DiscordConfig from './DiscordConfig';
import TelegramConfig from './TelegramConfig';
Expand All @@ -21,6 +22,7 @@ interface ChannelSetupModalProps {
}

function ChannelConfigContent({ definition }: { definition: ChannelDefinition }) {
const { t } = useT();
const channelId = definition.id as ChannelType;
switch (channelId) {
case 'telegram':
Expand All @@ -30,13 +32,14 @@ function ChannelConfigContent({ definition }: { definition: ChannelDefinition })
default:
return (
<p className="text-sm text-stone-400 py-4">
Configuration for {definition.display_name} is not available yet.
{t('channels.configNotAvailable')} {definition.display_name}
</p>
);
}
}

export default function ChannelSetupModal({ definition, onClose }: ChannelSetupModalProps) {
const { t } = useT();
const modalRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand Down Expand Up @@ -88,7 +91,7 @@ export default function ChannelSetupModal({ definition, onClose }: ChannelSetupM
{definition.display_name}
</h2>
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded-md bg-primary-500/15 text-primary-600">
channel
{t('channels.channel')}
</span>
</div>
<p className="text-xs text-stone-500 mt-1.5">{definition.description}</p>
Expand Down
15 changes: 10 additions & 5 deletions app/src/components/chat/TokenUsagePill.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useUsageState } from '../../hooks/useUsageState';
import { useT } from '../../lib/i18n/I18nContext';
import { useAppSelector } from '../../store/hooks';
import { BILLING_DASHBOARD_URL } from '../../utils/links';
import { openUrl } from '../../utils/openUrl';
Expand Down Expand Up @@ -42,6 +43,7 @@ function severityFromPct(pct: number): PillSeverity {
}

const TokenUsagePill = () => {
const { t } = useT();
const sessionTokens = useAppSelector(state => state.chatRuntime.sessionTokenUsage);
const { usagePct10h, usagePct7d, isAtLimit, isNearLimit, currentTier, teamUsage } =
useUsageState();
Expand All @@ -54,9 +56,9 @@ const TokenUsagePill = () => {
const showPlanPill = teamUsage !== null;

const planTitle = (() => {
if (isAtLimit) return 'Usage limit reached — click to top up';
if (isNearLimit) return 'Approaching usage limit';
return `${currentTier.toLowerCase()} plan — click for details`;
if (isAtLimit) return t('token.usageLimitReached');
if (isNearLimit) return t('token.approachingLimit');
return `${currentTier.toLowerCase()} ${t('token.planClickForDetails')}`;
})();

if (!showSessionCounter && !showPlanPill) return null;
Expand All @@ -66,7 +68,10 @@ const TokenUsagePill = () => {
{showSessionCounter ? (
<span
className="inline-flex items-center gap-1 rounded-full bg-stone-100 px-2 py-1 font-mono text-stone-600 ring-1 ring-stone-200/60"
title={`Session tokens: ${sessionTokens.inputTokens.toLocaleString()} in / ${sessionTokens.outputTokens.toLocaleString()} out across ${sessionTokens.turns} turn(s)`}>
title={t('token.sessionTokens')
.replace('{in}', sessionTokens.inputTokens.toLocaleString())
.replace('{out}', sessionTokens.outputTokens.toLocaleString())
.replace('{turns}', String(sessionTokens.turns))}>
<svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
Expand All @@ -86,7 +91,7 @@ const TokenUsagePill = () => {
}}
title={planTitle}
className={`inline-flex items-center gap-1 rounded-full px-2 py-1 font-medium ring-1 transition-colors ${planSeverity.bg} ${planSeverity.text} ${planSeverity.ring} hover:opacity-80`}>
{isAtLimit ? 'Limit' : planSeverity.label}
{isAtLimit ? t('token.limit') : planSeverity.label}
</button>
) : null}
</div>
Expand Down
10 changes: 6 additions & 4 deletions app/src/components/intelligence/ActionableCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

import { useT } from '../../lib/i18n/I18nContext';
import type { ActionableItem, SnoozeOption } from '../../types/intelligence';

interface ActionableCardProps {
Expand Down Expand Up @@ -207,6 +208,7 @@ export function ActionableCard({
onSnooze,
className = '',
}: ActionableCardProps) {
const { t } = useT();
const [showSnoozeMenu, setShowSnoozeMenu] = useState(false);
const [isAnimatingOut, setIsAnimatingOut] = useState(false);
const snoozeButtonRef = useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -284,7 +286,7 @@ export function ActionableCard({
<button
onClick={handleComplete}
className="w-6 h-6 flex items-center justify-center rounded-md text-stone-400 hover:text-sage-400 hover:bg-sage-400/10 transition-all duration-150"
title="Complete">
title={t('actionable.complete')}>
<svg
className="w-3.5 h-3.5"
fill="none"
Expand All @@ -303,7 +305,7 @@ export function ActionableCard({
<button
onClick={handleDismiss}
className="w-6 h-6 flex items-center justify-center rounded-md text-stone-400 hover:text-coral-400 hover:bg-coral-400/10 transition-all duration-150"
title="Dismiss">
title={t('actionable.dismiss')}>
<svg
className="w-3.5 h-3.5"
fill="none"
Expand All @@ -324,7 +326,7 @@ export function ActionableCard({
ref={snoozeButtonRef}
onClick={() => setShowSnoozeMenu(!showSnoozeMenu)}
className="w-6 h-6 flex items-center justify-center rounded-md text-stone-400 hover:text-amber-400 hover:bg-amber-400/10 transition-all duration-150"
title="Snooze">
title={t('actionable.snooze')}>
<svg
className="w-3.5 h-3.5"
fill="none"
Expand Down Expand Up @@ -354,7 +356,7 @@ export function ActionableCard({
<>
<span className="text-xs text-stone-600">•</span>
<span className="text-xs bg-sage-500 text-white px-1.5 py-0.5 rounded-sm font-medium">
New
{t('actionable.new')}
</span>
</>
)}
Expand Down
23 changes: 14 additions & 9 deletions app/src/components/intelligence/BackendChooser.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react';

import { useT } from '../../lib/i18n/I18nContext';
import type { Backend } from '../../lib/intelligence/settingsApi';

interface BackendChooserProps {
Expand Down Expand Up @@ -28,13 +29,17 @@ export default function BackendChooser({
costEstimate = '$0.42 / mo est.',
busy = false,
}: BackendChooserProps) {
const { t } = useT();
const [hoveredCloud, setHoveredCloud] = useState(false);

const cardBase =
'flex-1 min-h-[160px] px-6 py-5 rounded-2xl text-left transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed';

return (
<div className="flex gap-4 flex-col sm:flex-row" role="radiogroup" aria-label="AI backend">
<div
className="flex gap-4 flex-col sm:flex-row"
role="radiogroup"
aria-label={t('backend.aiBackend')}>
{/* Cloud */}
<button
type="button"
Expand All @@ -54,14 +59,14 @@ export default function BackendChooser({
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<RadioDot active={value === 'cloud'} />
<span className="text-sm font-semibold text-stone-900">Cloud</span>
<span className="text-sm font-semibold text-stone-900">{t('backend.cloud')}</span>
<span className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded-full bg-primary-50 text-primary-700 border border-primary-100">
Recommended
{t('backend.recommended')}
</span>
</div>
</div>
<p className="text-xs text-stone-600 leading-relaxed mb-3">
Runs on OpenHuman servers. Costs credits. No local CPU.
{t('backend.cloudDescription')}
</p>
<div className="font-mono text-[11px] text-stone-500">{costEstimate}</div>
{/* Privacy reassurance — appears on hover/focus of the Cloud card. */}
Expand All @@ -70,7 +75,7 @@ export default function BackendChooser({
hoveredCloud ? 'opacity-100' : 'opacity-0'
}`}
aria-live="polite">
Your data still stays local. bge-m3 embedder runs on your machine regardless.
{t('backend.privacyNote')}
</div>
</button>

Expand All @@ -89,14 +94,14 @@ export default function BackendChooser({
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<RadioDot active={value === 'local'} />
<span className="text-sm font-semibold text-stone-900">Local</span>
<span className="text-sm font-semibold text-stone-900">{t('backend.local')}</span>
<span className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded-full bg-stone-100 text-stone-600 border border-stone-200">
Advanced
{t('backend.advanced')}
</span>
</div>
</div>
<p className="text-xs text-stone-600 leading-relaxed mb-3">
Runs on your machine. Free. Uses your CPU and battery.
{t('backend.localDescription')}
</p>
<div className="flex items-center gap-1.5 text-[11px] text-amber-700">
<svg
Expand All @@ -111,7 +116,7 @@ export default function BackendChooser({
d="M12 9v3.75m0 3.75h.008v.008H12v-.008zM21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>≥8 GB RAM recommended</span>
<span>{t('backend.ramRecommended')}</span>
</div>
</button>
</div>
Expand Down
Loading