@lark-base-open/base-database-cli 1.1.0 → 1.3.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.
Files changed (222) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/LICENSE +6 -0
  3. package/README.md +55 -31
  4. package/dist/ai-functions.d.ts +42 -0
  5. package/dist/ai-functions.d.ts.map +1 -0
  6. package/dist/ai-functions.js +327 -0
  7. package/dist/ai-functions.js.map +1 -0
  8. package/dist/archive.d.ts +21 -0
  9. package/dist/archive.d.ts.map +1 -1
  10. package/dist/archive.js +65 -10
  11. package/dist/archive.js.map +1 -1
  12. package/dist/auth.d.ts +8 -1
  13. package/dist/auth.d.ts.map +1 -1
  14. package/dist/auth.js +60 -6
  15. package/dist/auth.js.map +1 -1
  16. package/dist/check.d.ts +74 -0
  17. package/dist/check.d.ts.map +1 -0
  18. package/dist/check.js +283 -0
  19. package/dist/check.js.map +1 -0
  20. package/dist/dependency-policy.d.ts +10 -0
  21. package/dist/dependency-policy.d.ts.map +1 -0
  22. package/dist/dependency-policy.js +162 -0
  23. package/dist/dependency-policy.js.map +1 -0
  24. package/dist/deploy.d.ts +1 -0
  25. package/dist/deploy.d.ts.map +1 -1
  26. package/dist/deploy.js +122 -175
  27. package/dist/deploy.js.map +1 -1
  28. package/dist/environment-variables.d.ts +52 -0
  29. package/dist/environment-variables.d.ts.map +1 -0
  30. package/dist/environment-variables.js +563 -0
  31. package/dist/environment-variables.js.map +1 -0
  32. package/dist/environment.d.ts +1 -1
  33. package/dist/environment.js +1 -1
  34. package/dist/environment.js.map +1 -1
  35. package/dist/function-input-types.d.ts +10 -0
  36. package/dist/function-input-types.d.ts.map +1 -0
  37. package/dist/function-input-types.js +160 -0
  38. package/dist/function-input-types.js.map +1 -0
  39. package/dist/function-schema.d.ts.map +1 -1
  40. package/dist/function-schema.js +89 -36
  41. package/dist/function-schema.js.map +1 -1
  42. package/dist/init-field-retry.d.ts +17 -0
  43. package/dist/init-field-retry.d.ts.map +1 -0
  44. package/dist/init-field-retry.js +127 -0
  45. package/dist/init-field-retry.js.map +1 -0
  46. package/dist/init-journal.d.ts +98 -0
  47. package/dist/init-journal.d.ts.map +1 -0
  48. package/dist/init-journal.js +492 -0
  49. package/dist/init-journal.js.map +1 -0
  50. package/dist/init-schema.d.ts +8 -0
  51. package/dist/init-schema.d.ts.map +1 -0
  52. package/dist/init-schema.js +423 -0
  53. package/dist/init-schema.js.map +1 -0
  54. package/dist/init.d.ts +14 -0
  55. package/dist/init.d.ts.map +1 -1
  56. package/dist/init.js +886 -243
  57. package/dist/init.js.map +1 -1
  58. package/dist/lark-cli-error.d.ts +46 -0
  59. package/dist/lark-cli-error.d.ts.map +1 -0
  60. package/dist/lark-cli-error.js +171 -0
  61. package/dist/lark-cli-error.js.map +1 -0
  62. package/dist/lark-cli.d.ts +2 -1
  63. package/dist/lark-cli.d.ts.map +1 -1
  64. package/dist/lark-cli.js +292 -29
  65. package/dist/lark-cli.js.map +1 -1
  66. package/dist/mutation-schema.d.ts +146 -0
  67. package/dist/mutation-schema.d.ts.map +1 -0
  68. package/dist/mutation-schema.js +153 -0
  69. package/dist/mutation-schema.js.map +1 -0
  70. package/dist/mutation.d.ts +10 -0
  71. package/dist/mutation.d.ts.map +1 -0
  72. package/dist/mutation.js +131 -0
  73. package/dist/mutation.js.map +1 -0
  74. package/dist/npm.d.ts +1 -0
  75. package/dist/npm.d.ts.map +1 -1
  76. package/dist/npm.js +2 -2
  77. package/dist/npm.js.map +1 -1
  78. package/dist/preview-receipt.d.ts.map +1 -1
  79. package/dist/preview-receipt.js +15 -5
  80. package/dist/preview-receipt.js.map +1 -1
  81. package/dist/process.d.ts +3 -1
  82. package/dist/process.d.ts.map +1 -1
  83. package/dist/process.js +16 -4
  84. package/dist/process.js.map +1 -1
  85. package/dist/program.d.ts.map +1 -1
  86. package/dist/program.js +241 -42
  87. package/dist/program.js.map +1 -1
  88. package/dist/project-build.d.ts +25 -0
  89. package/dist/project-build.d.ts.map +1 -0
  90. package/dist/project-build.js +229 -0
  91. package/dist/project-build.js.map +1 -0
  92. package/dist/project-context.d.ts +344 -0
  93. package/dist/project-context.d.ts.map +1 -0
  94. package/dist/project-context.js +485 -0
  95. package/dist/project-context.js.map +1 -0
  96. package/dist/project-layout.d.ts +5 -0
  97. package/dist/project-layout.d.ts.map +1 -0
  98. package/dist/project-layout.js +24 -0
  99. package/dist/project-layout.js.map +1 -0
  100. package/dist/public-sql-error.d.ts +25 -0
  101. package/dist/public-sql-error.d.ts.map +1 -0
  102. package/dist/public-sql-error.js +81 -0
  103. package/dist/public-sql-error.js.map +1 -0
  104. package/dist/pull.d.ts.map +1 -1
  105. package/dist/pull.js +54 -15
  106. package/dist/pull.js.map +1 -1
  107. package/dist/site-readme.d.ts +3 -0
  108. package/dist/site-readme.d.ts.map +1 -0
  109. package/dist/site-readme.js +83 -0
  110. package/dist/site-readme.js.map +1 -0
  111. package/dist/workspace-fingerprint.d.ts +21 -1
  112. package/dist/workspace-fingerprint.d.ts.map +1 -1
  113. package/dist/workspace-fingerprint.js +81 -2
  114. package/dist/workspace-fingerprint.js.map +1 -1
  115. package/package.json +2 -1
  116. package/skills/lark-base-database/SKILL.md +70 -53
  117. package/skills/lark-base-database/references/ai-function.md +471 -0
  118. package/skills/lark-base-database/references/base-capability-routing.md +3 -3
  119. package/skills/lark-base-database/references/database-function-limitations.md +46 -0
  120. package/skills/lark-base-database/references/database-function.md +349 -0
  121. package/skills/lark-base-database/references/design/components/actions-fields.md +16 -0
  122. package/skills/lark-base-database/references/design/components/code/controls.css +122 -0
  123. package/skills/lark-base-database/references/design/components/code/controls.tsx +73 -0
  124. package/skills/lark-base-database/references/design/components/code/date-picker.tsx +42 -0
  125. package/skills/lark-base-database/references/design/components/code/floating.tsx +41 -0
  126. package/skills/lark-base-database/references/design/components/code/overlays.tsx +19 -0
  127. package/skills/lark-base-database/references/design/components/code/select.tsx +34 -0
  128. package/skills/lark-base-database/references/design/components/code/styles.d.ts +1 -0
  129. package/skills/lark-base-database/references/design/components/code/theme.css +49 -0
  130. package/skills/lark-base-database/references/design/components/code/usage.tsx +38 -0
  131. package/skills/lark-base-database/references/design/components/date-time-number.md +9 -0
  132. package/skills/lark-base-database/references/design/components/feedback-navigation.md +9 -0
  133. package/skills/lark-base-database/references/design/components/index.md +18 -0
  134. package/skills/lark-base-database/references/design/components/overlays.md +9 -0
  135. package/skills/lark-base-database/references/design/components/patterns/flat-business.md +7 -0
  136. package/skills/lark-base-database/references/design/components/selection.md +24 -0
  137. package/skills/lark-base-database/references/design/components.md +91 -0
  138. package/skills/lark-base-database/references/design/composition.md +90 -0
  139. package/skills/lark-base-database/references/design/control-recipes.md +107 -0
  140. package/skills/lark-base-database/references/design/examples.md +172 -0
  141. package/skills/lark-base-database/references/design/foundations.md +126 -0
  142. package/skills/lark-base-database/references/design/review.md +82 -0
  143. package/skills/lark-base-database/references/design/scenes/analysis-report.md +66 -0
  144. package/skills/lark-base-database/references/design/scenes/brand-showcase.md +70 -0
  145. package/skills/lark-base-database/references/design/scenes/business-operations.md +85 -0
  146. package/skills/lark-base-database/references/design/scenes/campaign-report.md +66 -0
  147. package/skills/lark-base-database/references/design/scenes/dashboard.md +77 -0
  148. package/skills/lark-base-database/references/design/scenes/festive-campaign.md +69 -0
  149. package/skills/lark-base-database/references/design/theme.md +27 -0
  150. package/skills/lark-base-database/references/design/typography.md +56 -0
  151. package/skills/lark-base-database/references/development.md +165 -32
  152. package/skills/lark-base-database/references/environment-variables.md +55 -0
  153. package/skills/lark-base-database/references/faas.md +41 -48
  154. package/skills/lark-base-database/references/file-transfer.md +25 -25
  155. package/skills/lark-base-database/references/frontend-design-guide.md +33 -77
  156. package/skills/lark-base-database/references/mutations.md +295 -0
  157. package/skills/lark-base-database/references/permission-design.md +24 -24
  158. package/skills/lark-base-database/references/release.md +54 -27
  159. package/skills/lark-base-database/references/style-guide.md +10 -67
  160. package/skills/lark-base-database/references/verification.md +17 -13
  161. package/skills/lark-base-database/references/workspace.md +71 -22
  162. package/templates/database-starter/README.md +34 -4
  163. package/templates/database-starter/database-functions/README.md +8 -0
  164. package/templates/database-starter/mutations/README.md +19 -0
  165. package/templates/database-starter/page/README.md +25 -19
  166. package/templates/database-starter/page/package-lock.json +8 -16
  167. package/templates/database-starter/page/package.json +2 -3
  168. package/templates/database-starter/page/src/App.tsx +23 -3
  169. package/templates/database-starter/page/src/components/UserSelectorEditor/index.tsx +32 -4
  170. package/templates/database-starter/page/src/components/UserSelectorPanel/index.tsx +104 -34
  171. package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.test.ts +55 -11
  172. package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.ts +68 -25
  173. package/templates/database-starter/page/src/components/database-runtime-ready.tsx +1 -1
  174. package/templates/database-starter/page/src/components/database-user-menu/index.tsx +4 -7
  175. package/templates/database-starter/page/src/components/feedback/page-state.tsx +40 -5
  176. package/templates/database-starter/page/src/components/floating-button/assets/edit-active.svg +3 -0
  177. package/templates/database-starter/page/src/components/floating-button/assets/edit-default.svg +3 -0
  178. package/templates/database-starter/page/src/components/floating-button/index.tsx +75 -112
  179. package/templates/database-starter/page/src/components/floating-button/utils/project-meta.test.ts +107 -37
  180. package/templates/database-starter/page/src/components/floating-button/utils/project-meta.ts +13 -50
  181. package/templates/database-starter/page/src/components/ui-language.test.tsx +101 -0
  182. package/templates/database-starter/page/src/examples/ai-function/ImageTaskOperation.tsx +202 -0
  183. package/templates/database-starter/page/src/examples/ai-function/README.md +14 -0
  184. package/templates/database-starter/page/src/examples/ai-function/TextStreamOperation.tsx +184 -0
  185. package/templates/database-starter/page/src/examples/ai-function/ai-copy.ts +173 -0
  186. package/templates/database-starter/page/src/examples/ai-function/ai-delta.test.ts +30 -0
  187. package/templates/database-starter/page/src/examples/ai-function/ai-delta.ts +26 -0
  188. package/templates/database-starter/page/src/examples/ai-function/ai-language.test.tsx +86 -0
  189. package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.test.ts +201 -0
  190. package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.tsx +382 -0
  191. package/templates/database-starter/page/src/lib/attachment-image.tsx +144 -0
  192. package/templates/database-starter/page/src/lib/database-function-inputs.generated.ts +4 -0
  193. package/templates/database-starter/page/src/lib/database-runtime-attachment.test.ts +180 -76
  194. package/templates/database-starter/page/src/lib/database-runtime.test.ts +130 -10
  195. package/templates/database-starter/page/src/lib/database-runtime.ts +484 -51
  196. package/templates/database-starter/page/src/lib/date-time.ts +61 -0
  197. package/templates/database-starter/page/src/lib/ui-language.ts +86 -0
  198. package/templates/database-starter/page/src/main.tsx +1 -1
  199. package/templates/database-starter/page/tsconfig.json +2 -2
  200. package/templates/database-starter/page/vite.config.ts +1 -2
  201. package/templates/database-starter/prettierignore +1 -1
  202. package/templates/database-starter/server-functions/README.md +2 -2
  203. package/templates/database-starter/server-functions/package-lock.json +4 -4
  204. package/templates/database-starter/server-functions/package.json +1 -1
  205. package/dist/agent-guide.d.ts +0 -19
  206. package/dist/agent-guide.d.ts.map +0 -1
  207. package/dist/agent-guide.js +0 -31
  208. package/dist/agent-guide.js.map +0 -1
  209. package/dist/database-skill.d.ts +0 -24
  210. package/dist/database-skill.d.ts.map +0 -1
  211. package/dist/database-skill.js +0 -151
  212. package/dist/database-skill.js.map +0 -1
  213. package/skills/lark-base-database/references/base-sql-unsupported.md +0 -42
  214. package/skills/lark-base-database/references/database-capability-skill.md +0 -40
  215. package/skills/lark-base-database/references/sql-function.md +0 -289
  216. package/templates/database-capability/SKILL.md.tmpl +0 -58
  217. package/templates/database-starter/page/src/components/floating-button/chatbot-entry.test.ts +0 -23
  218. package/templates/database-starter/page/src/components/floating-button/chatbot-entry.tsx +0 -873
  219. package/templates/database-starter/page/src/components/floating-button/local-chat-panel.test.ts +0 -25
  220. package/templates/database-starter/page/src/components/floating-button/local-chat-panel.tsx +0 -197
  221. package/templates/database-starter/page/src/components/floating-button/utils/env.ts +0 -18
  222. package/templates/database-starter/sql/README.md +0 -8
@@ -8,6 +8,7 @@ import React, {
8
8
  } from 'react'
9
9
 
10
10
  import { cn } from '../../lib/cn'
11
+ import { useUiLanguage, type UiLanguage } from '../../lib/ui-language'
11
12
  import './index.css'
12
13
  import { requestUsers, type SearchUsersData, type User } from './user-search'
13
14
 
@@ -24,6 +25,57 @@ const SEARCH_CACHE_TTL_MS = 30_000
24
25
  const RECORD_CREATOR_AVATAR_URL =
25
26
  'https://lf-package-cn.feishucdn.com/obj/feishu-static/ccm/static_resource/scm_upload/base_filter_current_user.png'
26
27
 
28
+ const USER_SELECTOR_PANEL_COPY = {
29
+ en: {
30
+ invalidResponse: 'Member search response has an invalid structure',
31
+ loading: 'Loading...',
32
+ memberList: 'Member list',
33
+ noMatches: 'No matching members. Try another search term.',
34
+ noRecommendations: 'No recommended members yet',
35
+ recordCreator: 'Add the person who created this record',
36
+ searchFailed: 'Member search failed',
37
+ searchFailedCode: (code: number) =>
38
+ `Could not search members (error code ${code})`,
39
+ searchMembers: 'Search members',
40
+ },
41
+ zh: {
42
+ invalidResponse: '成员搜索响应结构无效',
43
+ loading: '加载中...',
44
+ memberList: '成员列表',
45
+ noMatches: '没有匹配的成员,请换个关键词',
46
+ noRecommendations: '暂无推荐成员',
47
+ recordCreator: '添加此记录的人员',
48
+ searchFailed: '搜索成员失败',
49
+ searchFailedCode: (code: number) => `搜索成员失败(错误码 ${code})`,
50
+ searchMembers: '搜索成员',
51
+ },
52
+ ja: {
53
+ invalidResponse: 'メンバー検索の応答形式が正しくありません',
54
+ loading: '読み込み中...',
55
+ memberList: 'メンバー一覧',
56
+ noMatches: '一致するメンバーがいません。別のキーワードで検索してください。',
57
+ noRecommendations: 'おすすめのメンバーがいません',
58
+ recordCreator: 'このレコードの作成者を追加',
59
+ searchFailed: 'メンバーの検索に失敗しました',
60
+ searchFailedCode: (code: number) =>
61
+ `メンバーを検索できませんでした(エラーコード ${code})`,
62
+ searchMembers: 'メンバーを検索',
63
+ },
64
+ } satisfies Record<
65
+ UiLanguage,
66
+ {
67
+ invalidResponse: string
68
+ loading: string
69
+ memberList: string
70
+ noMatches: string
71
+ noRecommendations: string
72
+ recordCreator: string
73
+ searchFailed: string
74
+ searchFailedCode: (code: number) => string
75
+ searchMembers: string
76
+ }
77
+ >
78
+
27
79
  interface SearchUsersCacheEntry {
28
80
  expiresAt: number
29
81
  request: Promise<SearchUsersData>
@@ -72,6 +124,8 @@ export default function UserSelectorPanel(
72
124
  mode,
73
125
  onKeyDown,
74
126
  } = props
127
+ const uiLanguage = useUiLanguage()
128
+ const i18n = USER_SELECTOR_PANEL_COPY[uiLanguage]
75
129
  const isValueControlled = Object.prototype.hasOwnProperty.call(props, 'value')
76
130
  const [innerValue, setInnerValue] = useState(defaultValue)
77
131
  const [query, setQuery] = useState('')
@@ -94,28 +148,31 @@ export default function UserSelectorPanel(
94
148
  activeIndex >= 0 ? `${generatedListboxId}-option-${activeIndex}` : undefined
95
149
  const isEmpty = !loading && optionCount === 0
96
150
 
97
- const getSearchResult = useCallback((keyword: string) => {
98
- const cacheKey = keyword.trim()
99
- const cachedResult = searchUsersCache.get(cacheKey)
100
- if (cachedResult && cachedResult.expiresAt > Date.now()) {
101
- return cachedResult.request
102
- }
103
- searchUsersCache.delete(cacheKey)
104
-
105
- const request = requestUsers(cacheKey)
106
- .then(normalizeSearchUsersResponse)
107
- .catch((error: unknown) => {
108
- if (searchUsersCache.get(cacheKey)?.request === request) {
109
- searchUsersCache.delete(cacheKey)
110
- }
111
- throw error
151
+ const getSearchResult = useCallback(
152
+ (keyword: string) => {
153
+ const cacheKey = `${uiLanguage}:${keyword.trim()}`
154
+ const cachedResult = searchUsersCache.get(cacheKey)
155
+ if (cachedResult && cachedResult.expiresAt > Date.now()) {
156
+ return cachedResult.request
157
+ }
158
+ searchUsersCache.delete(cacheKey)
159
+
160
+ const request = requestUsers(keyword.trim(), uiLanguage)
161
+ .then((value) => normalizeSearchUsersResponse(value, uiLanguage))
162
+ .catch((error: unknown) => {
163
+ if (searchUsersCache.get(cacheKey)?.request === request) {
164
+ searchUsersCache.delete(cacheKey)
165
+ }
166
+ throw error
167
+ })
168
+ searchUsersCache.set(cacheKey, {
169
+ expiresAt: Date.now() + SEARCH_CACHE_TTL_MS,
170
+ request,
112
171
  })
113
- searchUsersCache.set(cacheKey, {
114
- expiresAt: Date.now() + SEARCH_CACHE_TTL_MS,
115
- request,
116
- })
117
- return request
118
- }, [])
172
+ return request
173
+ },
174
+ [uiLanguage],
175
+ )
119
176
 
120
177
  const updateQuery = useCallback(
121
178
  (nextQuery: string, action: UserSelectorSearchAction) => {
@@ -149,7 +206,9 @@ export default function UserSelectorPanel(
149
206
  } catch (searchError) {
150
207
  if (requestSequence !== requestSequenceRef.current) return
151
208
  setError(
152
- searchError instanceof Error ? searchError.message : '搜索成员失败',
209
+ searchError instanceof Error
210
+ ? searchError.message
211
+ : i18n.searchFailed,
153
212
  )
154
213
  setUsers([])
155
214
  } finally {
@@ -158,7 +217,7 @@ export default function UserSelectorPanel(
158
217
  }
159
218
  }
160
219
  },
161
- [getSearchResult],
220
+ [getSearchResult, i18n],
162
221
  )
163
222
 
164
223
  useEffect(() => {
@@ -312,14 +371,14 @@ export default function UserSelectorPanel(
312
371
  }}
313
372
  onCompositionStart={() => setIsComposing(true)}
314
373
  onKeyDown={handleKeyDown}
315
- placeholder="搜索成员"
374
+ placeholder={i18n.searchMembers}
316
375
  ref={inputRef}
317
376
  role="searchbox"
318
377
  value={query}
319
378
  />
320
379
 
321
380
  <div
322
- aria-label="成员列表"
381
+ aria-label={i18n.memberList}
323
382
  aria-multiselectable={isMultiple || undefined}
324
383
  className={cn(
325
384
  'flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto px-1 py-1 [scrollbar-color:#bbbfc4_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:border [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-[#bbbfc4] [&::-webkit-scrollbar-thumb]:bg-clip-padding [&::-webkit-scrollbar]:w-2',
@@ -351,7 +410,9 @@ export default function UserSelectorPanel(
351
410
  className="mr-2 size-8 shrink-0 rounded-full object-cover"
352
411
  src={RECORD_CREATOR_AVATAR_URL}
353
412
  />
354
- <span className="min-w-0 flex-1 truncate">添加此记录的人员</span>
413
+ <span className="min-w-0 flex-1 truncate">
414
+ {i18n.recordCreator}
415
+ </span>
355
416
  <span aria-hidden="true" className="ml-[5px] size-3 shrink-0" />
356
417
  </button>
357
418
  ) : null}
@@ -427,7 +488,7 @@ export default function UserSelectorPanel(
427
488
  />
428
489
  </svg>
429
490
  </span>
430
- <span className="sr-only">加载中...</span>
491
+ <span className="sr-only">{i18n.loading}</span>
431
492
  </div>
432
493
  ) : null}
433
494
  {isEmpty ? (
@@ -436,8 +497,8 @@ export default function UserSelectorPanel(
436
497
  {error
437
498
  ? error
438
499
  : query.trim()
439
- ? '没有匹配结果,换个关键词试试吧'
440
- : '暂无推荐成员'}
500
+ ? i18n.noMatches
501
+ : i18n.noRecommendations}
441
502
  </div>
442
503
  </div>
443
504
  ) : null}
@@ -446,19 +507,28 @@ export default function UserSelectorPanel(
446
507
  )
447
508
  }
448
509
 
449
- function normalizeSearchUsersResponse(value: unknown): SearchUsersData {
510
+ function normalizeSearchUsersResponse(
511
+ value: unknown,
512
+ language: UiLanguage,
513
+ ): SearchUsersData {
514
+ const i18n = USER_SELECTOR_PANEL_COPY[language]
450
515
  if (!isRecord(value) || typeof value.code !== 'number') {
451
- throw new Error('搜索用户返回结构不正确')
516
+ throw new Error(i18n.invalidResponse)
452
517
  }
453
518
 
454
519
  if (value.code !== 0) {
455
- throw new Error(
456
- typeof value.msg === 'string' && value.msg ? value.msg : '搜索成员失败',
520
+ const message = typeof value.msg === 'string' ? value.msg.trim() : ''
521
+ throw Object.assign(
522
+ new Error(message || i18n.searchFailedCode(value.code)),
523
+ {
524
+ code: String(value.code),
525
+ cause: value,
526
+ },
457
527
  )
458
528
  }
459
529
 
460
530
  if (!isRecord(value.data) || !Array.isArray(value.data.users)) {
461
- throw new Error('搜索用户返回结构不正确')
531
+ throw new Error(i18n.invalidResponse)
462
532
  }
463
533
 
464
534
  return {
@@ -17,7 +17,11 @@ const recommendPayload = {
17
17
  department: '研究部',
18
18
  en_name: 'Ada',
19
19
  id: '6882577547121393684',
20
- i18n_department: { en_us: 'Research', zh_cn: '研究部' },
20
+ i18n_department: {
21
+ en_us: 'Research',
22
+ zh_cn: '研究部',
23
+ ja_jp: '研究部門',
24
+ },
21
25
  is_external: false,
22
26
  },
23
27
  '6882577547121393685': {
@@ -26,7 +30,11 @@ const recommendPayload = {
26
30
  department: '外部组织',
27
31
  en_name: 'Grace',
28
32
  id: '6882577547121393685',
29
- i18n_tenant: { en_us: 'External tenant', zh_cn: '外部租户' },
33
+ i18n_tenant: {
34
+ en_us: 'External tenant',
35
+ zh_cn: '外部租户',
36
+ ja_jp: '外部組織',
37
+ },
30
38
  is_external: true,
31
39
  },
32
40
  },
@@ -40,9 +48,7 @@ const recommendPayload = {
40
48
 
41
49
  beforeEach(() => {
42
50
  runtime.fetch.mockReset()
43
- vi.stubGlobal('document', {
44
- documentElement: { lang: 'zh-CN' },
45
- })
51
+ vi.stubGlobal('navigator', { language: 'en-US' })
46
52
  projectMeta.fetchProjectMeta.mockReset()
47
53
  projectMeta.fetchProjectMeta.mockResolvedValue({
48
54
  baseBindings: [{ baseToken: 'bascnDemo', tableIds: ['tblDemo'] }],
@@ -96,20 +102,28 @@ describe('requestUsers', () => {
96
102
  it('fails before transport when the Site has no bound Base', async () => {
97
103
  projectMeta.fetchProjectMeta.mockResolvedValue({ baseBindings: [] })
98
104
 
99
- await expect(requestUsers('ada')).rejects.toThrow('应用未绑定 Base')
105
+ await expect(requestUsers('ada')).rejects.toThrow(
106
+ 'application is not bound to a Base',
107
+ )
108
+ await expect(requestUsers('ada', 'zh')).rejects.toThrow(
109
+ '无法搜索成员:应用尚未绑定多维表格',
110
+ )
111
+ await expect(requestUsers('ada', 'ja')).rejects.toThrow(
112
+ 'メンバーを検索できません:アプリにBaseが関連付けられていません',
113
+ )
100
114
  expect(runtime.fetch).not.toHaveBeenCalled()
101
115
  })
102
116
 
103
117
  it('reports an HTTP failure without attempting to parse the body', async () => {
104
118
  runtime.fetch.mockResolvedValue(new Response('', { status: 503 }))
105
119
 
106
- await expect(requestUsers('ada')).rejects.toThrow('HTTP 状态码:503')
120
+ await expect(requestUsers('ada')).rejects.toThrow('HTTP 503')
107
121
  })
108
122
  })
109
123
 
110
124
  describe('normalizeRecommendUsersResponse', () => {
111
125
  it('keeps recommendation order and maps localized names and descriptions', () => {
112
- const result = normalizeRecommendUsersResponse(recommendPayload, 'en-US')
126
+ const result = normalizeRecommendUsersResponse(recommendPayload, 'en')
113
127
 
114
128
  expect(result.users).toEqual([
115
129
  expect.objectContaining({
@@ -146,7 +160,7 @@ describe('normalizeRecommendUsersResponse', () => {
146
160
  },
147
161
  result_list: [{ token: 'token_ada', type: 0 }],
148
162
  },
149
- 'en-US',
163
+ 'en',
150
164
  )
151
165
 
152
166
  expect(result.users).toEqual([
@@ -162,14 +176,44 @@ describe('normalizeRecommendUsersResponse', () => {
162
176
  expect(
163
177
  normalizeRecommendUsersResponse(
164
178
  { code: 0, data: recommendPayload, msg: 'ok' },
165
- 'zh-CN',
179
+ 'zh',
166
180
  ).users.map((user) => user.name),
167
181
  ).toEqual(['格蕾丝', '艾达'])
168
182
  })
169
183
 
184
+ it('uses Japanese names and descriptions when provided', () => {
185
+ const localizedPayload = {
186
+ ...recommendPayload,
187
+ entities: {
188
+ users: {
189
+ ...recommendPayload.entities.users,
190
+ '6882577547121393684': {
191
+ ...recommendPayload.entities.users['6882577547121393684'],
192
+ display_name: { ja_jp: 'エイダ' },
193
+ },
194
+ },
195
+ },
196
+ }
197
+ const result = normalizeRecommendUsersResponse(localizedPayload, 'ja')
198
+ expect(result.users.map((user) => user.name)).toEqual(['Grace', 'エイダ'])
199
+ expect(result.users.map((user) => user.description)).toEqual([
200
+ '外部組織',
201
+ '研究部門',
202
+ ])
203
+ })
204
+
170
205
  it('surfaces backend business errors', () => {
171
206
  expect(() =>
172
207
  normalizeRecommendUsersResponse({ code: 123, msg: '无权限' }),
173
- ).toThrow('无权限')
208
+ ).toThrow(/^无权限$/)
209
+ expect(() =>
210
+ normalizeRecommendUsersResponse({ code: 123, msg: '无权限' }, 'zh'),
211
+ ).toThrow(/^无权限$/)
212
+ expect(() =>
213
+ normalizeRecommendUsersResponse({ code: 123, msg: '無権限' }, 'ja'),
214
+ ).toThrow(/^無権限$/)
215
+ expect(() =>
216
+ normalizeRecommendUsersResponse({ code: 123, msg: ' ' }, 'en'),
217
+ ).toThrow('Could not search members (error code 123)')
174
218
  })
175
219
  })
@@ -1,4 +1,9 @@
1
1
  import { fetch as databaseFetch } from '../../lib/database-runtime'
2
+ import {
3
+ getUiLocale,
4
+ getUiLanguage,
5
+ type UiLanguage,
6
+ } from '../../lib/ui-language'
2
7
  import { fetchProjectMeta } from '../floating-button/utils/project-meta'
3
8
 
4
9
  const RECOMMEND_USERS_API_PATH = '/space/api/mention/recommend.v2/'
@@ -7,6 +12,41 @@ const BITABLE_MENTION_SOURCE = '4'
7
12
  const USER_MENTION_TYPE = '0'
8
13
  const SEARCH_PAGE_SIZE = 20
9
14
 
15
+ const USER_SEARCH_COPY = {
16
+ en: {
17
+ businessError: (code: number) =>
18
+ `Could not search members (error code ${code})`,
19
+ httpError: (status: number) =>
20
+ `Member search request failed: HTTP ${status}`,
21
+ invalidResponse: 'Member search response has an invalid structure',
22
+ noBoundBase:
23
+ 'Cannot search members: the application is not bound to a Base',
24
+ },
25
+ zh: {
26
+ businessError: (code: number) => `搜索成员失败(错误码 ${code})`,
27
+ httpError: (status: number) => `搜索成员请求失败:HTTP ${status}`,
28
+ invalidResponse: '成员搜索响应结构无效',
29
+ noBoundBase: '无法搜索成员:应用尚未绑定多维表格',
30
+ },
31
+ ja: {
32
+ businessError: (code: number) =>
33
+ `メンバーを検索できませんでした(エラーコード ${code})`,
34
+ httpError: (status: number) =>
35
+ `メンバー検索のリクエストに失敗しました:HTTP ${status}`,
36
+ invalidResponse: 'メンバー検索の応答形式が正しくありません',
37
+ noBoundBase:
38
+ 'メンバーを検索できません:アプリにBaseが関連付けられていません',
39
+ },
40
+ } satisfies Record<
41
+ UiLanguage,
42
+ {
43
+ businessError: (code: number) => string
44
+ httpError: (status: number) => string
45
+ invalidResponse: string
46
+ noBoundBase: string
47
+ }
48
+ >
49
+
10
50
  export interface UserAvatar {
11
51
  avatar_72: string
12
52
  avatar_240: string
@@ -38,11 +78,14 @@ export type SearchUsers = (keyword: string) => Promise<SearchUsersResponse>
38
78
 
39
79
  export async function requestUsers(
40
80
  keyword: string,
81
+ language?: UiLanguage,
41
82
  ): Promise<SearchUsersResponse> {
83
+ const selectedLanguage = language ?? getUiLanguage()
84
+ const i18n = USER_SEARCH_COPY[selectedLanguage]
42
85
  const projectMeta = await fetchProjectMeta()
43
86
  const baseToken = projectMeta.baseBindings[0]?.baseToken
44
87
  if (!baseToken) {
45
- throw new Error('应用未绑定 Base,无法搜索成员')
88
+ throw new Error(i18n.noBoundBase)
46
89
  }
47
90
 
48
91
  const body = new URLSearchParams({
@@ -66,30 +109,32 @@ export async function requestUsers(
66
109
  })
67
110
 
68
111
  if (!response.ok) {
69
- throw new Error(`搜索用户请求失败,HTTP 状态码:${response.status}`)
112
+ throw new Error(i18n.httpError(response.status))
70
113
  }
71
114
 
72
115
  const payload: unknown = await response.json()
73
116
  return {
74
117
  code: 0,
75
118
  msg: 'ok',
76
- data: normalizeRecommendUsersResponse(payload),
119
+ data: normalizeRecommendUsersResponse(payload, selectedLanguage),
77
120
  }
78
121
  }
79
122
 
80
123
  export function normalizeRecommendUsersResponse(
81
124
  value: unknown,
82
- locale = getLocale(),
125
+ language: UiLanguage = getUiLanguage(),
83
126
  ): SearchUsersData {
84
- const payload = unwrapRecommendPayload(value)
127
+ const locale = getUiLocale(language)
128
+ const i18n = USER_SEARCH_COPY[language]
129
+ const payload = unwrapRecommendPayload(value, i18n)
85
130
  if (!payload) {
86
- throw new Error('搜索用户返回结构不正确')
131
+ throw new Error(i18n.invalidResponse)
87
132
  }
88
133
 
89
134
  const entities = payload.entities
90
135
  const users = isRecord(entities) ? entities.users : undefined
91
136
  if (!isRecord(users) || !Array.isArray(payload.result_list)) {
92
- throw new Error('搜索用户返回结构不正确')
137
+ throw new Error(i18n.invalidResponse)
93
138
  }
94
139
 
95
140
  const result: User[] = []
@@ -130,11 +175,16 @@ export function normalizeRecommendUsersResponse(
130
175
 
131
176
  function unwrapRecommendPayload(
132
177
  value: unknown,
178
+ i18n: (typeof USER_SEARCH_COPY)[UiLanguage],
133
179
  ): Record<string, unknown> | undefined {
134
180
  if (!isRecord(value)) return undefined
135
181
 
136
182
  if (typeof value.code === 'number' && value.code !== 0) {
137
- throw new Error(readString(value.msg) || '搜索成员失败')
183
+ const message = readString(value.msg)
184
+ throw Object.assign(new Error(message || i18n.businessError(value.code)), {
185
+ code: String(value.code),
186
+ cause: value,
187
+ })
138
188
  }
139
189
 
140
190
  if ('entities' in value && 'result_list' in value) return value
@@ -144,12 +194,12 @@ function unwrapRecommendPayload(
144
194
  function readUserName(entity: Record<string, unknown>, locale: string): string {
145
195
  return (
146
196
  readLocalizedString(entity.display_name, locale) ||
147
- (locale.toLowerCase().startsWith('en')
148
- ? readString(entity.en_name)
149
- : readString(entity.cn_name)) ||
197
+ (locale.toLowerCase().startsWith('zh')
198
+ ? readString(entity.cn_name)
199
+ : readString(entity.en_name)) ||
150
200
  readString(entity.name) ||
151
- readString(entity.cn_name) ||
152
- readString(entity.en_name)
201
+ readString(entity.en_name) ||
202
+ readString(entity.cn_name)
153
203
  )
154
204
  }
155
205
 
@@ -168,11 +218,14 @@ function readLocalizedString(value: unknown, locale: string): string {
168
218
 
169
219
  const normalizedLocale = locale.toLowerCase().replace('-', '_')
170
220
  const language = normalizedLocale.split('_')[0] ?? ''
221
+ const fallback =
222
+ language === 'zh'
223
+ ? readString(value.zh_cn) || readString(value.en_us)
224
+ : readString(value.en_us) || readString(value.zh_cn)
171
225
  return (
172
226
  readString(value[normalizedLocale]) ||
173
227
  readString(value[language]) ||
174
- readString(value.zh_cn) ||
175
- readString(value.en_us)
228
+ fallback
176
229
  )
177
230
  }
178
231
 
@@ -186,16 +239,6 @@ function readStringArray(value: unknown): string[] {
186
239
  : []
187
240
  }
188
241
 
189
- function getLocale(): string {
190
- if (typeof document !== 'undefined' && document.documentElement.lang) {
191
- return document.documentElement.lang
192
- }
193
- if (typeof navigator !== 'undefined' && navigator.language) {
194
- return navigator.language
195
- }
196
- return 'zh-CN'
197
- }
198
-
199
242
  function isRecord(value: unknown): value is Record<string, unknown> {
200
243
  return typeof value === 'object' && value !== null && !Array.isArray(value)
201
244
  }
@@ -11,7 +11,7 @@ import { siteRuntime } from '../lib/database-runtime'
11
11
  export function DatabaseRuntimeReady(): null {
12
12
  useEffect(() => {
13
13
  void siteRuntime.ready().catch((error: unknown) => {
14
- console.error('Application Runtime ready 上报失败', error)
14
+ console.error('Failed to report Application Runtime readiness', error)
15
15
  })
16
16
  }, [])
17
17
 
@@ -5,8 +5,7 @@ import {
5
5
  } from '../floating-button/utils/current-user'
6
6
  import { fetchProjectMeta } from '../floating-button/utils/project-meta'
7
7
  import { logout } from '../../lib/database-runtime'
8
-
9
- export type DatabaseUserMenuLanguage = 'en' | 'zh'
8
+ import { useUiLanguage, type UiLanguage } from '../../lib/ui-language'
10
9
 
11
10
  /** 应用面向的使用场景。
12
11
  *
@@ -17,8 +16,6 @@ export type DatabaseUserMenuLanguage = 'en' | 'zh'
17
16
  export type DatabaseAudience = 'enterprise' | 'personal'
18
17
 
19
18
  interface DatabaseUserMenuProps {
20
- /** 界面语言,默认 en。 */
21
- language?: DatabaseUserMenuLanguage
22
19
  /** 默认 enterprise:拿不准时宁可多显示一个头像,也不要让用户找不到退出登录。 */
23
20
  audience?: DatabaseAudience
24
21
  /** 弹层相对触发器的对齐方向,默认 right(触发器在右上角时用)。 */
@@ -35,9 +32,10 @@ interface DatabaseUserMenuI18n {
35
32
  logout: string
36
33
  }
37
34
 
38
- const I18N: Record<DatabaseUserMenuLanguage, DatabaseUserMenuI18n> = {
35
+ const I18N: Record<UiLanguage, DatabaseUserMenuI18n> = {
39
36
  en: { account: 'Account', logout: 'Log out' },
40
37
  zh: { account: '账号', logout: '退出登录' },
38
+ ja: { account: 'アカウント', logout: 'ログアウト' },
41
39
  }
42
40
 
43
41
  /** 应用自身导航里的当前用户入口:头像 + 姓名,点开是组织名与退出登录。
@@ -51,7 +49,6 @@ const I18N: Record<DatabaseUserMenuLanguage, DatabaseUserMenuI18n> = {
51
49
  * 不占位、不显示空头像。
52
50
  */
53
51
  export function DatabaseUserMenu({
54
- language = 'en',
55
52
  audience = 'enterprise',
56
53
  align = 'right',
57
54
  side = 'down',
@@ -62,7 +59,7 @@ export function DatabaseUserMenu({
62
59
  const [organization, setOrganization] = useState('')
63
60
  const [isOpen, setIsOpen] = useState(false)
64
61
  const rootRef = useRef<HTMLDivElement>(null)
65
- const i18n = I18N[language]
62
+ const i18n = I18N[useUiLanguage()]
66
63
 
67
64
  useEffect(() => {
68
65
  if (audience !== 'enterprise') {
@@ -1,6 +1,40 @@
1
1
  import { CircleAlert, Inbox, LoaderCircle } from 'lucide-react'
2
+ import { useUiLanguage, type UiLanguage } from '../../lib/ui-language'
2
3
  import { Button } from '../ui/button'
3
4
 
5
+ const PAGE_STATE_COPY = {
6
+ en: {
7
+ empty: 'No data yet',
8
+ error: 'Could not load data',
9
+ loading: 'Loading',
10
+ retry: 'Retry',
11
+ retryLater: 'Please try again later',
12
+ },
13
+ zh: {
14
+ empty: '暂无数据',
15
+ error: '加载失败',
16
+ loading: '正在加载',
17
+ retry: '重试',
18
+ retryLater: '请稍后重试',
19
+ },
20
+ ja: {
21
+ empty: 'データがありません',
22
+ error: 'データを読み込めませんでした',
23
+ loading: '読み込み中',
24
+ retry: '再試行',
25
+ retryLater: 'しばらくしてからもう一度お試しください',
26
+ },
27
+ } satisfies Record<
28
+ UiLanguage,
29
+ {
30
+ empty: string
31
+ error: string
32
+ loading: string
33
+ retry: string
34
+ retryLater: string
35
+ }
36
+ >
37
+
4
38
  interface PageStateProps {
5
39
  status: 'loading' | 'empty' | 'error'
6
40
  title?: string
@@ -15,6 +49,7 @@ export function PageState({
15
49
  description,
16
50
  onRetry,
17
51
  }: PageStateProps) {
52
+ const i18n = PAGE_STATE_COPY[useUiLanguage()]
18
53
  if (status === 'loading') {
19
54
  return (
20
55
  <div
@@ -25,7 +60,7 @@ export function PageState({
25
60
  aria-hidden="true"
26
61
  className="size-6 motion-safe:animate-spin"
27
62
  />
28
- <p className="text-sm font-medium">{title ?? '正在加载'}</p>
63
+ <p className="text-sm font-medium">{title ?? i18n.loading}</p>
29
64
  </div>
30
65
  )
31
66
  }
@@ -35,7 +70,7 @@ export function PageState({
35
70
  <div className="flex min-h-48 flex-col items-center justify-center gap-3 text-center">
36
71
  <Inbox aria-hidden="true" className="size-7 text-muted-foreground" />
37
72
  <div>
38
- <p className="font-semibold">{title ?? '暂无数据'}</p>
73
+ <p className="font-semibold">{title ?? i18n.empty}</p>
39
74
  {description ? (
40
75
  <p className="mt-1 text-sm text-muted-foreground">{description}</p>
41
76
  ) : null}
@@ -51,14 +86,14 @@ export function PageState({
51
86
  >
52
87
  <CircleAlert aria-hidden="true" className="size-7 text-danger" />
53
88
  <div>
54
- <p className="font-semibold">{title ?? '加载失败'}</p>
89
+ <p className="font-semibold">{title ?? i18n.error}</p>
55
90
  <p className="mt-1 text-sm text-muted-foreground">
56
- {description ?? '请稍后重试'}
91
+ {description ?? i18n.retryLater}
57
92
  </p>
58
93
  </div>
59
94
  {onRetry ? (
60
95
  <Button variant="secondary" onClick={onRetry}>
61
- 重试
96
+ {i18n.retry}
62
97
  </Button>
63
98
  ) : null}
64
99
  </div>
@@ -0,0 +1,3 @@
1
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M17.1956 16.3595C17.6558 16.3595 18.0289 16.7326 18.0289 17.1928C18.0287 17.6529 17.6557 18.0261 17.1956 18.0261H9.48724C9.0273 18.0259 8.65411 17.6527 8.6539 17.1928C8.6539 16.7327 9.02717 16.3597 9.48724 16.3595H17.1956ZM12.5732 2.47195C13.8817 1.28992 15.902 1.32977 17.163 2.59076L17.281 2.71527C18.4249 3.98149 18.4257 5.91435 17.2818 7.18061L17.163 7.3043L9.32366 15.1445C9.12419 15.3439 8.89959 15.5169 8.65716 15.6596L8.40976 15.7923L4.13079 17.8471C2.75554 18.5075 1.31776 17.1336 1.84889 15.7565L1.90667 15.623L3.96152 11.344C4.12431 11.0051 4.34347 10.696 4.60931 10.4301L12.4495 2.59076L12.5732 2.47195ZM5.78769 11.6093C5.65491 11.7421 5.54596 11.8966 5.46461 12.0659L3.40976 16.344L7.68792 14.2892C7.85719 14.2078 8.01164 14.0989 8.14446 13.9661L13.4643 8.64545L11.1075 6.28868L5.78769 11.6093ZM15.9846 3.76915C15.3338 3.11835 14.2788 3.11851 13.6279 3.76915L12.2867 5.11029L14.6435 7.46706L15.9846 6.12592C16.6353 5.47503 16.6354 4.41994 15.9846 3.76915Z" fill="#1456F0"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M17.1956 16.3595C17.6558 16.3595 18.0289 16.7326 18.0289 17.1928C18.0287 17.6529 17.6557 18.0261 17.1956 18.0261H9.48724C9.0273 18.0259 8.65411 17.6527 8.6539 17.1928C8.6539 16.7327 9.02717 16.3597 9.48724 16.3595H17.1956ZM12.5732 2.47195C13.8817 1.28992 15.902 1.32977 17.163 2.59076L17.281 2.71527C18.4249 3.98149 18.4257 5.91435 17.2818 7.18061L17.163 7.3043L9.32366 15.1445C9.12419 15.3439 8.89959 15.5169 8.65716 15.6596L8.40976 15.7923L4.13079 17.8471C2.75554 18.5075 1.31776 17.1336 1.84889 15.7565L1.90667 15.623L3.96152 11.344C4.12431 11.0051 4.34347 10.696 4.60931 10.4301L12.4495 2.59076L12.5732 2.47195ZM5.78769 11.6093C5.65491 11.7421 5.54596 11.8966 5.46461 12.0659L3.40976 16.344L7.68792 14.2892C7.85719 14.2078 8.01164 14.0989 8.14446 13.9661L13.4643 8.64545L11.1075 6.28868L5.78769 11.6093ZM15.9846 3.76915C15.3338 3.11835 14.2788 3.11851 13.6279 3.76915L12.2867 5.11029L14.6435 7.46706L15.9846 6.12592C16.6353 5.47503 16.6354 4.41994 15.9846 3.76915Z" fill="#2B2F36"/>
3
+ </svg>