Skip to content

Commit 02cf2ce

Browse files
LuoYe17senamakel
andauthored
feat: add Chinese (简体中文) i18n support (tinyhumansai#1518)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
1 parent 3dfd2b2 commit 02cf2ce

88 files changed

Lines changed: 3587 additions & 1193 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/src/App.tsx

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import PersistRehydrationScreen from './components/PersistRehydrationScreen';
1919
import GlobalUpsellBanner from './components/upsell/GlobalUpsellBanner';
2020
import AppWalkthrough from './components/walkthrough/AppWalkthrough';
2121
import { MascotFrameProducer } from './features/meet/MascotFrameProducer';
22+
import { I18nProvider } from './lib/i18n/I18nContext';
2223
// [#1123] Commented out — welcome-agent onboarding replaced by Joyride walkthrough
2324
// import { isWelcomeLocked } from './lib/coreState/store';
2425
import { startNativeNotificationsService } from './lib/nativeNotifications';
@@ -58,24 +59,26 @@ function App() {
5859
)}>
5960
<Provider store={store}>
6061
<PersistGate loading={<PersistRehydrationScreen />} persistor={persistor}>
61-
<BootCheckGate>
62-
<CoreStateProvider>
63-
<SocketProvider>
64-
<ChatRuntimeProvider>
65-
<Router>
66-
<CommandProvider>
67-
<ServiceBlockingGate>
68-
<AppShell />
69-
<DictationHotkeyManager />
70-
<LocalAIDownloadSnackbar />
71-
<AppUpdatePrompt />
72-
</ServiceBlockingGate>
73-
</CommandProvider>
74-
</Router>
75-
</ChatRuntimeProvider>
76-
</SocketProvider>
77-
</CoreStateProvider>
78-
</BootCheckGate>
62+
<I18nProvider>
63+
<BootCheckGate>
64+
<CoreStateProvider>
65+
<SocketProvider>
66+
<ChatRuntimeProvider>
67+
<Router>
68+
<CommandProvider>
69+
<ServiceBlockingGate>
70+
<AppShell />
71+
<DictationHotkeyManager />
72+
<LocalAIDownloadSnackbar />
73+
<AppUpdatePrompt />
74+
</ServiceBlockingGate>
75+
</CommandProvider>
76+
</Router>
77+
</ChatRuntimeProvider>
78+
</SocketProvider>
79+
</CoreStateProvider>
80+
</BootCheckGate>
81+
</I18nProvider>
7982
</PersistGate>
8083
</Provider>
8184
</Sentry.ErrorBoundary>

app/src/components/BootCheckGate/BootCheckGate.tsx

Lines changed: 56 additions & 65 deletions
Large diffs are not rendered by default.

app/src/components/BottomTabBar.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import { useState } from 'react';
1+
import { useMemo, useState } from 'react';
22
import { useLocation, useNavigate } from 'react-router-dom';
33

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

11-
const tabs = [
12+
const makeTabs = (t: (key: string) => string) => [
1213
{
1314
id: 'home',
14-
label: 'Home',
15+
label: t('nav.home'),
1516
path: '/home',
1617
icon: (
1718
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -26,7 +27,7 @@ const tabs = [
2627
},
2728
{
2829
id: 'human',
29-
label: 'Human',
30+
label: t('nav.human'),
3031
path: '/human',
3132
icon: (
3233
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -41,7 +42,7 @@ const tabs = [
4142
},
4243
{
4344
id: 'chat',
44-
label: 'Chat',
45+
label: t('nav.chat'),
4546
path: '/chat',
4647
icon: (
4748
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -56,7 +57,7 @@ const tabs = [
5657
},
5758
{
5859
id: 'skills',
59-
label: 'Connections',
60+
label: t('nav.connections'),
6061
path: '/skills',
6162
icon: (
6263
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -71,7 +72,7 @@ const tabs = [
7172
},
7273
{
7374
id: 'intelligence',
74-
label: 'Memory',
75+
label: t('nav.memory'),
7576
path: '/intelligence',
7677
icon: (
7778
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -86,7 +87,7 @@ const tabs = [
8687
},
8788
{
8889
id: 'notifications',
89-
label: 'Alerts',
90+
label: t('nav.alerts'),
9091
path: '/notifications',
9192
icon: (
9293
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -101,7 +102,7 @@ const tabs = [
101102
},
102103
{
103104
id: 'rewards',
104-
label: 'Rewards',
105+
label: t('nav.rewards'),
105106
path: '/rewards',
106107
icon: (
107108
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -116,7 +117,7 @@ const tabs = [
116117
},
117118
{
118119
id: 'settings',
119-
label: 'Settings',
120+
label: t('nav.settings'),
120121
path: '/settings',
121122
icon: (
122123
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -138,6 +139,8 @@ const tabs = [
138139
];
139140

140141
const BottomTabBar = () => {
142+
const { t } = useT();
143+
const tabs = useMemo(() => makeTabs(t), [t]);
141144
const location = useLocation();
142145
const navigate = useNavigate();
143146
const { snapshot } = useCoreState();
@@ -230,7 +233,7 @@ const BottomTabBar = () => {
230233
}`}
231234
aria-label={
232235
tab.id === 'notifications' && unreadCount > 0
233-
? `${tab.label} (${unreadCount} unread)`
236+
? `${tab.label} (${unreadCount} ${t('alerts.unread')})`
234237
: tab.label
235238
}>
236239
<span className="relative inline-flex flex-shrink-0">

app/src/components/channels/ChannelSetupModal.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { useEffect, useRef } from 'react';
66
import { createPortal } from 'react-dom';
77

8+
import { useT } from '../../lib/i18n/I18nContext';
89
import type { ChannelDefinition, ChannelType } from '../../types/channels';
910
import DiscordConfig from './DiscordConfig';
1011
import TelegramConfig from './TelegramConfig';
@@ -21,6 +22,7 @@ interface ChannelSetupModalProps {
2122
}
2223

2324
function ChannelConfigContent({ definition }: { definition: ChannelDefinition }) {
25+
const { t } = useT();
2426
const channelId = definition.id as ChannelType;
2527
switch (channelId) {
2628
case 'telegram':
@@ -30,13 +32,14 @@ function ChannelConfigContent({ definition }: { definition: ChannelDefinition })
3032
default:
3133
return (
3234
<p className="text-sm text-stone-400 py-4">
33-
Configuration for {definition.display_name} is not available yet.
35+
{t('channels.configNotAvailable')} {definition.display_name}
3436
</p>
3537
);
3638
}
3739
}
3840

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

4245
useEffect(() => {
@@ -88,7 +91,7 @@ export default function ChannelSetupModal({ definition, onClose }: ChannelSetupM
8891
{definition.display_name}
8992
</h2>
9093
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded-md bg-primary-500/15 text-primary-600">
91-
channel
94+
{t('channels.channel')}
9295
</span>
9396
</div>
9497
<p className="text-xs text-stone-500 mt-1.5">{definition.description}</p>

app/src/components/chat/TokenUsagePill.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useUsageState } from '../../hooks/useUsageState';
2+
import { useT } from '../../lib/i18n/I18nContext';
23
import { useAppSelector } from '../../store/hooks';
34
import { BILLING_DASHBOARD_URL } from '../../utils/links';
45
import { openUrl } from '../../utils/openUrl';
@@ -42,6 +43,7 @@ function severityFromPct(pct: number): PillSeverity {
4243
}
4344

4445
const TokenUsagePill = () => {
46+
const { t } = useT();
4547
const sessionTokens = useAppSelector(state => state.chatRuntime.sessionTokenUsage);
4648
const { usagePct10h, usagePct7d, isAtLimit, isNearLimit, currentTier, teamUsage } =
4749
useUsageState();
@@ -54,9 +56,9 @@ const TokenUsagePill = () => {
5456
const showPlanPill = teamUsage !== null;
5557

5658
const planTitle = (() => {
57-
if (isAtLimit) return 'Usage limit reached — click to top up';
58-
if (isNearLimit) return 'Approaching usage limit';
59-
return `${currentTier.toLowerCase()} plan — click for details`;
59+
if (isAtLimit) return t('token.usageLimitReached');
60+
if (isNearLimit) return t('token.approachingLimit');
61+
return `${currentTier.toLowerCase()} ${t('token.planClickForDetails')}`;
6062
})();
6163

6264
if (!showSessionCounter && !showPlanPill) return null;
@@ -66,7 +68,10 @@ const TokenUsagePill = () => {
6668
{showSessionCounter ? (
6769
<span
6870
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"
69-
title={`Session tokens: ${sessionTokens.inputTokens.toLocaleString()} in / ${sessionTokens.outputTokens.toLocaleString()} out across ${sessionTokens.turns} turn(s)`}>
71+
title={t('token.sessionTokens')
72+
.replace('{in}', sessionTokens.inputTokens.toLocaleString())
73+
.replace('{out}', sessionTokens.outputTokens.toLocaleString())
74+
.replace('{turns}', String(sessionTokens.turns))}>
7075
<svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
7176
<path
7277
strokeLinecap="round"
@@ -86,7 +91,7 @@ const TokenUsagePill = () => {
8691
}}
8792
title={planTitle}
8893
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`}>
89-
{isAtLimit ? 'Limit' : planSeverity.label}
94+
{isAtLimit ? t('token.limit') : planSeverity.label}
9095
</button>
9196
) : null}
9297
</div>

app/src/components/intelligence/ActionableCard.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useRef, useState } from 'react';
22
import { createPortal } from 'react-dom';
33

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

67
interface ActionableCardProps {
@@ -207,6 +208,7 @@ export function ActionableCard({
207208
onSnooze,
208209
className = '',
209210
}: ActionableCardProps) {
211+
const { t } = useT();
210212
const [showSnoozeMenu, setShowSnoozeMenu] = useState(false);
211213
const [isAnimatingOut, setIsAnimatingOut] = useState(false);
212214
const snoozeButtonRef = useRef<HTMLButtonElement>(null);
@@ -284,7 +286,7 @@ export function ActionableCard({
284286
<button
285287
onClick={handleComplete}
286288
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"
287-
title="Complete">
289+
title={t('actionable.complete')}>
288290
<svg
289291
className="w-3.5 h-3.5"
290292
fill="none"
@@ -303,7 +305,7 @@ export function ActionableCard({
303305
<button
304306
onClick={handleDismiss}
305307
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"
306-
title="Dismiss">
308+
title={t('actionable.dismiss')}>
307309
<svg
308310
className="w-3.5 h-3.5"
309311
fill="none"
@@ -324,7 +326,7 @@ export function ActionableCard({
324326
ref={snoozeButtonRef}
325327
onClick={() => setShowSnoozeMenu(!showSnoozeMenu)}
326328
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"
327-
title="Snooze">
329+
title={t('actionable.snooze')}>
328330
<svg
329331
className="w-3.5 h-3.5"
330332
fill="none"
@@ -354,7 +356,7 @@ export function ActionableCard({
354356
<>
355357
<span className="text-xs text-stone-600"></span>
356358
<span className="text-xs bg-sage-500 text-white px-1.5 py-0.5 rounded-sm font-medium">
357-
New
359+
{t('actionable.new')}
358360
</span>
359361
</>
360362
)}

app/src/components/intelligence/ConfirmationModal.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState } from 'react';
22

3+
import { useT } from '../../lib/i18n/I18nContext';
34
import type { ConfirmationModal as ConfirmationModalType } from '../../types/intelligence';
45

56
interface ConfirmationModalProps {
@@ -8,6 +9,7 @@ interface ConfirmationModalProps {
89
}
910

1011
export function ConfirmationModal({ modal, onClose }: ConfirmationModalProps) {
12+
const { t } = useT();
1113
const [dontShowAgain, setDontShowAgain] = useState(false);
1214

1315
if (!modal.isOpen) return null;
@@ -73,7 +75,7 @@ export function ConfirmationModal({ modal, onClose }: ConfirmationModalProps) {
7375
onChange={e => setDontShowAgain(e.target.checked)}
7476
className="rounded border-stone-300 bg-stone-100 text-primary-500 focus:ring-primary-500 focus:ring-offset-0"
7577
/>
76-
Don't show similar suggestions
78+
{t('modal.dontShowAgain')}
7779
</label>
7880
</div>
7981
)}
@@ -83,7 +85,7 @@ export function ConfirmationModal({ modal, onClose }: ConfirmationModalProps) {
8385
<button
8486
onClick={handleCancel}
8587
className="px-4 py-2 text-sm font-medium text-stone-600 hover:text-stone-900 rounded-lg hover:bg-stone-100 transition-colors">
86-
{modal.cancelText || 'Cancel'}
88+
{modal.cancelText || t('common.cancel')}
8789
</button>
8890
<button
8991
onClick={handleConfirm}
@@ -95,7 +97,7 @@ export function ConfirmationModal({ modal, onClose }: ConfirmationModalProps) {
9597
: 'bg-primary-500 hover:bg-primary-600 text-white'
9698
}
9799
`}>
98-
{modal.confirmText || 'Confirm'}
100+
{modal.confirmText || t('common.confirm')}
99101
</button>
100102
</div>
101103
</div>

0 commit comments

Comments
 (0)