@lobehub/chat 1.77.6 → 1.77.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/scripts/pr-comment.js +80 -0
- package/.github/scripts/pr-release-body.js +59 -0
- package/.github/workflows/release-desktop.yml +331 -0
- package/.github/workflows/test.yml +1 -1
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/next.config.ts +16 -11
- package/package.json +92 -89
- package/packages/electron-client-ipc/README.md +48 -0
- package/packages/electron-client-ipc/package.json +7 -0
- package/packages/electron-client-ipc/src/events/devtools.ts +6 -0
- package/packages/electron-client-ipc/src/events/index.ts +13 -0
- package/packages/electron-client-ipc/src/index.ts +2 -0
- package/packages/electron-client-ipc/src/types/dispatch.ts +10 -0
- package/packages/electron-client-ipc/src/types/index.ts +1 -0
- package/packages/electron-server-ipc/README.md +1 -1
- package/pnpm-workspace.yaml +1 -0
- package/scripts/setup-test-postgres-db.sh +21 -0
- package/src/app/desktop/devtools/page.tsx +89 -0
- package/src/app/desktop/layout.tsx +31 -0
- package/src/app/layout.tsx +11 -0
- package/src/app/not-found.tsx +1 -0
- package/src/const/desktop.ts +1 -0
- package/src/const/version.ts +2 -0
- package/src/database/client/db.ts +3 -10
- package/src/database/models/__tests__/message.test.ts +97 -26
- package/src/database/models/__tests__/session.test.ts +2 -0
- package/src/database/models/drizzleMigration.ts +15 -0
- package/src/database/models/message.ts +10 -5
- package/src/database/models/user.ts +3 -0
- package/src/features/DevPanel/features/FloatPanel.tsx +23 -6
- package/src/features/User/UserPanel/index.tsx +10 -6
- package/src/libs/trpc/middleware/userAuth.ts +10 -0
- package/src/server/routers/tools/__tests__/search.test.ts +1 -0
- package/src/server/translation.test.ts +72 -52
- package/src/server/translation.ts +2 -11
- package/src/services/electron/devtools.ts +9 -0
- package/src/styles/electron.ts +14 -0
- package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/index.tsx +3 -8
- package/src/tools/web-browsing/Render/Search/SearchResult/ShowMore.tsx +2 -4
- package/src/types/electron.ts +11 -0
- package/src/utils/electron/dispatch.ts +10 -0
- package/tsconfig.json +6 -6
- package/vitest.config.ts +3 -1
- package/vitest.server.config.ts +7 -3
@@ -1660,7 +1660,15 @@ describe('MessageModel', () => {
|
|
1660
1660
|
|
1661
1661
|
describe('getHeatmaps', () => {
|
1662
1662
|
it('should return heatmap data for the last year', async () => {
|
1663
|
-
|
1663
|
+
// 使用固定日期进行测试
|
1664
|
+
vi.useFakeTimers();
|
1665
|
+
const fixedDate = new Date('2023-04-07T13:00:00Z');
|
1666
|
+
vi.setSystemTime(fixedDate);
|
1667
|
+
|
1668
|
+
const today = dayjs(fixedDate);
|
1669
|
+
const twoDaysAgoDate = today.subtract(2, 'day').format('YYYY-MM-DD');
|
1670
|
+
const oneDayAgoDate = today.subtract(1, 'day').format('YYYY-MM-DD');
|
1671
|
+
const todayDate = today.format('YYYY-MM-DD');
|
1664
1672
|
|
1665
1673
|
// 创建测试数据
|
1666
1674
|
await serverDB.insert(messages).values([
|
@@ -1695,31 +1703,39 @@ describe('MessageModel', () => {
|
|
1695
1703
|
expect(result.length).toBeLessThan(368);
|
1696
1704
|
|
1697
1705
|
// 检查两天前的数据
|
1698
|
-
const twoDaysAgo = result.find(
|
1699
|
-
(item) => item.date === today.subtract(2, 'day').format('YYYY-MM-DD'),
|
1700
|
-
);
|
1706
|
+
const twoDaysAgo = result.find((item) => item.date === twoDaysAgoDate);
|
1701
1707
|
expect(twoDaysAgo?.count).toBe(2);
|
1702
1708
|
expect(twoDaysAgo?.level).toBe(1);
|
1703
1709
|
|
1704
1710
|
// 检查一天前的数据
|
1705
|
-
const oneDayAgo = result.find(
|
1706
|
-
(item) => item.date === today.subtract(1, 'day').format('YYYY-MM-DD'),
|
1707
|
-
);
|
1711
|
+
const oneDayAgo = result.find((item) => item.date === oneDayAgoDate);
|
1708
1712
|
expect(oneDayAgo?.count).toBe(1);
|
1709
1713
|
expect(oneDayAgo?.level).toBe(1);
|
1710
1714
|
|
1711
1715
|
// 检查今天的数据
|
1712
|
-
const todayData = result.find((item) => item.date ===
|
1716
|
+
const todayData = result.find((item) => item.date === todayDate);
|
1713
1717
|
expect(todayData?.count).toBe(0);
|
1714
1718
|
expect(todayData?.level).toBe(0);
|
1719
|
+
|
1720
|
+
vi.useRealTimers();
|
1715
1721
|
});
|
1716
1722
|
|
1717
1723
|
it('should calculate correct levels based on message count', async () => {
|
1718
|
-
|
1724
|
+
// 使用固定日期进行测试
|
1725
|
+
vi.useFakeTimers();
|
1726
|
+
const fixedDate = new Date('2023-05-15T12:00:00Z');
|
1727
|
+
vi.setSystemTime(fixedDate);
|
1728
|
+
|
1729
|
+
const today = dayjs(fixedDate);
|
1730
|
+
const fourDaysAgoDate = today.subtract(4, 'day').format('YYYY-MM-DD');
|
1731
|
+
const threeDaysAgoDate = today.subtract(3, 'day').format('YYYY-MM-DD');
|
1732
|
+
const twoDaysAgoDate = today.subtract(2, 'day').format('YYYY-MM-DD');
|
1733
|
+
const oneDayAgoDate = today.subtract(1, 'day').format('YYYY-MM-DD');
|
1734
|
+
const todayDate = today.format('YYYY-MM-DD');
|
1719
1735
|
|
1720
1736
|
// 创建测试数据 - 不同数量的消息以测试不同的等级
|
1721
1737
|
await serverDB.insert(messages).values([
|
1722
|
-
// 1 message - level
|
1738
|
+
// 1 message - level 1
|
1723
1739
|
{
|
1724
1740
|
id: '1',
|
1725
1741
|
userId,
|
@@ -1727,7 +1743,7 @@ describe('MessageModel', () => {
|
|
1727
1743
|
content: 'message 1',
|
1728
1744
|
createdAt: today.subtract(4, 'day').toDate(),
|
1729
1745
|
},
|
1730
|
-
// 6 messages - level
|
1746
|
+
// 6 messages - level 2
|
1731
1747
|
...Array(6)
|
1732
1748
|
.fill(0)
|
1733
1749
|
.map((_, i) => ({
|
@@ -1737,7 +1753,7 @@ describe('MessageModel', () => {
|
|
1737
1753
|
content: `message 2-${i}`,
|
1738
1754
|
createdAt: today.subtract(3, 'day').toDate(),
|
1739
1755
|
})),
|
1740
|
-
// 11 messages - level
|
1756
|
+
// 11 messages - level 3
|
1741
1757
|
...Array(11)
|
1742
1758
|
.fill(0)
|
1743
1759
|
.map((_, i) => ({
|
@@ -1747,7 +1763,7 @@ describe('MessageModel', () => {
|
|
1747
1763
|
content: `message 3-${i}`,
|
1748
1764
|
createdAt: today.subtract(2, 'day').toDate(),
|
1749
1765
|
})),
|
1750
|
-
// 16 messages - level
|
1766
|
+
// 16 messages - level 4
|
1751
1767
|
...Array(16)
|
1752
1768
|
.fill(0)
|
1753
1769
|
.map((_, i) => ({
|
@@ -1773,33 +1789,88 @@ describe('MessageModel', () => {
|
|
1773
1789
|
const result = await messageModel.getHeatmaps();
|
1774
1790
|
|
1775
1791
|
// 检查不同天数的等级
|
1776
|
-
const fourDaysAgo = result.find(
|
1777
|
-
(item) => item.date === today.subtract(4, 'day').format('YYYY-MM-DD'),
|
1778
|
-
);
|
1792
|
+
const fourDaysAgo = result.find((item) => item.date === fourDaysAgoDate);
|
1779
1793
|
expect(fourDaysAgo?.count).toBe(1);
|
1780
1794
|
expect(fourDaysAgo?.level).toBe(1);
|
1781
1795
|
|
1782
|
-
const threeDaysAgo = result.find(
|
1783
|
-
(item) => item.date === today.subtract(3, 'day').format('YYYY-MM-DD'),
|
1784
|
-
);
|
1796
|
+
const threeDaysAgo = result.find((item) => item.date === threeDaysAgoDate);
|
1785
1797
|
expect(threeDaysAgo?.count).toBe(6);
|
1786
1798
|
expect(threeDaysAgo?.level).toBe(2);
|
1787
1799
|
|
1788
|
-
const twoDaysAgo = result.find(
|
1789
|
-
(item) => item.date === today.subtract(2, 'day').format('YYYY-MM-DD'),
|
1790
|
-
);
|
1800
|
+
const twoDaysAgo = result.find((item) => item.date === twoDaysAgoDate);
|
1791
1801
|
expect(twoDaysAgo?.count).toBe(11);
|
1792
1802
|
expect(twoDaysAgo?.level).toBe(3);
|
1793
1803
|
|
1794
|
-
const oneDayAgo = result.find(
|
1795
|
-
(item) => item.date === today.subtract(1, 'day').format('YYYY-MM-DD'),
|
1796
|
-
);
|
1804
|
+
const oneDayAgo = result.find((item) => item.date === oneDayAgoDate);
|
1797
1805
|
expect(oneDayAgo?.count).toBe(16);
|
1798
1806
|
expect(oneDayAgo?.level).toBe(4);
|
1799
1807
|
|
1800
|
-
const todayData = result.find((item) => item.date ===
|
1808
|
+
const todayData = result.find((item) => item.date === todayDate);
|
1801
1809
|
expect(todayData?.count).toBe(21);
|
1802
1810
|
expect(todayData?.level).toBe(4);
|
1811
|
+
|
1812
|
+
vi.useRealTimers();
|
1813
|
+
});
|
1814
|
+
|
1815
|
+
it.skip('should return time count correctly when 19:00 time', async () => {
|
1816
|
+
// 使用固定日期进行测试
|
1817
|
+
vi.useFakeTimers();
|
1818
|
+
const fixedDate = new Date('2025-04-02T19:00:00Z');
|
1819
|
+
vi.setSystemTime(fixedDate);
|
1820
|
+
|
1821
|
+
const today = dayjs(fixedDate);
|
1822
|
+
const twoDaysAgoDate = today.subtract(2, 'day').format('YYYY-MM-DD');
|
1823
|
+
const oneDayAgoDate = today.subtract(1, 'day').format('YYYY-MM-DD');
|
1824
|
+
const todayDate = today.format('YYYY-MM-DD');
|
1825
|
+
|
1826
|
+
// 创建测试数据
|
1827
|
+
await serverDB.insert(messages).values([
|
1828
|
+
{
|
1829
|
+
id: '1',
|
1830
|
+
userId,
|
1831
|
+
role: 'user',
|
1832
|
+
content: 'message 1',
|
1833
|
+
createdAt: today.subtract(2, 'day').toDate(),
|
1834
|
+
},
|
1835
|
+
{
|
1836
|
+
id: '2',
|
1837
|
+
userId,
|
1838
|
+
role: 'user',
|
1839
|
+
content: 'message 2',
|
1840
|
+
createdAt: today.subtract(2, 'day').toDate(),
|
1841
|
+
},
|
1842
|
+
{
|
1843
|
+
id: '3',
|
1844
|
+
userId,
|
1845
|
+
role: 'user',
|
1846
|
+
content: 'message 3',
|
1847
|
+
createdAt: today.subtract(1, 'day').toDate(),
|
1848
|
+
},
|
1849
|
+
]);
|
1850
|
+
|
1851
|
+
// 调用 getHeatmaps 方法
|
1852
|
+
const result = await messageModel.getHeatmaps();
|
1853
|
+
|
1854
|
+
// 断言结果
|
1855
|
+
expect(result.length).toBeGreaterThanOrEqual(366);
|
1856
|
+
expect(result.length).toBeLessThan(368);
|
1857
|
+
|
1858
|
+
// 检查两天前的数据
|
1859
|
+
const twoDaysAgo = result.find((item) => item.date === twoDaysAgoDate);
|
1860
|
+
expect(twoDaysAgo?.count).toBe(2);
|
1861
|
+
expect(twoDaysAgo?.level).toBe(1);
|
1862
|
+
|
1863
|
+
// 检查一天前的数据
|
1864
|
+
const oneDayAgo = result.find((item) => item.date === oneDayAgoDate);
|
1865
|
+
expect(oneDayAgo?.count).toBe(1);
|
1866
|
+
expect(oneDayAgo?.level).toBe(1);
|
1867
|
+
|
1868
|
+
// 检查今天的数据
|
1869
|
+
const todayData = result.find((item) => item.date === todayDate);
|
1870
|
+
expect(todayData?.count).toBe(0);
|
1871
|
+
expect(todayData?.level).toBe(0);
|
1872
|
+
|
1873
|
+
vi.useRealTimers();
|
1803
1874
|
});
|
1804
1875
|
|
1805
1876
|
it('should handle empty data', async () => {
|
@@ -894,7 +894,9 @@ describe('SessionModel', () => {
|
|
894
894
|
await trx.insert(topics).values([
|
895
895
|
{ id: 't1', sessionId: '1', userId },
|
896
896
|
{ id: 't2', sessionId: '1', userId },
|
897
|
+
{ id: 't6', sessionId: '1', userId },
|
897
898
|
{ id: 't3', sessionId: '2', userId },
|
899
|
+
{ id: 't8', sessionId: '2', userId },
|
898
900
|
{ id: 't4', sessionId: '3', userId },
|
899
901
|
]);
|
900
902
|
});
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
2
|
+
|
1
3
|
import { LobeChatDatabase } from '@/database/type';
|
2
4
|
import { MigrationTableItem } from '@/types/clientDB';
|
3
5
|
|
@@ -8,6 +10,19 @@ export class DrizzleMigrationModel {
|
|
8
10
|
this.db = db;
|
9
11
|
}
|
10
12
|
|
13
|
+
getTableCounts = async () => {
|
14
|
+
// 这里使用 pg_tables 系统表查询用户表数量
|
15
|
+
const result = await this.db.execute(
|
16
|
+
sql`
|
17
|
+
SELECT COUNT(*) as table_count
|
18
|
+
FROM information_schema.tables
|
19
|
+
WHERE table_schema = 'public'
|
20
|
+
`,
|
21
|
+
);
|
22
|
+
|
23
|
+
return parseInt((result.rows[0] as any).table_count || '0');
|
24
|
+
};
|
25
|
+
|
11
26
|
getMigrationList = async () => {
|
12
27
|
const res = await this.db.execute(
|
13
28
|
'SELECT * FROM "drizzle"."__drizzle_migrations" ORDER BY "created_at" DESC;',
|
@@ -382,15 +382,20 @@ export class MessageModel {
|
|
382
382
|
.orderBy(desc(sql`heatmaps_date`));
|
383
383
|
|
384
384
|
const heatmapData: HeatmapsProps['data'] = [];
|
385
|
-
let currentDate = startDate;
|
385
|
+
let currentDate = startDate.clone();
|
386
|
+
|
387
|
+
const dateCountMap = new Map<string, number>();
|
388
|
+
for (const item of result) {
|
389
|
+
if (item?.date) {
|
390
|
+
const dateStr = dayjs(item.date as string).format('YYYY-MM-DD');
|
391
|
+
dateCountMap.set(dateStr, Number(item.count) || 0);
|
392
|
+
}
|
393
|
+
}
|
386
394
|
|
387
395
|
while (currentDate.isBefore(endDate) || currentDate.isSame(endDate, 'day')) {
|
388
396
|
const formattedDate = currentDate.format('YYYY-MM-DD');
|
389
|
-
const
|
390
|
-
(r) => r?.date && dayjs(r.date as string).format('YYYY-MM-DD') === formattedDate,
|
391
|
-
);
|
397
|
+
const count = dateCountMap.get(formattedDate) || 0;
|
392
398
|
|
393
|
-
const count = matchingResult ? matchingResult.count : 0;
|
394
399
|
const levelCount = count > 0 ? Math.ceil(count / 5) : 0;
|
395
400
|
const level = levelCount > 4 ? 4 : levelCount;
|
396
401
|
|
@@ -177,6 +177,9 @@ export class UserModel {
|
|
177
177
|
};
|
178
178
|
|
179
179
|
// Static method
|
180
|
+
static makeSureUserExist = async (db: LobeChatDatabase, userId: string) => {
|
181
|
+
await db.insert(users).values({ id: userId }).onConflictDoNothing();
|
182
|
+
};
|
180
183
|
|
181
184
|
static createUser = async (db: LobeChatDatabase, params: NewUser) => {
|
182
185
|
// if user already exists, skip creation
|
@@ -4,14 +4,16 @@ import { ActionIcon, FluentEmoji, Icon, SideNav } from '@lobehub/ui';
|
|
4
4
|
import { FloatButton } from 'antd';
|
5
5
|
import { createStyles } from 'antd-style';
|
6
6
|
import { BugIcon, BugOff, XIcon } from 'lucide-react';
|
7
|
+
import { usePathname } from 'next/navigation';
|
7
8
|
import { ReactNode, memo, useEffect, useState } from 'react';
|
8
9
|
import { Flexbox } from 'react-layout-kit';
|
9
10
|
import { Rnd } from 'react-rnd';
|
10
11
|
|
11
12
|
import { BRANDING_NAME } from '@/const/branding';
|
13
|
+
import { isDesktop } from '@/const/version';
|
12
14
|
|
13
15
|
// 定义样式
|
14
|
-
const useStyles = createStyles(({ token, css, prefixCls }) => {
|
16
|
+
export const useStyles = createStyles(({ token, css, prefixCls }) => {
|
15
17
|
return {
|
16
18
|
collapsed: css`
|
17
19
|
pointer-events: none;
|
@@ -86,6 +88,7 @@ const CollapsibleFloatPanel = memo<CollapsibleFloatPanelProps>(({ items }) => {
|
|
86
88
|
const [position, setPosition] = useState({ x: 100, y: 100 });
|
87
89
|
const [size, setSize] = useState({ height: minHeight, width: minWidth });
|
88
90
|
|
91
|
+
const pathname = usePathname();
|
89
92
|
useEffect(() => {
|
90
93
|
try {
|
91
94
|
const localStoragePosition = localStorage.getItem('debug-panel-position');
|
@@ -108,11 +111,25 @@ const CollapsibleFloatPanel = memo<CollapsibleFloatPanelProps>(({ items }) => {
|
|
108
111
|
|
109
112
|
return (
|
110
113
|
<>
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
114
|
+
{
|
115
|
+
// desktop devtools 下隐藏
|
116
|
+
pathname !== '/desktop/devtools' && (
|
117
|
+
<FloatButton
|
118
|
+
className={styles.floatButton}
|
119
|
+
icon={<Icon icon={isExpanded ? BugOff : BugIcon} />}
|
120
|
+
onClick={async () => {
|
121
|
+
if (isDesktop) {
|
122
|
+
const { electronDevtoolsService } = await import('@/services/electron/devtools');
|
123
|
+
|
124
|
+
await electronDevtoolsService.openDevtools();
|
125
|
+
|
126
|
+
return;
|
127
|
+
}
|
128
|
+
setIsExpanded(!isExpanded);
|
129
|
+
}}
|
130
|
+
/>
|
131
|
+
)
|
132
|
+
}
|
116
133
|
{isExpanded && (
|
117
134
|
<Rnd
|
118
135
|
bounds="window"
|
@@ -4,16 +4,20 @@ import { Popover } from 'antd';
|
|
4
4
|
import { createStyles } from 'antd-style';
|
5
5
|
import { PropsWithChildren, memo, useState } from 'react';
|
6
6
|
|
7
|
+
import { isDesktop } from '@/const/version';
|
8
|
+
|
7
9
|
import PanelContent from './PanelContent';
|
8
10
|
import UpgradeBadge from './UpgradeBadge';
|
9
11
|
import { useNewVersion } from './useNewVersion';
|
10
12
|
|
11
|
-
const useStyles = createStyles(({ css }) =>
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
const useStyles = createStyles(({ css }) => {
|
14
|
+
return {
|
15
|
+
popover: css`
|
16
|
+
inset-block-start: ${isDesktop ? 24 : 8}px !important;
|
17
|
+
inset-inline-start: 8px !important;
|
18
|
+
`,
|
19
|
+
};
|
20
|
+
});
|
17
21
|
|
18
22
|
const UserPanel = memo<PropsWithChildren>(({ children }) => {
|
19
23
|
const hasNewVersion = useNewVersion();
|
@@ -1,11 +1,21 @@
|
|
1
1
|
import { TRPCError } from '@trpc/server';
|
2
2
|
|
3
3
|
import { enableClerk } from '@/const/auth';
|
4
|
+
import { DESKTOP_USER_ID } from '@/const/desktop';
|
5
|
+
import { isDesktop } from '@/const/version';
|
4
6
|
|
5
7
|
import { trpc } from '../init';
|
6
8
|
|
7
9
|
export const userAuth = trpc.middleware(async (opts) => {
|
8
10
|
const { ctx } = opts;
|
11
|
+
|
12
|
+
if (isDesktop) {
|
13
|
+
return opts.next({
|
14
|
+
ctx: {
|
15
|
+
userId: DESKTOP_USER_ID,
|
16
|
+
},
|
17
|
+
});
|
18
|
+
}
|
9
19
|
// `ctx.user` is nullable
|
10
20
|
if (!ctx.userId) {
|
11
21
|
if (enableClerk) {
|
@@ -1,12 +1,9 @@
|
|
1
1
|
// @vitest-environment node
|
2
2
|
import { cookies } from 'next/headers';
|
3
|
-
import
|
4
|
-
import * as path from 'node:path';
|
5
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
6
4
|
|
7
|
-
import { DEFAULT_LANG
|
5
|
+
import { DEFAULT_LANG } from '@/const/locale';
|
8
6
|
import { normalizeLocale } from '@/locales/resources';
|
9
|
-
import * as env from '@/utils/env';
|
10
7
|
|
11
8
|
import { getLocale, translation } from './translation';
|
12
9
|
|
@@ -15,15 +12,6 @@ vi.mock('next/headers', () => ({
|
|
15
12
|
cookies: vi.fn(),
|
16
13
|
}));
|
17
14
|
|
18
|
-
vi.mock('node:fs', () => ({
|
19
|
-
existsSync: vi.fn(),
|
20
|
-
readFileSync: vi.fn(),
|
21
|
-
}));
|
22
|
-
|
23
|
-
vi.mock('node:path', () => ({
|
24
|
-
join: vi.fn(),
|
25
|
-
}));
|
26
|
-
|
27
15
|
vi.mock('@/const/locale', () => ({
|
28
16
|
DEFAULT_LANG: 'en-US',
|
29
17
|
LOBE_LOCALE_COOKIE: 'LOBE_LOCALE',
|
@@ -37,6 +25,28 @@ vi.mock('@/utils/env', () => ({
|
|
37
25
|
isDev: false,
|
38
26
|
}));
|
39
27
|
|
28
|
+
// 模拟动态导入结果
|
29
|
+
const mockTranslations = {
|
30
|
+
key1: 'Value 1',
|
31
|
+
key2: 'Value 2 with {{param}}',
|
32
|
+
nested: { key: 'Nested value' },
|
33
|
+
};
|
34
|
+
|
35
|
+
const mockDefaultTranslations = {
|
36
|
+
key1: '默认值 1',
|
37
|
+
key2: '默认值 2 带 {{param}}',
|
38
|
+
nested: { key: '默认嵌套值' },
|
39
|
+
};
|
40
|
+
|
41
|
+
// 重写导入函数
|
42
|
+
vi.mock('@/../locales/en-US/common.json', async () => {
|
43
|
+
return mockTranslations;
|
44
|
+
});
|
45
|
+
|
46
|
+
vi.mock('@/locales/default/common', async () => {
|
47
|
+
return mockDefaultTranslations;
|
48
|
+
});
|
49
|
+
|
40
50
|
describe('getLocale', () => {
|
41
51
|
const mockCookieStore = {
|
42
52
|
get: vi.fn(),
|
@@ -61,17 +71,12 @@ describe('getLocale', () => {
|
|
61
71
|
});
|
62
72
|
|
63
73
|
describe('translation', () => {
|
64
|
-
const mockTranslations = {
|
65
|
-
key1: 'Value 1',
|
66
|
-
key2: 'Value 2 with {{param}}',
|
67
|
-
nested: { key: 'Nested value' },
|
68
|
-
};
|
69
|
-
|
70
74
|
beforeEach(() => {
|
71
75
|
vi.clearAllMocks();
|
72
|
-
|
73
|
-
(
|
74
|
-
|
76
|
+
// 重置 import 模拟
|
77
|
+
vi.doMock('@/../locales/en-US/common.json', async () => {
|
78
|
+
return mockTranslations;
|
79
|
+
});
|
75
80
|
});
|
76
81
|
|
77
82
|
it('should return correct translation object', async () => {
|
@@ -88,43 +93,58 @@ describe('translation', () => {
|
|
88
93
|
expect(t('nested.key')).toBe('Nested value');
|
89
94
|
});
|
90
95
|
|
91
|
-
it('should
|
96
|
+
it('should handle multiple parameters in translation string', async () => {
|
97
|
+
// 模拟多参数翻译
|
98
|
+
vi.doMock('@/../locales/en-US/common.json', async () => ({
|
99
|
+
multiParam: 'Hello {{name}}, you have {{count}} messages',
|
100
|
+
}));
|
101
|
+
|
92
102
|
const { t } = await translation('common', 'en-US');
|
93
|
-
expect(t('
|
103
|
+
expect(t('multiParam', { name: 'John', count: '5' })).toBe('Hello John, you have 5 messages');
|
94
104
|
});
|
95
105
|
|
96
|
-
it('should
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
);
|
106
|
+
it('should handle different namespaces', async () => {
|
107
|
+
// 模拟另一个命名空间
|
108
|
+
vi.doMock('@/../locales/en-US/chat.json', async () => ({
|
109
|
+
welcome: 'Welcome to the chat',
|
110
|
+
}));
|
111
|
+
|
112
|
+
const { t } = await translation('chat', 'en-US');
|
113
|
+
expect(t('welcome')).toBe('Welcome to the chat');
|
103
114
|
});
|
104
115
|
|
105
|
-
it('should
|
106
|
-
|
107
|
-
(
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
116
|
+
it('should handle deep nested objects in translations', async () => {
|
117
|
+
// 模拟深层嵌套对象
|
118
|
+
vi.doMock('@/../locales/en-US/common.json', async () => ({
|
119
|
+
very: {
|
120
|
+
deeply: {
|
121
|
+
nested: {
|
122
|
+
key: 'Found the nested value',
|
123
|
+
},
|
124
|
+
},
|
125
|
+
},
|
126
|
+
}));
|
127
|
+
|
128
|
+
const { t } = await translation('common', 'en-US');
|
129
|
+
expect(t('very.deeply.nested.key')).toBe('Found the nested value');
|
113
130
|
});
|
114
131
|
|
115
|
-
it('should handle
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
});
|
132
|
+
it('should handle empty parameters object', async () => {
|
133
|
+
vi.doMock('@/../locales/en-US/common.json', async () => ({
|
134
|
+
simpleText: 'Just a simple text',
|
135
|
+
}));
|
120
136
|
|
121
|
-
const
|
122
|
-
expect(
|
123
|
-
|
124
|
-
'Error while reading translation file',
|
125
|
-
expect.any(Error),
|
126
|
-
);
|
137
|
+
const { t } = await translation('common', 'en-US');
|
138
|
+
expect(t('simpleText', {})).toBe('Just a simple text');
|
139
|
+
});
|
127
140
|
|
128
|
-
|
141
|
+
it('should handle missing parameters in translation string', async () => {
|
142
|
+
vi.doMock('@/../locales/en-US/common.json', async () => ({
|
143
|
+
withParam: 'Text with {{param}}',
|
144
|
+
}));
|
145
|
+
|
146
|
+
const { t } = await translation('common', 'en-US');
|
147
|
+
// 当缺少参数时应保留占位符
|
148
|
+
expect(t('withParam')).toBe('Text with {{param}}');
|
129
149
|
});
|
130
150
|
});
|
@@ -1,8 +1,6 @@
|
|
1
1
|
'use server';
|
2
2
|
|
3
3
|
import { get } from 'lodash-es';
|
4
|
-
import { existsSync, readFileSync } from 'node:fs';
|
5
|
-
import { join } from 'node:path';
|
6
4
|
|
7
5
|
import { DEFAULT_LANG } from '@/const/locale';
|
8
6
|
import { Locales, NS, normalizeLocale } from '@/locales/resources';
|
@@ -17,15 +15,8 @@ export const translation = async (ns: NS = 'common', hl: string) => {
|
|
17
15
|
let i18ns = {};
|
18
16
|
const lng = await getLocale(hl);
|
19
17
|
try {
|
20
|
-
|
21
|
-
|
22
|
-
if (!isExist)
|
23
|
-
filepath = join(
|
24
|
-
process.cwd(),
|
25
|
-
`locales/${normalizeLocale(isDev ? 'zh-CN' : DEFAULT_LANG)}/${ns}.json`,
|
26
|
-
);
|
27
|
-
const file = readFileSync(filepath, 'utf8');
|
28
|
-
i18ns = JSON.parse(file);
|
18
|
+
if (isDev && lng === 'zh-CN') i18ns = await import(`@/locales/default/${ns}`);
|
19
|
+
i18ns = await import(`@/../locales/${normalizeLocale(lng)}/${ns}.json`);
|
29
20
|
} catch (e) {
|
30
21
|
console.error('Error while reading translation file', e);
|
31
22
|
}
|
@@ -16,25 +16,22 @@ const useStyles = createStyles(({ css, token }) => {
|
|
16
16
|
flex: 1;
|
17
17
|
|
18
18
|
padding: 8px;
|
19
|
+
border-radius: 8px;
|
19
20
|
|
20
21
|
color: initial;
|
21
22
|
|
22
|
-
border-radius: 8px;
|
23
|
-
|
24
23
|
&:hover {
|
25
24
|
background: ${token.colorFillTertiary};
|
26
25
|
}
|
27
26
|
`,
|
28
27
|
desc: css`
|
29
28
|
overflow: hidden;
|
30
|
-
|
31
29
|
display: -webkit-box;
|
32
30
|
-webkit-box-orient: vertical;
|
31
|
+
-webkit-line-clamp: 2;
|
33
32
|
|
34
33
|
color: ${token.colorTextTertiary};
|
35
34
|
text-overflow: ellipsis;
|
36
|
-
|
37
|
-
-webkit-line-clamp: 2;
|
38
35
|
`,
|
39
36
|
displayLink: css`
|
40
37
|
color: ${token.colorTextQuaternary};
|
@@ -45,14 +42,12 @@ const useStyles = createStyles(({ css, token }) => {
|
|
45
42
|
`,
|
46
43
|
url: css`
|
47
44
|
overflow: hidden;
|
48
|
-
{ /* stylelint-disable-line */ }
|
49
45
|
display: -webkit-box;
|
50
46
|
-webkit-box-orient: vertical;
|
47
|
+
-webkit-line-clamp: 1;
|
51
48
|
|
52
49
|
color: ${token.colorTextDescription};
|
53
50
|
text-overflow: ellipsis;
|
54
|
-
|
55
|
-
-webkit-line-clamp: 1;
|
56
51
|
`,
|
57
52
|
};
|
58
53
|
});
|