@nocobase/portal-template-default 1.0.0 → 1.0.2

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 (194) hide show
  1. package/README.MD +90 -1
  2. package/components.json +3 -1
  3. package/package.json +21 -5
  4. package/registry/nocobase-acl/README.md +19 -0
  5. package/registry/nocobase-acl/components/acl-boundary.tsx +106 -0
  6. package/registry/nocobase-acl/components/index.ts +3 -0
  7. package/registry/nocobase-acl/components/role-options.ts +36 -0
  8. package/registry/nocobase-acl/components/role-switcher.tsx +135 -0
  9. package/registry/nocobase-acl/demo/boundary-api.tsx +186 -0
  10. package/registry/nocobase-acl/demo/components.tsx +231 -0
  11. package/registry/nocobase-acl/demo/index.tsx +505 -0
  12. package/registry/nocobase-acl/demo/lazy-route.tsx +10 -0
  13. package/registry/nocobase-acl/demo/prompt-generator.tsx +134 -0
  14. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  15. package/registry/nocobase-acl/demo/scenario-section.tsx +37 -0
  16. package/registry/nocobase-acl/extension.tsx +71 -0
  17. package/registry/nocobase-acl/index.ts +1 -0
  18. package/registry/nocobase-acl/tests/acl-regression.mjs +314 -0
  19. package/registry/nocobase-ai/README.md +70 -0
  20. package/registry/nocobase-ai/adapters/react-hook-form.ts +20 -0
  21. package/registry/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  22. package/registry/nocobase-ai/components/ai-root-provider.tsx +32 -0
  23. package/registry/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  24. package/registry/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  25. package/registry/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  26. package/registry/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  27. package/registry/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  28. package/registry/nocobase-ai/components/chat/chat-header.tsx +88 -0
  29. package/registry/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  30. package/registry/nocobase-ai/components/chat/chat-message.tsx +252 -0
  31. package/registry/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  32. package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
  33. package/registry/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  34. package/registry/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  35. package/registry/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  36. package/registry/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  37. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  38. package/registry/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  39. package/registry/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  40. package/registry/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  41. package/registry/nocobase-ai/components/index.ts +59 -0
  42. package/registry/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  43. package/registry/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  44. package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  45. package/registry/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  46. package/registry/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  47. package/registry/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  48. package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  49. package/registry/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  50. package/registry/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  51. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  52. package/registry/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  53. package/registry/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  54. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  55. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  56. package/registry/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  57. package/registry/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  58. package/registry/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  59. package/registry/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  60. package/registry/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  61. package/registry/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  62. package/registry/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  63. package/registry/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  64. package/registry/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  65. package/registry/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  66. package/registry/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  67. package/registry/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  68. package/registry/nocobase-ai/demo/configuration-gate.tsx +49 -0
  69. package/registry/nocobase-ai/demo/container-showcase.tsx +210 -0
  70. package/registry/nocobase-ai/demo/floating.tsx +134 -0
  71. package/registry/nocobase-ai/demo/index.tsx +346 -0
  72. package/registry/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  73. package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  74. package/registry/nocobase-ai/demo/page-context.tsx +884 -0
  75. package/registry/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  76. package/registry/nocobase-ai/demo/prompt-card.tsx +41 -0
  77. package/registry/nocobase-ai/demo/prompt-generator.tsx +452 -0
  78. package/registry/nocobase-ai/demo/shortcut.tsx +1207 -0
  79. package/registry/nocobase-ai/demo/tool-cards.tsx +530 -0
  80. package/registry/nocobase-ai/extension.tsx +158 -0
  81. package/registry/nocobase-ai/global-ai-chat.tsx +213 -0
  82. package/registry/nocobase-ai/index.ts +3 -0
  83. package/registry/nocobase-ai/providers/ai-provider.tsx +323 -0
  84. package/registry/nocobase-ai/providers/avatars.ts +817 -0
  85. package/registry/nocobase-ai/providers/chat-context.tsx +109 -0
  86. package/registry/nocobase-ai/providers/chat-controller.ts +84 -0
  87. package/registry/nocobase-ai/providers/chat-message-utils.ts +198 -0
  88. package/registry/nocobase-ai/providers/chat-provider.tsx +871 -0
  89. package/registry/nocobase-ai/providers/chat-reducer.ts +147 -0
  90. package/registry/nocobase-ai/providers/chat-task-utils.ts +105 -0
  91. package/registry/nocobase-ai/providers/chat-transport.ts +184 -0
  92. package/registry/nocobase-ai/providers/form-registry.tsx +271 -0
  93. package/registry/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  94. package/registry/nocobase-ai/providers/index.ts +61 -0
  95. package/registry/nocobase-ai/providers/model.ts +32 -0
  96. package/registry/nocobase-ai/providers/page-context.tsx +136 -0
  97. package/registry/nocobase-ai/providers/stream-coalescer.ts +60 -0
  98. package/registry/nocobase-ai/providers/stream-event-utils.ts +82 -0
  99. package/registry/nocobase-ai/providers/stream-parser.ts +61 -0
  100. package/registry/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  101. package/registry/nocobase-ai/providers/types.ts +183 -0
  102. package/registry/nocobase-ai/providers/ui-message-stream.ts +463 -0
  103. package/registry/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  104. package/registry/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  105. package/registry/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  106. package/registry/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  107. package/registry/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  108. package/registry/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  109. package/registry/nocobase-ai/providers/use-conversation-history.ts +178 -0
  110. package/registry/nocobase-ai/services/index.ts +7 -0
  111. package/registry/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  112. package/registry/nocobase-ai/services/types.ts +82 -0
  113. package/registry/nocobase-ai/tests/business-report-regression.mjs +70 -0
  114. package/registry/nocobase-ai/tests/form-filler-regression.mjs +129 -0
  115. package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +128 -0
  116. package/registry/nocobase-ai/tests/page-context-regression.mjs +48 -0
  117. package/registry/nocobase-ai/tests/stream-regression.mjs +207 -0
  118. package/registry/nocobase-i18n/README.md +53 -0
  119. package/registry/nocobase-i18n/components/index.ts +1 -0
  120. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  121. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  122. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  123. package/registry/nocobase-i18n/extension.tsx +46 -0
  124. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  125. package/registry/nocobase-i18n/index.ts +6 -0
  126. package/registry/nocobase-i18n/locale-store.ts +134 -0
  127. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  128. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  129. package/registry/nocobase-i18n/provider.tsx +32 -0
  130. package/registry/nocobase-i18n/runtime.ts +118 -0
  131. package/registry/nocobase-i18n/server-resources.ts +184 -0
  132. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  133. package/registry.config.json +163 -0
  134. package/scripts/registry.mjs +164 -0
  135. package/src/App.css +95 -0
  136. package/src/App.tsx +87 -69
  137. package/src/app/extension.ts +11 -0
  138. package/src/app/extensions.tsx +49 -0
  139. package/src/components/access-control/access-denied.tsx +30 -0
  140. package/src/components/access-control/acl-bootstrap.tsx +9 -0
  141. package/src/components/access-control/acl-gate.tsx +36 -0
  142. package/src/components/access-control/navigate-to-accessible-resource.tsx +17 -0
  143. package/src/components/access-control/resource-access-guard.tsx +21 -0
  144. package/src/components/access-control/use-acl-runtime.ts +28 -0
  145. package/src/components/app-shell/breadcrumb.tsx +39 -3
  146. package/src/components/app-shell/document-title-handler.tsx +11 -20
  147. package/src/components/app-shell/header.tsx +22 -4
  148. package/src/components/app-shell/loading-overlay.tsx +2 -4
  149. package/src/components/app-shell/loading-state.tsx +15 -0
  150. package/src/components/app-shell/sidebar.tsx +103 -45
  151. package/src/components/data-table/data-table-pagination.tsx +27 -7
  152. package/src/components/data-table/data-table-styles.ts +57 -0
  153. package/src/components/data-table/data-table.tsx +14 -59
  154. package/src/components/resources/resource-label.ts +101 -0
  155. package/src/components/resources/views/create-view.tsx +17 -14
  156. package/src/components/resources/views/edit-view.tsx +17 -15
  157. package/src/components/resources/views/list-view.tsx +20 -10
  158. package/src/components/resources/views/show-view.tsx +20 -18
  159. package/src/index.tsx +2 -3
  160. package/src/lib/i18n.ts +161 -0
  161. package/src/lib/nocobase/acl/action.ts +147 -0
  162. package/src/lib/nocobase/acl/data-source.ts +24 -0
  163. package/src/lib/nocobase/acl/index.ts +6 -0
  164. package/src/lib/nocobase/acl/menu.ts +41 -0
  165. package/src/lib/nocobase/acl/record-permissions.ts +111 -0
  166. package/src/lib/nocobase/acl/store.ts +182 -0
  167. package/src/lib/nocobase/acl/types.ts +96 -0
  168. package/src/lib/nocobase/client.ts +279 -0
  169. package/src/lib/nocobase/error.ts +32 -0
  170. package/src/locales/en-US.ts +82 -0
  171. package/src/locales/index.ts +13 -0
  172. package/src/locales/zh-CN.ts +79 -0
  173. package/src/pages/users/create.tsx +98 -0
  174. package/src/pages/users/edit.tsx +92 -0
  175. package/src/pages/users/form-context.ts +57 -0
  176. package/src/pages/users/form-fields.tsx +211 -0
  177. package/src/pages/users/list.tsx +205 -0
  178. package/src/pages/users/show.tsx +187 -0
  179. package/src/pages/users/types.ts +20 -0
  180. package/src/providers/access-control.ts +34 -0
  181. package/src/providers/auth.ts +74 -60
  182. package/src/providers/constants.ts +3 -37
  183. package/src/providers/data.ts +242 -85
  184. package/vite.config.ts +17 -0
  185. package/src/pages/blog-posts/create.tsx +0 -177
  186. package/src/pages/blog-posts/edit.tsx +0 -197
  187. package/src/pages/blog-posts/list.tsx +0 -131
  188. package/src/pages/blog-posts/show.tsx +0 -81
  189. package/src/pages/categories/create.tsx +0 -73
  190. package/src/pages/categories/edit.tsx +0 -73
  191. package/src/pages/categories/index.ts +0 -4
  192. package/src/pages/categories/list.tsx +0 -86
  193. package/src/pages/categories/show.tsx +0 -37
  194. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -1,8 +1,7 @@
1
1
  "use client";
2
2
 
3
- import type { HttpError, BaseRecord } from "@refinedev/core";
3
+ import { useTranslate, type HttpError, type BaseRecord } from "@refinedev/core";
4
4
  import type { UseTableReturnType } from "@refinedev/react-table";
5
- import type { Column } from "@tanstack/react-table";
6
5
  import { flexRender } from "@tanstack/react-table";
7
6
  import { Loader2 } from "lucide-react";
8
7
  import { useEffect, useRef, useState } from "react";
@@ -16,6 +15,7 @@ import {
16
15
  TableRow,
17
16
  } from "@/components/ui/table";
18
17
  import { DataTablePagination } from "@/components/data-table/data-table-pagination";
18
+ import { getCommonStyles } from "@/components/data-table/data-table-styles";
19
19
  import { cn } from "@/lib/utils";
20
20
 
21
21
  type DataTableProps<TData extends BaseRecord> = {
@@ -25,6 +25,7 @@ type DataTableProps<TData extends BaseRecord> = {
25
25
  export function DataTable<TData extends BaseRecord>({
26
26
  table,
27
27
  }: DataTableProps<TData>) {
28
+ const translate = useTranslate();
28
29
  const {
29
30
  reactTable: { getHeaderGroups, getRowModel, getAllColumns },
30
31
  refineCore: {
@@ -198,6 +199,11 @@ export function DataTable<TData extends BaseRecord>({
198
199
  <DataTableNoData
199
200
  isOverflowing={isOverflowing}
200
201
  columnsLength={columns.length}
202
+ title={translate("table.empty.title", "No data to display")}
203
+ description={translate(
204
+ "table.empty.description",
205
+ "This table is empty for the time being."
206
+ )}
201
207
  />
202
208
  )}
203
209
  </TableBody>
@@ -220,9 +226,13 @@ export function DataTable<TData extends BaseRecord>({
220
226
  function DataTableNoData({
221
227
  isOverflowing,
222
228
  columnsLength,
229
+ title,
230
+ description,
223
231
  }: {
224
232
  isOverflowing: { horizontal: boolean; vertical: boolean };
225
233
  columnsLength: number;
234
+ title: string;
235
+ description: string;
226
236
  }) {
227
237
  return (
228
238
  <TableRow className="hover:bg-transparent">
@@ -252,10 +262,10 @@ function DataTableNoData({
252
262
  }}
253
263
  >
254
264
  <div className={cn("text-lg", "font-semibold", "text-foreground")}>
255
- No data to display
265
+ {title}
256
266
  </div>
257
267
  <div className={cn("text-sm", "text-muted-foreground")}>
258
- This table is empty for the time being.
268
+ {description}
259
269
  </div>
260
270
  </div>
261
271
  </TableCell>
@@ -263,59 +273,4 @@ function DataTableNoData({
263
273
  );
264
274
  }
265
275
 
266
- export function getCommonStyles<TData>({
267
- column,
268
- isOverflowing,
269
- }: {
270
- column: Column<TData>;
271
- isOverflowing: {
272
- horizontal: boolean;
273
- vertical: boolean;
274
- };
275
- }): React.CSSProperties {
276
- const isPinned = column.getIsPinned();
277
- const isLastLeftPinnedColumn =
278
- isPinned === "left" && column.getIsLastColumn("left");
279
- const isFirstRightPinnedColumn =
280
- isPinned === "right" && column.getIsFirstColumn("right");
281
-
282
- return {
283
- boxShadow:
284
- isOverflowing.horizontal && isLastLeftPinnedColumn
285
- ? "-4px 0 4px -4px var(--border) inset"
286
- : isOverflowing.horizontal && isFirstRightPinnedColumn
287
- ? "4px 0 4px -4px var(--border) inset"
288
- : undefined,
289
- left:
290
- isOverflowing.horizontal && isPinned === "left"
291
- ? `${column.getStart("left")}px`
292
- : undefined,
293
- right:
294
- isOverflowing.horizontal && isPinned === "right"
295
- ? `${column.getAfter("right")}px`
296
- : undefined,
297
- opacity: 1,
298
- position: isOverflowing.horizontal && isPinned ? "sticky" : "relative",
299
- background: isOverflowing.horizontal && isPinned ? "var(--background)" : "",
300
- borderTopRightRadius:
301
- isOverflowing.horizontal && isPinned === "right"
302
- ? "var(--radius)"
303
- : undefined,
304
- borderBottomRightRadius:
305
- isOverflowing.horizontal && isPinned === "right"
306
- ? "var(--radius)"
307
- : undefined,
308
- borderTopLeftRadius:
309
- isOverflowing.horizontal && isPinned === "left"
310
- ? "var(--radius)"
311
- : undefined,
312
- borderBottomLeftRadius:
313
- isOverflowing.horizontal && isPinned === "left"
314
- ? "var(--radius)"
315
- : undefined,
316
- width: column.getSize(),
317
- zIndex: isOverflowing.horizontal && isPinned ? 1 : 0,
318
- };
319
- }
320
-
321
276
  DataTable.displayName = "DataTable";
@@ -0,0 +1,101 @@
1
+ import {
2
+ type IResourceItem,
3
+ useTranslate,
4
+ useUserFriendlyName,
5
+ } from "@refinedev/core";
6
+
7
+ import {
8
+ parseTranslationExpression,
9
+ type TranslationOptions,
10
+ } from "@/lib/i18n";
11
+
12
+ export type ResourceLabelForm = "plural" | "singular";
13
+
14
+ type ResourceLabelMeta = {
15
+ i18nKey?: string;
16
+ i18nOptions?: TranslationOptions;
17
+ i18nSingularKey?: string;
18
+ label?: unknown;
19
+ singularLabel?: unknown;
20
+ };
21
+
22
+ type Translate = ReturnType<typeof useTranslate>;
23
+ type UserFriendlyName = ReturnType<typeof useUserFriendlyName>;
24
+
25
+ function translateWithFallback(
26
+ translate: Translate,
27
+ key: string,
28
+ fallback: string,
29
+ options?: TranslationOptions
30
+ ) {
31
+ const translated = options
32
+ ? translate(key, options, fallback)
33
+ : translate(key, fallback);
34
+
35
+ return !translated || translated === key ? fallback : translated;
36
+ }
37
+
38
+ function resolveExplicitLabel(value: unknown, translate: Translate) {
39
+ if (typeof value !== "string") {
40
+ return value == null ? undefined : String(value);
41
+ }
42
+
43
+ const expression = parseTranslationExpression(value);
44
+ if (!expression) return value;
45
+
46
+ const fallback = expression.options?.defaultValue ?? expression.key;
47
+ return translateWithFallback(
48
+ translate,
49
+ expression.key,
50
+ fallback,
51
+ expression.options
52
+ );
53
+ }
54
+
55
+ export function getResourceLabel(
56
+ resource: IResourceItem | undefined,
57
+ form: ResourceLabelForm,
58
+ translate: Translate,
59
+ getUserFriendlyName: UserFriendlyName,
60
+ fallbackIdentifier?: string
61
+ ) {
62
+ const meta = resource?.meta as ResourceLabelMeta | undefined;
63
+ const topLevelLabel = (
64
+ resource as (IResourceItem & { label?: unknown }) | undefined
65
+ )?.label;
66
+ const explicitLabel =
67
+ form === "singular"
68
+ ? meta?.singularLabel ?? meta?.label ?? topLevelLabel
69
+ : meta?.label ?? topLevelLabel;
70
+ const fallback =
71
+ resolveExplicitLabel(explicitLabel, translate) ??
72
+ getUserFriendlyName(
73
+ fallbackIdentifier ?? resource?.identifier ?? resource?.name ?? "",
74
+ form
75
+ );
76
+ const i18nKey =
77
+ form === "singular"
78
+ ? meta?.i18nSingularKey ?? meta?.i18nKey
79
+ : meta?.i18nKey;
80
+
81
+ return i18nKey
82
+ ? translateWithFallback(translate, i18nKey, fallback, meta?.i18nOptions)
83
+ : fallback;
84
+ }
85
+
86
+ export function useResourceLabel(
87
+ resource: IResourceItem | undefined,
88
+ form: ResourceLabelForm,
89
+ fallbackIdentifier?: string
90
+ ) {
91
+ const translate = useTranslate();
92
+ const getUserFriendlyName = useUserFriendlyName();
93
+
94
+ return getResourceLabel(
95
+ resource,
96
+ form,
97
+ translate,
98
+ getUserFriendlyName,
99
+ fallbackIdentifier
100
+ );
101
+ }
@@ -1,15 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  import { cn } from "@/lib/utils";
4
- import {
5
- useBack,
6
- useResourceParams,
7
- useUserFriendlyName,
8
- } from "@refinedev/core";
4
+ import { useBack, useResourceParams, useTranslate } from "@refinedev/core";
9
5
  import type { PropsWithChildren } from "react";
10
6
  import { Breadcrumb } from "@/components/app-shell/breadcrumb";
11
7
  import { Button } from "@/components/ui/button";
12
8
  import { ArrowLeftIcon } from "lucide-react";
9
+ import { useResourceLabel } from "@/components/resources/resource-label";
13
10
 
14
11
  type CreateViewProps = PropsWithChildren<{
15
12
  className?: string;
@@ -38,18 +35,25 @@ export const CreateViewHeader = ({
38
35
  headerClassName,
39
36
  }: CreateHeaderProps) => {
40
37
  const back = useBack();
41
-
42
- const getUserFriendlyName = useUserFriendlyName();
38
+ const translate = useTranslate();
43
39
 
44
40
  const { resource, identifier } = useResourceParams({
45
41
  resource: resourceFromProps,
46
42
  });
47
43
 
48
- const resourceTitle = getUserFriendlyName(
49
- resource?.meta?.label ?? identifier ?? resource?.name,
50
- "singular"
44
+ const resourceTitle = useResourceLabel(resource, "singular", identifier);
45
+ const title =
46
+ titleFromProps ??
47
+ translate(
48
+ "views.create.title",
49
+ { resource: resourceTitle },
50
+ `Create ${resourceTitle}`
51
+ );
52
+ const description = translate(
53
+ "views.create.description",
54
+ { resource: resourceTitle.toLocaleLowerCase() },
55
+ `Add a new ${resourceTitle.toLocaleLowerCase()} to your NocoBase workspace.`
51
56
  );
52
- const title = titleFromProps ?? `Create ${resourceTitle}`;
53
57
 
54
58
  return (
55
59
  <div className={cn("flex flex-col", "gap-3", wrapperClassName)}>
@@ -62,6 +66,7 @@ export const CreateViewHeader = ({
62
66
  size="icon"
63
67
  className="mt-0.5 rounded-lg"
64
68
  onClick={back}
69
+ aria-label={translate("buttons.cancel", "Cancel")}
65
70
  >
66
71
  <ArrowLeftIcon className="h-4 w-4" />
67
72
  </Button>
@@ -69,9 +74,7 @@ export const CreateViewHeader = ({
69
74
  <h2 className="text-3xl font-semibold tracking-[-0.035em]">
70
75
  {title}
71
76
  </h2>
72
- <p className="mt-2 text-sm text-muted-foreground">
73
- Add a new {resourceTitle.toLowerCase()} to your NocoBase workspace.
74
- </p>
77
+ <p className="mt-2 text-sm text-muted-foreground">{description}</p>
75
78
  </div>
76
79
  </div>
77
80
  </div>
@@ -1,16 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import { cn } from "@/lib/utils";
4
- import {
5
- useBack,
6
- useResourceParams,
7
- useUserFriendlyName,
8
- } from "@refinedev/core";
4
+ import { useBack, useResourceParams, useTranslate } from "@refinedev/core";
9
5
  import type { PropsWithChildren } from "react";
10
6
  import { Breadcrumb } from "@/components/app-shell/breadcrumb";
11
7
  import { Button } from "@/components/ui/button";
12
8
  import { RefreshButton } from "@/components/resources/buttons/refresh";
13
9
  import { ArrowLeftIcon } from "lucide-react";
10
+ import { useResourceLabel } from "@/components/resources/resource-label";
14
11
 
15
12
  type EditViewProps = PropsWithChildren<{
16
13
  className?: string;
@@ -41,8 +38,7 @@ export const EditViewHeader = ({
41
38
  headerClassName,
42
39
  }: EditViewHeaderProps) => {
43
40
  const back = useBack();
44
-
45
- const getUserFriendlyName = useUserFriendlyName();
41
+ const translate = useTranslate();
46
42
 
47
43
  const { resource, identifier } = useResourceParams({
48
44
  resource: resourceFromProps,
@@ -51,11 +47,19 @@ export const EditViewHeader = ({
51
47
 
52
48
  const resourceName = resource?.name ?? identifier;
53
49
 
54
- const resourceTitle = getUserFriendlyName(
55
- resource?.meta?.label ?? identifier ?? resource?.name,
56
- "singular"
50
+ const resourceTitle = useResourceLabel(resource, "singular", identifier);
51
+ const title =
52
+ titleFromProps ??
53
+ translate(
54
+ "views.edit.title",
55
+ { resource: resourceTitle },
56
+ `Edit ${resourceTitle}`
57
+ );
58
+ const description = translate(
59
+ "views.edit.description",
60
+ { resource: resourceTitle.toLocaleLowerCase() },
61
+ `Update this ${resourceTitle.toLocaleLowerCase()} while NocoBase keeps the data consistent.`
57
62
  );
58
- const title = titleFromProps ?? `Edit ${resourceTitle}`;
59
63
 
60
64
  return (
61
65
  <div className={cn("flex flex-col", "gap-3", wrapperClassName)}>
@@ -74,6 +78,7 @@ export const EditViewHeader = ({
74
78
  size="icon"
75
79
  className="mt-0.5 rounded-lg"
76
80
  onClick={back}
81
+ aria-label={translate("buttons.cancel", "Cancel")}
77
82
  >
78
83
  <ArrowLeftIcon className="h-4 w-4" />
79
84
  </Button>
@@ -81,10 +86,7 @@ export const EditViewHeader = ({
81
86
  <h2 className="text-3xl font-semibold tracking-[-0.035em]">
82
87
  {title}
83
88
  </h2>
84
- <p className="mt-2 text-sm text-muted-foreground">
85
- Update this {resourceTitle.toLowerCase()} while NocoBase keeps
86
- the data consistent.
87
- </p>
89
+ <p className="mt-2 text-sm text-muted-foreground">{description}</p>
88
90
  </div>
89
91
  </div>
90
92
 
@@ -2,9 +2,11 @@
2
2
 
3
3
  import type { PropsWithChildren } from "react";
4
4
 
5
- import { useResourceParams, useUserFriendlyName } from "@refinedev/core";
5
+ import { useResourceParams, useTranslate } from "@refinedev/core";
6
6
  import { Breadcrumb } from "@/components/app-shell/breadcrumb";
7
7
  import { CreateButton } from "@/components/resources/buttons/create";
8
+ import { useResourceLabel } from "@/components/resources/resource-label";
9
+ import { resolveTranslatableText, type TranslationOptions } from "@/lib/i18n";
8
10
  import { cn } from "@/lib/utils";
9
11
 
10
12
  type ListViewProps = PropsWithChildren<{
@@ -35,22 +37,30 @@ export const ListViewHeader = ({
35
37
  wrapperClassName,
36
38
  headerClassName,
37
39
  }: ListHeaderProps) => {
38
- const getUserFriendlyName = useUserFriendlyName();
39
-
40
40
  const { resource, identifier } = useResourceParams({
41
41
  resource: resourceFromProps,
42
42
  });
43
+ const translate = useTranslate();
43
44
  const resourceName = identifier ?? resource?.name;
44
45
 
45
46
  const isCreateButtonVisible = canCreate ?? !!resource?.create;
46
47
 
47
- const title =
48
- titleFromProps ??
49
- getUserFriendlyName(
50
- resource?.meta?.label ?? identifier ?? resource?.name,
51
- "plural"
52
- );
53
- const description = resource?.meta?.description as string | undefined;
48
+ const resourceTitle = useResourceLabel(resource, "plural", identifier);
49
+ const title = titleFromProps ?? resourceTitle;
50
+ const meta = resource?.meta as
51
+ | {
52
+ description?: string;
53
+ descriptionI18nKey?: string;
54
+ i18nOptions?: TranslationOptions;
55
+ }
56
+ | undefined;
57
+ const description = meta?.descriptionI18nKey
58
+ ? translate(
59
+ meta.descriptionI18nKey,
60
+ meta.i18nOptions,
61
+ meta.description ?? meta.descriptionI18nKey
62
+ )
63
+ : resolveTranslatableText(meta?.description);
54
64
 
55
65
  return (
56
66
  <div className={cn("flex flex-col", "gap-3", wrapperClassName)}>
@@ -3,25 +3,29 @@
3
3
  import type { PropsWithChildren } from "react";
4
4
 
5
5
  import { ArrowLeftIcon } from "lucide-react";
6
- import {
7
- useBack,
8
- useResourceParams,
9
- useUserFriendlyName,
10
- } from "@refinedev/core";
6
+ import { useBack, useResourceParams, useTranslate } from "@refinedev/core";
11
7
  import { Breadcrumb } from "@/components/app-shell/breadcrumb";
12
8
  import { Button } from "@/components/ui/button";
13
9
  import { RefreshButton } from "@/components/resources/buttons/refresh";
14
10
  import { cn } from "@/lib/utils";
15
11
  import { EditButton } from "../buttons/edit";
12
+ import { useResourceLabel } from "@/components/resources/resource-label";
16
13
 
17
14
  type ShowViewProps = PropsWithChildren<{
18
15
  className?: string;
16
+ resource?: string;
17
+ title?: string;
19
18
  }>;
20
19
 
21
- export function ShowView({ children, className }: ShowViewProps) {
20
+ export function ShowView({
21
+ children,
22
+ className,
23
+ resource,
24
+ title,
25
+ }: ShowViewProps) {
22
26
  return (
23
27
  <div className={cn("flex flex-col", "gap-6", className)}>
24
- <ShowViewHeader />
28
+ <ShowViewHeader resource={resource} title={title} />
25
29
  {children}
26
30
  </div>
27
31
  );
@@ -41,8 +45,7 @@ export const ShowViewHeader = ({
41
45
  headerClassName,
42
46
  }: ShowViewHeaderProps) => {
43
47
  const back = useBack();
44
-
45
- const getUserFriendlyName = useUserFriendlyName();
48
+ const translate = useTranslate();
46
49
 
47
50
  const { resource, identifier } = useResourceParams({
48
51
  resource: resourceFromProps,
@@ -51,12 +54,12 @@ export const ShowViewHeader = ({
51
54
 
52
55
  const resourceName = resource?.name ?? identifier;
53
56
 
54
- const title =
55
- titleFromProps ??
56
- getUserFriendlyName(
57
- resource?.meta?.label ?? identifier ?? resource?.name,
58
- "singular"
59
- );
57
+ const resourceTitle = useResourceLabel(resource, "singular", identifier);
58
+ const title = titleFromProps ?? resourceTitle;
59
+ const description = translate(
60
+ "views.show.description",
61
+ "Review the record and its NocoBase-managed data."
62
+ );
60
63
 
61
64
  return (
62
65
  <div className={cn("flex flex-col", "gap-3", wrapperClassName)}>
@@ -75,6 +78,7 @@ export const ShowViewHeader = ({
75
78
  size="icon"
76
79
  className="mt-0.5 rounded-lg"
77
80
  onClick={back}
81
+ aria-label={translate("buttons.cancel", "Cancel")}
78
82
  >
79
83
  <ArrowLeftIcon className="h-4 w-4" />
80
84
  </Button>
@@ -82,9 +86,7 @@ export const ShowViewHeader = ({
82
86
  <h2 className="text-3xl font-semibold tracking-[-0.035em]">
83
87
  {title}
84
88
  </h2>
85
- <p className="mt-2 text-sm text-muted-foreground">
86
- Review the record and its NocoBase-managed data.
87
- </p>
89
+ <p className="mt-2 text-sm text-muted-foreground">{description}</p>
88
90
  </div>
89
91
  </div>
90
92
 
package/src/index.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { createRoot } from "react-dom/client";
3
3
 
4
+ import "./locales";
4
5
  import App from "./App";
5
6
 
6
7
  const container = document.getElementById("root") as HTMLElement;
@@ -12,6 +13,4 @@ const root = createRoot(container);
12
13
  // </React.StrictMode>
13
14
  // );
14
15
 
15
- root.render(
16
- <App />
17
- );
16
+ root.render(<App />);
@@ -0,0 +1,161 @@
1
+ export type TranslationOptions = Record<string, unknown> & {
2
+ defaultValue?: string;
3
+ ns?: string | string[];
4
+ };
5
+
6
+ export type TranslationResolver = (
7
+ key: string,
8
+ options?: TranslationOptions,
9
+ defaultMessage?: string
10
+ ) => string;
11
+
12
+ export type TranslationExpression = {
13
+ key: string;
14
+ options?: TranslationOptions;
15
+ };
16
+
17
+ export type TranslationResource = Record<string, string | number | boolean>;
18
+
19
+ export type TranslationResourceBundle = Record<string, TranslationResource>;
20
+
21
+ type TranslationResourceListener = (
22
+ namespace: string,
23
+ resources: TranslationResourceBundle
24
+ ) => void;
25
+
26
+ let translationResolver: TranslationResolver | undefined;
27
+ const translationResources = new Map<string, TranslationResourceBundle>();
28
+ const translationResourceListeners = new Set<TranslationResourceListener>();
29
+
30
+ const quotedValuePattern = `(?:"(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*')`;
31
+ const translationExpressionPattern = new RegExp(
32
+ `^\\s*\\{\\{\\s*t\\(\\s*(${quotedValuePattern})\\s*(?:,\\s*(\\{[\\s\\S]*\\}))?\\s*\\)\\s*\\}\\}\\s*$`
33
+ );
34
+
35
+ function decodeQuotedValue(value: string) {
36
+ if (value.startsWith('"')) {
37
+ try {
38
+ return JSON.parse(value) as string;
39
+ } catch {
40
+ return value.slice(1, -1);
41
+ }
42
+ }
43
+
44
+ return value
45
+ .slice(1, -1)
46
+ .replace(/\\'/g, "'")
47
+ .replace(/\\"/g, '"')
48
+ .replace(/\\n/g, "\n")
49
+ .replace(/\\r/g, "\r")
50
+ .replace(/\\t/g, "\t")
51
+ .replace(/\\\\/g, "\\");
52
+ }
53
+
54
+ function parsePrimitive(value: string): unknown {
55
+ const normalized = value.trim();
56
+ if (normalized.startsWith('"') || normalized.startsWith("'")) {
57
+ return decodeQuotedValue(normalized);
58
+ }
59
+ if (normalized === "true") return true;
60
+ if (normalized === "false") return false;
61
+ if (normalized === "null") return null;
62
+ if (/^-?\d+(?:\.\d+)?$/.test(normalized)) return Number(normalized);
63
+ return undefined;
64
+ }
65
+
66
+ function parseOptions(source?: string): TranslationOptions | undefined {
67
+ if (!source) return undefined;
68
+
69
+ const options: TranslationOptions = {};
70
+ const propertyPattern = new RegExp(
71
+ `([A-Za-z_$][\\w$]*)\\s*:\\s*(${quotedValuePattern}|-?\\d+(?:\\.\\d+)?|true|false|null)`,
72
+ "g"
73
+ );
74
+
75
+ for (const match of source.matchAll(propertyPattern)) {
76
+ const value = parsePrimitive(match[2]);
77
+ if (typeof value !== "undefined") options[match[1]] = value;
78
+ }
79
+
80
+ const namespaceArray = source.match(/\bns\s*:\s*\[([^\]]*)\]/);
81
+ if (namespaceArray) {
82
+ const namespaces = [
83
+ ...namespaceArray[1].matchAll(new RegExp(quotedValuePattern, "g")),
84
+ ].map((match) => decodeQuotedValue(match[0]));
85
+ if (namespaces.length) options.ns = namespaces;
86
+ }
87
+
88
+ return Object.keys(options).length ? options : undefined;
89
+ }
90
+
91
+ export function parseTranslationExpression(
92
+ value: string
93
+ ): TranslationExpression | undefined {
94
+ const match = translationExpressionPattern.exec(value);
95
+ if (!match) return undefined;
96
+
97
+ return {
98
+ key: decodeQuotedValue(match[1]),
99
+ options: parseOptions(match[2]),
100
+ };
101
+ }
102
+
103
+ export function setTranslationResolver(resolver?: TranslationResolver) {
104
+ translationResolver = resolver;
105
+ return () => {
106
+ if (translationResolver === resolver) translationResolver = undefined;
107
+ };
108
+ }
109
+
110
+ export function registerTranslationResources(
111
+ namespace: string,
112
+ resources: TranslationResourceBundle
113
+ ) {
114
+ const current = translationResources.get(namespace) ?? {};
115
+ const merged = Object.fromEntries(
116
+ [...new Set([...Object.keys(current), ...Object.keys(resources)])].map(
117
+ (locale) => [
118
+ locale,
119
+ {
120
+ ...(current[locale] ?? {}),
121
+ ...(resources[locale] ?? {}),
122
+ },
123
+ ]
124
+ )
125
+ );
126
+
127
+ translationResources.set(namespace, merged);
128
+ translationResourceListeners.forEach((listener) =>
129
+ listener(namespace, resources)
130
+ );
131
+ }
132
+
133
+ export function getTranslationResources() {
134
+ return [...translationResources.entries()];
135
+ }
136
+
137
+ export function subscribeTranslationResources(
138
+ listener: TranslationResourceListener
139
+ ) {
140
+ translationResourceListeners.add(listener);
141
+ return () => translationResourceListeners.delete(listener);
142
+ }
143
+
144
+ export function resolveTranslatableText(
145
+ value: unknown,
146
+ options?: TranslationOptions
147
+ ): string {
148
+ if (typeof value !== "string") return value == null ? "" : String(value);
149
+
150
+ const expression = parseTranslationExpression(value);
151
+ if (!expression) {
152
+ return translationResolver && options
153
+ ? translationResolver(value, options, value)
154
+ : value;
155
+ }
156
+
157
+ const defaultMessage = expression.options?.defaultValue ?? expression.key;
158
+ return translationResolver
159
+ ? translationResolver(expression.key, expression.options, defaultMessage)
160
+ : defaultMessage;
161
+ }