@nocobase/portal-template-default 1.0.1 → 1.0.3

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 (186) hide show
  1. package/README.MD +22 -3
  2. package/package.json +4 -2
  3. package/registry/nocobase-acl/components/role-options.ts +7 -0
  4. package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
  5. package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
  6. package/registry/nocobase-acl/demo/components.tsx +32 -11
  7. package/registry/nocobase-acl/demo/index.tsx +45 -14
  8. package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
  9. package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
  10. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
  11. package/registry/nocobase-acl/extension.tsx +1 -1
  12. package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
  13. package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
  14. package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
  15. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
  16. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
  17. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
  18. package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
  19. package/registry/nocobase-ai/extension.tsx +2 -7
  20. package/registry/nocobase-i18n/README.md +53 -0
  21. package/registry/nocobase-i18n/components/index.ts +1 -0
  22. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  23. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  24. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  25. package/registry/nocobase-i18n/extension.tsx +46 -0
  26. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  27. package/registry/nocobase-i18n/index.ts +6 -0
  28. package/registry/nocobase-i18n/locale-store.ts +134 -0
  29. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  30. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  31. package/registry/nocobase-i18n/provider.tsx +32 -0
  32. package/registry/nocobase-i18n/runtime.ts +118 -0
  33. package/registry/nocobase-i18n/server-resources.ts +184 -0
  34. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  35. package/registry.config.json +34 -14
  36. package/scripts/registry.mjs +5 -0
  37. package/src/App.tsx +27 -86
  38. package/src/app/extension.ts +3 -1
  39. package/src/app/extensions.tsx +21 -0
  40. package/src/components/access-control/acl-gate.tsx +4 -10
  41. package/src/components/app-shell/breadcrumb.tsx +39 -3
  42. package/src/components/app-shell/document-title-handler.tsx +11 -20
  43. package/src/components/app-shell/header.tsx +18 -3
  44. package/src/components/app-shell/loading-overlay.tsx +2 -4
  45. package/src/components/app-shell/loading-state.tsx +15 -0
  46. package/src/components/app-shell/sidebar.tsx +75 -47
  47. package/src/components/data-table/data-table-pagination.tsx +27 -7
  48. package/src/components/data-table/data-table-styles.ts +57 -0
  49. package/src/components/data-table/data-table.tsx +14 -59
  50. package/src/components/resources/resource-label.ts +101 -0
  51. package/src/components/resources/views/create-view.tsx +17 -14
  52. package/src/components/resources/views/edit-view.tsx +17 -15
  53. package/src/components/resources/views/list-view.tsx +20 -10
  54. package/src/components/resources/views/show-view.tsx +20 -18
  55. package/src/extensions/nocobase-acl/README.md +19 -0
  56. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +106 -0
  57. package/src/extensions/nocobase-acl/components/index.ts +3 -0
  58. package/src/extensions/nocobase-acl/components/role-options.ts +36 -0
  59. package/src/extensions/nocobase-acl/components/role-switcher.tsx +135 -0
  60. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +186 -0
  61. package/src/extensions/nocobase-acl/demo/components.tsx +231 -0
  62. package/src/extensions/nocobase-acl/demo/index.tsx +505 -0
  63. package/src/extensions/nocobase-acl/demo/lazy-route.tsx +10 -0
  64. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +134 -0
  65. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  66. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +37 -0
  67. package/src/extensions/nocobase-acl/extension.tsx +71 -0
  68. package/src/extensions/nocobase-acl/index.ts +1 -0
  69. package/src/extensions/nocobase-ai/README.md +70 -0
  70. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +20 -0
  71. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  72. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +32 -0
  73. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  74. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  75. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  76. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  77. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  78. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +88 -0
  79. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  80. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +252 -0
  81. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  82. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +118 -0
  83. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  84. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  85. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  86. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  87. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  88. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  89. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  90. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  91. package/src/extensions/nocobase-ai/components/index.ts +59 -0
  92. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  93. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  94. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  95. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  96. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  97. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  98. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  99. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  100. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  101. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  102. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  103. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  104. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  105. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  106. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  107. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  108. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  109. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  110. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  111. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  112. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  113. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  114. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  115. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  116. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  117. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  118. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +49 -0
  119. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +210 -0
  120. package/src/extensions/nocobase-ai/demo/floating.tsx +134 -0
  121. package/src/extensions/nocobase-ai/demo/index.tsx +346 -0
  122. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  123. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  124. package/src/extensions/nocobase-ai/demo/page-context.tsx +884 -0
  125. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  126. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +41 -0
  127. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +452 -0
  128. package/src/extensions/nocobase-ai/demo/shortcut.tsx +1207 -0
  129. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +530 -0
  130. package/src/extensions/nocobase-ai/extension.tsx +158 -0
  131. package/src/extensions/nocobase-ai/global-ai-chat.tsx +213 -0
  132. package/src/extensions/nocobase-ai/index.ts +3 -0
  133. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +323 -0
  134. package/src/extensions/nocobase-ai/providers/avatars.ts +817 -0
  135. package/src/extensions/nocobase-ai/providers/chat-context.tsx +109 -0
  136. package/src/extensions/nocobase-ai/providers/chat-controller.ts +84 -0
  137. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +198 -0
  138. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +871 -0
  139. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +147 -0
  140. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +105 -0
  141. package/src/extensions/nocobase-ai/providers/chat-transport.ts +184 -0
  142. package/src/extensions/nocobase-ai/providers/form-registry.tsx +271 -0
  143. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  144. package/src/extensions/nocobase-ai/providers/index.ts +61 -0
  145. package/src/extensions/nocobase-ai/providers/model.ts +32 -0
  146. package/src/extensions/nocobase-ai/providers/page-context.tsx +136 -0
  147. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +60 -0
  148. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +82 -0
  149. package/src/extensions/nocobase-ai/providers/stream-parser.ts +61 -0
  150. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  151. package/src/extensions/nocobase-ai/providers/types.ts +183 -0
  152. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +463 -0
  153. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  154. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  155. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  156. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  157. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  158. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  159. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +178 -0
  160. package/src/extensions/nocobase-ai/services/index.ts +7 -0
  161. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  162. package/src/extensions/nocobase-ai/services/types.ts +82 -0
  163. package/src/index.tsx +2 -3
  164. package/src/lib/i18n.ts +161 -0
  165. package/src/lib/nocobase/client.ts +19 -2
  166. package/src/locales/en-US.ts +82 -0
  167. package/src/locales/index.ts +13 -0
  168. package/src/locales/zh-CN.ts +79 -0
  169. package/src/pages/users/create.tsx +98 -0
  170. package/src/pages/users/edit.tsx +92 -0
  171. package/src/pages/users/form-context.ts +57 -0
  172. package/src/pages/users/form-fields.tsx +211 -0
  173. package/src/pages/users/list.tsx +205 -0
  174. package/src/pages/users/show.tsx +187 -0
  175. package/src/pages/users/types.ts +20 -0
  176. package/src/providers/constants.ts +1 -0
  177. package/src/pages/blog-posts/create.tsx +0 -177
  178. package/src/pages/blog-posts/edit.tsx +0 -197
  179. package/src/pages/blog-posts/list.tsx +0 -131
  180. package/src/pages/blog-posts/show.tsx +0 -81
  181. package/src/pages/categories/create.tsx +0 -73
  182. package/src/pages/categories/edit.tsx +0 -73
  183. package/src/pages/categories/index.ts +0 -4
  184. package/src/pages/categories/list.tsx +0 -86
  185. package/src/pages/categories/show.tsx +0 -37
  186. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -0,0 +1,209 @@
1
+ import { useGetLocale, useTranslate } from "@refinedev/core";
2
+ import { Database, Languages, PanelsTopLeft } from "lucide-react";
3
+
4
+ import { Badge } from "@/components/ui/badge";
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from "@/components/ui/card";
12
+ import { nocobaseClient } from "@/lib/nocobase/client";
13
+ import {
14
+ LanguageSwitcher,
15
+ useEnabledLocales,
16
+ } from "@/extensions/nocobase-i18n";
17
+ import { I18nPromptGenerator } from "./prompt-generator";
18
+
19
+ export function I18nDemoPage() {
20
+ const t = useTranslate();
21
+ const getLocale = useGetLocale();
22
+ const locale = getLocale();
23
+ const locales = useEnabledLocales();
24
+ const canSwitchLanguage = locales.length > 1;
25
+
26
+ return (
27
+ <div className="space-y-10 pb-12">
28
+ <header className="space-y-2">
29
+ <div className="flex items-center gap-2">
30
+ <Badge variant="secondary">i18n</Badge>
31
+ <Badge variant="outline">{locale}</Badge>
32
+ </div>
33
+ <h1 className="text-2xl font-semibold tracking-[-0.025em] sm:text-3xl">
34
+ {t("demo.title", { ns: "nocobase-i18n" }, "Internationalization")}
35
+ </h1>
36
+ <p className="max-w-3xl text-sm leading-6 text-muted-foreground">
37
+ {t(
38
+ "demo.description",
39
+ { ns: "nocobase-i18n" },
40
+ "An optional hybrid locale runtime with frontend UI resources and selected NocoBase dynamic namespaces."
41
+ )}
42
+ </p>
43
+ </header>
44
+
45
+ <section className="grid gap-5 xl:grid-cols-3">
46
+ <Card>
47
+ <CardHeader>
48
+ <CardTitle className="flex items-center gap-2">
49
+ <Languages className="size-4" />
50
+ {t(
51
+ "demo.switcher.title",
52
+ { ns: "nocobase-i18n" },
53
+ "Language switcher"
54
+ )}
55
+ </CardTitle>
56
+ <CardDescription>
57
+ {t(
58
+ "demo.switcher.description",
59
+ { ns: "nocobase-i18n" },
60
+ "The same reusable component is also integrated into the signed-in user menu."
61
+ )}
62
+ </CardDescription>
63
+ </CardHeader>
64
+ <CardContent>
65
+ {canSwitchLanguage ? (
66
+ <LanguageSwitcher className="max-w-sm" />
67
+ ) : (
68
+ <div className="flex items-start gap-3 rounded-lg border bg-muted/30 p-4">
69
+ <Languages className="mt-0.5 size-5 shrink-0 text-muted-foreground" />
70
+ <div>
71
+ <p className="text-sm font-medium">
72
+ {t(
73
+ "demo.switcher.unavailable.title",
74
+ { ns: "nocobase-i18n" },
75
+ "Only one language is enabled"
76
+ )}
77
+ </p>
78
+ <p className="mt-1 text-xs leading-5 text-muted-foreground">
79
+ {t(
80
+ "demo.switcher.unavailable.description",
81
+ { ns: "nocobase-i18n" },
82
+ "Enable another language in NocoBase system settings to display the switcher."
83
+ )}
84
+ </p>
85
+ </div>
86
+ </div>
87
+ )}
88
+ </CardContent>
89
+ </Card>
90
+
91
+ <CapabilityCard
92
+ icon={<PanelsTopLeft className="size-4" />}
93
+ title={t(
94
+ "demo.frontend.title",
95
+ { ns: "nocobase-i18n" },
96
+ "Application UI translations"
97
+ )}
98
+ description={t(
99
+ "demo.frontend.description",
100
+ { ns: "nocobase-i18n" },
101
+ "Starter and Registry components keep their interface messages in local resource files."
102
+ )}
103
+ >
104
+ <div className="divide-y rounded-lg border">
105
+ <DemoRow
106
+ label={t(
107
+ "language.current",
108
+ { ns: "nocobase-i18n" },
109
+ "Current language"
110
+ )}
111
+ value={locale}
112
+ />
113
+ <DemoRow
114
+ label={t(
115
+ "demo.frontend.actions",
116
+ { ns: "nocobase-i18n" },
117
+ "Common actions"
118
+ )}
119
+ value={`${t("buttons.apply", "Apply")} · ${t(
120
+ "buttons.clear",
121
+ "Clear"
122
+ )}`}
123
+ />
124
+ </div>
125
+ </CapabilityCard>
126
+
127
+ <CapabilityCard
128
+ icon={<Database className="size-4" />}
129
+ title={t(
130
+ "demo.server.title",
131
+ { ns: "nocobase-i18n" },
132
+ "NocoBase metadata translations"
133
+ )}
134
+ description={t(
135
+ "demo.server.description",
136
+ { ns: "nocobase-i18n" },
137
+ "Collection and field labels are loaded from dynamic server namespaces such as lm-collections."
138
+ )}
139
+ >
140
+ <div className="divide-y rounded-lg border">
141
+ <DemoRow
142
+ label="Users"
143
+ value={t("Users", { ns: "lm-collections" }, "Users")}
144
+ />
145
+ <DemoRow
146
+ label="Nickname"
147
+ value={t("Nickname", { ns: "lm-collections" }, "Nickname")}
148
+ />
149
+ <DemoRow
150
+ label="X-Locale"
151
+ value={nocobaseClient.getLocale() ?? locale}
152
+ code
153
+ />
154
+ </div>
155
+ </CapabilityCard>
156
+ </section>
157
+
158
+ <section className="border-t pt-8">
159
+ <I18nPromptGenerator />
160
+ </section>
161
+ </div>
162
+ );
163
+ }
164
+
165
+ function DemoRow({
166
+ code,
167
+ label,
168
+ value,
169
+ }: {
170
+ code?: boolean;
171
+ label: string;
172
+ value: string;
173
+ }) {
174
+ return (
175
+ <div className="flex items-center justify-between gap-4 px-3 py-2.5">
176
+ <span className="text-xs text-muted-foreground">{label}</span>
177
+ {code ? (
178
+ <code className="rounded bg-muted px-2 py-1 text-xs">{value}</code>
179
+ ) : (
180
+ <span className="text-sm font-medium">{value}</span>
181
+ )}
182
+ </div>
183
+ );
184
+ }
185
+
186
+ function CapabilityCard({
187
+ children,
188
+ description,
189
+ icon,
190
+ title,
191
+ }: {
192
+ children: React.ReactNode;
193
+ description: string;
194
+ icon: React.ReactNode;
195
+ title: string;
196
+ }) {
197
+ return (
198
+ <Card>
199
+ <CardHeader>
200
+ <CardTitle className="flex items-center gap-2">
201
+ {icon}
202
+ {title}
203
+ </CardTitle>
204
+ <CardDescription>{description}</CardDescription>
205
+ </CardHeader>
206
+ <CardContent className="text-sm">{children}</CardContent>
207
+ </Card>
208
+ );
209
+ }
@@ -0,0 +1,155 @@
1
+ import { useTranslate } from "@refinedev/core";
2
+ import { Check, ChevronDown, Copy, Sparkles } from "lucide-react";
3
+ import { useMemo, useState } from "react";
4
+
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@/components/ui/card";
13
+ import { Input } from "@/components/ui/input";
14
+ import { Label } from "@/components/ui/label";
15
+ import { Textarea } from "@/components/ui/textarea";
16
+
17
+ export function I18nPromptGenerator() {
18
+ const t = useTranslate();
19
+ const [scene, setScene] = useState("A customer support workspace");
20
+ const [languages, setLanguages] = useState("English and Simplified Chinese");
21
+ const [copied, setCopied] = useState(false);
22
+ const [copyError, setCopyError] = useState(false);
23
+
24
+ const prompt = useMemo(
25
+ () => `Build a complete multilingual scene in the NocoBase Admin Starter.
26
+
27
+ Application scene
28
+ - Product or page: ${scene}
29
+ - Supported languages: ${languages}
30
+ - Translate the complete visible user flow, including empty, loading, validation, and error states.
31
+
32
+ Implementation contract
33
+ - Use the installed @/extensions/nocobase-i18n runtime and the application's translation and locale hooks.
34
+ - Put application-owned React messages in src/locales and register the app namespace through registerTranslationResources from @/lib/i18n.
35
+ - Keep each Registry component's messages in its own namespace and register them with registerLocaleResources().
36
+ - Do not place application messages inside src/extensions/nocobase-i18n/locales because installed Registry files may be refreshed independently.
37
+ - Use the reusable LanguageSwitcher when the page needs an explicit language control; the signed-in user menu already includes one.
38
+ - Keep Starter and Registry UI resources local. Use app:getLang only for registered dynamic server namespaces such as lm-collections.
39
+ - Let the shared NocoBase client send the selected locale through X-Locale.
40
+ - Preserve compatibility when server metadata contains exact {{t("...")}} expressions.
41
+ - Provide complete React components, locale resource files, route/resource integration, and realistic visible content.`,
42
+ [languages, scene]
43
+ );
44
+
45
+ return (
46
+ <details open className="group rounded-xl border bg-muted/15">
47
+ <summary className="flex cursor-pointer list-none items-center gap-2 px-4 py-3 text-sm font-medium marker:hidden">
48
+ <Sparkles className="size-4 text-primary" />
49
+ {t("demo.prompt.title", { ns: "nocobase-i18n" }, "Prompt generator")}
50
+ <ChevronDown className="ml-auto size-4 text-muted-foreground transition-transform group-open:rotate-180" />
51
+ </summary>
52
+ <div className="grid items-start gap-5 border-t p-4 xl:grid-cols-[360px_minmax(0,1fr)]">
53
+ <Card className="gap-0 py-0">
54
+ <CardHeader className="border-b py-4">
55
+ <CardTitle className="text-base">
56
+ {t(
57
+ "demo.prompt.settings",
58
+ { ns: "nocobase-i18n" },
59
+ "Scene settings"
60
+ )}
61
+ </CardTitle>
62
+ <CardDescription>
63
+ {t(
64
+ "demo.prompt.description",
65
+ { ns: "nocobase-i18n" },
66
+ "Describe a complete multilingual application scene and generate an implementation prompt."
67
+ )}
68
+ </CardDescription>
69
+ </CardHeader>
70
+ <CardContent className="space-y-5 py-5">
71
+ <div className="space-y-2">
72
+ <Label htmlFor="i18n-prompt-scene">
73
+ {t(
74
+ "demo.prompt.scene",
75
+ { ns: "nocobase-i18n" },
76
+ "Application scene"
77
+ )}
78
+ </Label>
79
+ <Input
80
+ id="i18n-prompt-scene"
81
+ value={scene}
82
+ onChange={(event) => setScene(event.target.value)}
83
+ />
84
+ </div>
85
+ <div className="space-y-2">
86
+ <Label htmlFor="i18n-prompt-languages">
87
+ {t(
88
+ "demo.prompt.languages",
89
+ { ns: "nocobase-i18n" },
90
+ "Languages"
91
+ )}
92
+ </Label>
93
+ <Textarea
94
+ id="i18n-prompt-languages"
95
+ value={languages}
96
+ onChange={(event) => setLanguages(event.target.value)}
97
+ className="min-h-24"
98
+ />
99
+ </div>
100
+ </CardContent>
101
+ </Card>
102
+ <Card className="min-w-0 gap-0 overflow-hidden py-0">
103
+ <div className="flex items-center justify-between gap-3 border-b px-4 py-3">
104
+ <div>
105
+ <div className="text-sm font-medium">
106
+ {t(
107
+ "demo.prompt.output",
108
+ { ns: "nocobase-i18n" },
109
+ "Generated prompt"
110
+ )}
111
+ </div>
112
+ <div className="text-xs text-muted-foreground">
113
+ {t(
114
+ "demo.prompt.outputDescription",
115
+ { ns: "nocobase-i18n" },
116
+ "Updates as you change the scene and language requirements."
117
+ )}
118
+ </div>
119
+ </div>
120
+ <Button
121
+ size="sm"
122
+ onClick={async () => {
123
+ try {
124
+ await navigator.clipboard.writeText(prompt);
125
+ setCopyError(false);
126
+ setCopied(true);
127
+ window.setTimeout(() => setCopied(false), 1500);
128
+ } catch {
129
+ setCopyError(true);
130
+ }
131
+ }}
132
+ >
133
+ {copied ? <Check /> : <Copy />}
134
+ {copied
135
+ ? t("demo.prompt.copied", { ns: "nocobase-i18n" }, "Copied")
136
+ : t("demo.prompt.copy", { ns: "nocobase-i18n" }, "Copy prompt")}
137
+ </Button>
138
+ </div>
139
+ {copyError ? (
140
+ <p className="border-b px-4 py-2 text-xs text-destructive">
141
+ {t(
142
+ "demo.prompt.copyError",
143
+ { ns: "nocobase-i18n" },
144
+ "Clipboard access failed. Select the prompt and copy it manually."
145
+ )}
146
+ </p>
147
+ ) : null}
148
+ <pre className="max-h-[520px] overflow-auto whitespace-pre-wrap bg-muted/25 p-5 font-mono text-xs leading-5 text-muted-foreground">
149
+ {prompt}
150
+ </pre>
151
+ </Card>
152
+ </div>
153
+ </details>
154
+ );
155
+ }
@@ -0,0 +1,46 @@
1
+ import type { AppExtension } from "@/app/extension";
2
+ import { LoadingState } from "@/components/app-shell/loading-state";
3
+ import { Languages } from "lucide-react";
4
+ import { lazy, Suspense } from "react";
5
+ import { Route } from "react-router";
6
+ import { LanguageUserMenuItems } from "./components";
7
+ import { i18nProvider } from "./i18n-provider";
8
+ import { LocalePreferenceProvider } from "./provider";
9
+
10
+ const I18nDemoPage = lazy(() =>
11
+ import("./demo").then((module) => ({ default: module.I18nDemoPage }))
12
+ );
13
+
14
+ const nocobaseI18nExtension: AppExtension = {
15
+ id: "nocobase-i18n",
16
+ i18nProvider,
17
+ Provider: LocalePreferenceProvider,
18
+ UserMenuItems: LanguageUserMenuItems,
19
+ resources: [
20
+ {
21
+ name: "i18n-demo",
22
+ list: "/i18n",
23
+ meta: {
24
+ label: "Internationalization",
25
+ i18nKey: "navigation.demo",
26
+ i18nOptions: { ns: "nocobase-i18n" },
27
+ icon: <Languages />,
28
+ description: "Optional frontend internationalization for the Starter.",
29
+ acl: { type: "authenticated" },
30
+ },
31
+ },
32
+ ],
33
+ routes: (
34
+ <Route
35
+ key="nocobase-i18n-demo"
36
+ path="/i18n"
37
+ element={
38
+ <Suspense fallback={<LoadingState className="min-h-80" />}>
39
+ <I18nDemoPage />
40
+ </Suspense>
41
+ }
42
+ />
43
+ ),
44
+ };
45
+
46
+ export default nocobaseI18nExtension;
@@ -0,0 +1,9 @@
1
+ import type { I18nProvider } from "@refinedev/core";
2
+
3
+ import { changeLocale, getCurrentLocale, translate } from "./runtime";
4
+
5
+ export const i18nProvider: I18nProvider = {
6
+ translate,
7
+ changeLocale,
8
+ getLocale: getCurrentLocale,
9
+ };
@@ -0,0 +1,6 @@
1
+ export * from "./components";
2
+ export * from "./i18n-provider";
3
+ export * from "./locale-store";
4
+ export * from "./provider";
5
+ export * from "./runtime";
6
+ export * from "./server-resources";
@@ -0,0 +1,134 @@
1
+ import { useSyncExternalStore } from "react";
2
+
3
+ export type LocaleDirection = "ltr" | "rtl";
4
+
5
+ export type LocaleDefinition = {
6
+ locale: string;
7
+ label: string;
8
+ direction?: LocaleDirection;
9
+ };
10
+
11
+ export const DEFAULT_LOCALE = "en-US";
12
+
13
+ const localeDefinitions = new Map<string, LocaleDefinition>([
14
+ ["en-US", { locale: "en-US", label: "English", direction: "ltr" }],
15
+ ["zh-CN", { locale: "zh-CN", label: "简体中文", direction: "ltr" }],
16
+ ]);
17
+ const listeners = new Set<() => void>();
18
+ let enabledLocaleCodes: string[] = [];
19
+ let enabledLocaleSnapshot: LocaleDefinition[] = [];
20
+
21
+ function emitChange() {
22
+ for (const listener of listeners) listener();
23
+ }
24
+
25
+ function subscribe(listener: () => void) {
26
+ listeners.add(listener);
27
+ return () => listeners.delete(listener);
28
+ }
29
+
30
+ function refreshSnapshot() {
31
+ const nextSnapshot = enabledLocaleCodes
32
+ .map((locale) => localeDefinitions.get(locale))
33
+ .filter((definition): definition is LocaleDefinition =>
34
+ Boolean(definition)
35
+ );
36
+ const changed =
37
+ nextSnapshot.length !== enabledLocaleSnapshot.length ||
38
+ nextSnapshot.some((definition, index) => {
39
+ const current = enabledLocaleSnapshot[index];
40
+ return (
41
+ definition.locale !== current?.locale ||
42
+ definition.label !== current.label ||
43
+ definition.direction !== current.direction
44
+ );
45
+ });
46
+
47
+ if (!changed) return;
48
+ enabledLocaleSnapshot = nextSnapshot;
49
+ emitChange();
50
+ }
51
+
52
+ export function getLocaleDirection(locale: string): LocaleDirection {
53
+ return /^(ar|fa|he|ku|ur)(-|$)/i.test(locale) ? "rtl" : "ltr";
54
+ }
55
+
56
+ export function getLocaleLabel(locale: string) {
57
+ const existing = localeDefinitions.get(locale)?.label;
58
+ if (existing && existing !== locale) return existing;
59
+
60
+ try {
61
+ return (
62
+ new Intl.DisplayNames([locale], { type: "language" }).of(locale) ?? locale
63
+ );
64
+ } catch {
65
+ return locale;
66
+ }
67
+ }
68
+
69
+ export function registerLocale(definition: LocaleDefinition) {
70
+ const current = localeDefinitions.get(definition.locale);
71
+ const currentLabel =
72
+ current?.label && current.label !== definition.locale
73
+ ? current.label
74
+ : undefined;
75
+ localeDefinitions.set(definition.locale, {
76
+ locale: definition.locale,
77
+ label:
78
+ definition.label && definition.label !== definition.locale
79
+ ? definition.label
80
+ : currentLabel ?? getLocaleLabel(definition.locale),
81
+ direction:
82
+ definition.direction ??
83
+ current?.direction ??
84
+ getLocaleDirection(definition.locale),
85
+ });
86
+
87
+ if (enabledLocaleCodes.includes(definition.locale)) refreshSnapshot();
88
+ }
89
+
90
+ export function setEnabledLocales(locales: string[]) {
91
+ enabledLocaleCodes = [...new Set(locales.filter(Boolean))];
92
+ for (const locale of enabledLocaleCodes) {
93
+ if (!localeDefinitions.has(locale)) {
94
+ localeDefinitions.set(locale, {
95
+ locale,
96
+ label: getLocaleLabel(locale),
97
+ direction: getLocaleDirection(locale),
98
+ });
99
+ }
100
+ }
101
+ refreshSnapshot();
102
+ }
103
+
104
+ export function getLocaleDefinitions() {
105
+ return enabledLocaleSnapshot;
106
+ }
107
+
108
+ export function useEnabledLocales() {
109
+ return useSyncExternalStore(
110
+ subscribe,
111
+ getLocaleDefinitions,
112
+ getLocaleDefinitions
113
+ );
114
+ }
115
+
116
+ export function resolveSupportedLocale(locale?: string) {
117
+ if (!locale) return DEFAULT_LOCALE;
118
+
119
+ const exact = [...localeDefinitions.keys()].find(
120
+ (candidate) => candidate.toLowerCase() === locale.toLowerCase()
121
+ );
122
+ if (exact) return exact;
123
+
124
+ const language = locale.split("-")[0]?.toLowerCase();
125
+ return (
126
+ [...localeDefinitions.keys()].find(
127
+ (candidate) => candidate.split("-")[0]?.toLowerCase() === language
128
+ ) ?? DEFAULT_LOCALE
129
+ );
130
+ }
131
+
132
+ export function getLocaleDefinition(locale: string) {
133
+ return localeDefinitions.get(locale);
134
+ }
@@ -0,0 +1,89 @@
1
+ const enUS = {
2
+ starter: {
3
+ "shell.workspace": "AI application workspace",
4
+ "shell.footer.freedom": "AI builds freely.",
5
+ "shell.footer.reliability": "NocoBase keeps it reliable.",
6
+ "auth.signOut": "Sign out",
7
+ "auth.signingOut": "Signing out...",
8
+ "buttons.apply": "Apply",
9
+ "buttons.clear": "Clear",
10
+ "buttons.undo": "Undo",
11
+ "pages.error.title": "Page not found.",
12
+ "pages.error.description": "The page you're looking for does not exist.",
13
+ "pages.error.backHome": "Back to homepage",
14
+ "table.filter.text.placeholder": "Filter by...",
15
+ "table.filter.numeric.placeholder": "Filter by...",
16
+ "table.filter.combobox.placeholder": "Select...",
17
+ "table.filter.combobox.search": "Search...",
18
+ "table.filter.combobox.noResults": "Results not found.",
19
+ "table.filter.operator.placeholder": "Search operator...",
20
+ "table.filter.operator.noResults": "No operator found.",
21
+ "table.filter.operator.eq": "Equals",
22
+ "table.filter.operator.ne": "Not equals",
23
+ "table.filter.operator.lt": "Less than",
24
+ "table.filter.operator.gt": "Greater than",
25
+ "table.filter.operator.lte": "Less than or equal",
26
+ "table.filter.operator.gte": "Greater than or equal",
27
+ "table.filter.operator.in": "Includes in an array",
28
+ "table.filter.operator.nin": "Not includes in an array",
29
+ "table.filter.operator.ina": "Includes in an array (case sensitive)",
30
+ "table.filter.operator.nina": "Not includes in an array (case sensitive)",
31
+ "table.filter.operator.contains": "Contains",
32
+ "table.filter.operator.ncontains": "Not contains",
33
+ "table.filter.operator.containss": "Contains (case sensitive)",
34
+ "table.filter.operator.ncontainss": "Not contains (case sensitive)",
35
+ "table.filter.operator.between": "Between",
36
+ "table.filter.operator.nbetween": "Not between",
37
+ "table.filter.operator.null": "Is null",
38
+ "table.filter.operator.nnull": "Is not null",
39
+ "table.filter.operator.startswith": "Starts with",
40
+ "table.filter.operator.nstartswith": "Not starts with",
41
+ "table.filter.operator.startswiths": "Starts with (case sensitive)",
42
+ "table.filter.operator.nstartswiths": "Not starts with (case sensitive)",
43
+ "table.filter.operator.endswith": "Ends with",
44
+ "table.filter.operator.nendswith": "Not ends with",
45
+ "table.filter.operator.endswiths": "Ends with (case sensitive)",
46
+ "table.filter.operator.nendswiths": "Not ends with (case sensitive)",
47
+ "table.filter.operator.eqs": "Equals (case sensitive)",
48
+ "table.filter.operator.nes": "Not equals (case sensitive)",
49
+ },
50
+ "nocobase-i18n": {
51
+ "language.label": "Language",
52
+ "language.current": "Current language",
53
+ "language.switchError": "Unable to switch language.",
54
+ "navigation.group": "Localization",
55
+ "navigation.demo": "Internationalization",
56
+ "demo.title": "Internationalization",
57
+ "demo.description":
58
+ "An optional hybrid locale runtime with frontend UI resources and selected NocoBase dynamic namespaces.",
59
+ "demo.switcher.title": "Language switcher",
60
+ "demo.switcher.description":
61
+ "The same reusable component is also integrated into the signed-in user menu.",
62
+ "demo.switcher.unavailable.title": "Only one language is enabled",
63
+ "demo.switcher.unavailable.description":
64
+ "Enable another language in NocoBase system settings to display the switcher.",
65
+ "demo.frontend.title": "Application UI translations",
66
+ "demo.frontend.description":
67
+ "Starter and Registry components keep their interface messages in local resource files.",
68
+ "demo.frontend.actions": "Common actions",
69
+ "demo.server.title": "NocoBase metadata translations",
70
+ "demo.server.description":
71
+ "Collection and field labels are loaded from dynamic server namespaces such as lm-collections.",
72
+ "demo.currentLocale": "Current locale: {{locale}}",
73
+ "demo.prompt.title": "Prompt generator",
74
+ "demo.prompt.description":
75
+ "Describe a complete multilingual application scene and generate an implementation prompt.",
76
+ "demo.prompt.settings": "Scene settings",
77
+ "demo.prompt.scene": "Application scene",
78
+ "demo.prompt.languages": "Languages",
79
+ "demo.prompt.copy": "Copy prompt",
80
+ "demo.prompt.copied": "Copied",
81
+ "demo.prompt.copyError":
82
+ "Clipboard access failed. Select the prompt and copy it manually.",
83
+ "demo.prompt.output": "Generated prompt",
84
+ "demo.prompt.outputDescription":
85
+ "Updates as you change the scene and language requirements.",
86
+ },
87
+ } as const;
88
+
89
+ export default enUS;