@nocobase/portal-template-default 1.0.2 → 1.0.4

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 (62) hide show
  1. package/README.MD +10 -5
  2. package/package.json +1 -1
  3. package/registry/nocobase-ai/components/chat/chat-message.tsx +8 -6
  4. package/registry/nocobase-ai/components/chat/chat-messages.tsx +8 -3
  5. package/registry/nocobase-ai/components/chat/markdown-message.tsx +26 -0
  6. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +3 -3
  7. package/registry/nocobase-ai/demo/configuration-gate.tsx +83 -35
  8. package/registry/nocobase-i18n/README.md +23 -32
  9. package/registry/nocobase-i18n/components/language-switcher.tsx +1 -1
  10. package/registry/nocobase-i18n/demo/index.tsx +2 -2
  11. package/registry/nocobase-i18n/demo/prompt-generator.tsx +3 -2
  12. package/registry/nocobase-i18n/extension.tsx +3 -4
  13. package/registry/nocobase-i18n/index.ts +4 -3
  14. package/registry/nocobase-i18n/locales/en-US.ts +2 -52
  15. package/registry/nocobase-i18n/locales/index.ts +8 -0
  16. package/registry/nocobase-i18n/locales/zh-CN.ts +2 -52
  17. package/registry/nocobase-i18n/provider.tsx +18 -6
  18. package/registry/nocobase-i18n/server-resources.ts +13 -31
  19. package/registry/nocobase-i18n/tests/i18n-regression.mjs +14 -5
  20. package/registry/nocobase-route-surfaces/README.md +21 -0
  21. package/registry/nocobase-route-surfaces/demo/index.tsx +266 -0
  22. package/registry/nocobase-route-surfaces/demo/lazy-route.tsx +11 -0
  23. package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +90 -0
  24. package/registry/nocobase-route-surfaces/demo/scenarios.ts +95 -0
  25. package/registry/nocobase-route-surfaces/extension.tsx +119 -0
  26. package/registry/nocobase-route-surfaces/index.ts +7 -0
  27. package/registry/nocobase-route-surfaces/route-dialog.tsx +145 -0
  28. package/registry/nocobase-route-surfaces/route-drawer.tsx +186 -0
  29. package/registry/nocobase-route-surfaces/route-page.tsx +29 -0
  30. package/registry/nocobase-route-surfaces/route-surface-context.ts +13 -0
  31. package/registry/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +106 -0
  32. package/registry/nocobase-route-surfaces/use-route-surface-close.ts +15 -0
  33. package/registry/nocobase-route-surfaces/use-route-surface-state.ts +65 -0
  34. package/registry.config.json +30 -3
  35. package/src/App.tsx +65 -35
  36. package/src/app/extension.ts +1 -2
  37. package/src/app/extensions.tsx +0 -14
  38. package/src/components/app-shell/breadcrumb.tsx +67 -2
  39. package/src/components/app-shell/sidebar.tsx +2 -2
  40. package/src/components/resources/views/list-view.tsx +3 -2
  41. package/src/components/ui/sheet.tsx +22 -21
  42. package/src/lib/nocobase/client.ts +6 -1
  43. package/src/locales/en-US.ts +81 -0
  44. package/src/locales/zh-CN.ts +77 -0
  45. package/src/pages/users/create.tsx +76 -53
  46. package/src/pages/users/edit.tsx +87 -56
  47. package/src/pages/users/index.ts +3 -0
  48. package/src/pages/users/layout.tsx +15 -0
  49. package/src/pages/users/list.tsx +19 -21
  50. package/src/pages/users/role-badges.tsx +43 -0
  51. package/src/pages/users/role-detail.tsx +272 -0
  52. package/src/pages/users/role-utils.ts +6 -0
  53. package/src/pages/users/routes.ts +24 -0
  54. package/src/pages/users/show.tsx +195 -104
  55. package/src/pages/users/types.ts +13 -0
  56. package/src/providers/i18n/index.ts +3 -0
  57. package/{registry/nocobase-i18n → src/providers/i18n}/runtime.ts +63 -16
  58. package/src/providers/system-settings/context.ts +26 -0
  59. package/src/providers/system-settings/index.ts +2 -0
  60. package/src/providers/system-settings/provider.tsx +94 -0
  61. /package/{registry/nocobase-i18n → src/providers/i18n}/i18n-provider.ts +0 -0
  62. /package/{registry/nocobase-i18n → src/providers/i18n}/locale-store.ts +0 -0
@@ -2,14 +2,41 @@
2
2
  "name": "nocobase-ai",
3
3
  "homepage": "https://www.nocobase.com",
4
4
  "items": [
5
+ {
6
+ "name": "route-surfaces",
7
+ "type": "registry:lib",
8
+ "title": "NocoBase Route Surfaces",
9
+ "description": "URL-backed drawer, dialog, and page containers with nested drawer stacking and a shared close lifecycle.",
10
+ "dependencies": [
11
+ "@base-ui/react@^1.6.0",
12
+ "@refinedev/core@^5.0.8",
13
+ "lucide-react@^0.487.0",
14
+ "react-router@^7.0.2"
15
+ ],
16
+ "registryDependencies": [
17
+ "alert-dialog",
18
+ "badge",
19
+ "button",
20
+ "card",
21
+ "input",
22
+ "label",
23
+ "select",
24
+ "textarea"
25
+ ],
26
+ "docs": "Provides presentation containers plus an integrated Demo and Prompt generator. Route definitions, ACL guards, data fetching, and business content remain application-owned.",
27
+ "source": {
28
+ "root": "registry/nocobase-route-surfaces",
29
+ "target": "src/extensions/nocobase-route-surfaces",
30
+ "include": ["."]
31
+ }
32
+ },
5
33
  {
6
34
  "name": "i18n",
7
35
  "type": "registry:block",
8
36
  "title": "NocoBase i18n",
9
- "description": "Optional hybrid internationalization with frontend-owned UI resources, selected NocoBase dynamic namespaces, X-Locale synchronization, a reusable language switcher, and an integrated showcase.",
37
+ "description": "Optional NocoBase server-language resources, user preference persistence, reusable language controls, and an integrated showcase for the Starter's built-in i18n runtime.",
10
38
  "dependencies": [
11
39
  "@refinedev/core@^5.0.8",
12
- "i18next@^25.3.2",
13
40
  "lucide-react@^0.487.0",
14
41
  "react-router@^7.0.2"
15
42
  ],
@@ -23,7 +50,7 @@
23
50
  "select",
24
51
  "textarea"
25
52
  ],
26
- "docs": "Requires a compatible NocoBase Starter with the generic AppExtension i18nProvider and UserMenuItems host fields. Starter and Registry translations stay local; app:getLang is used only to merge registered dynamic namespaces such as lm-collections. The integrated Demo route documents the complete usage model.",
53
+ "docs": "Requires a compatible NocoBase Starter with the built-in i18n provider, shared System Settings bootstrap, and AppExtension Provider/UserMenuItems host fields. The Registry reuses systemSettings:get data and calls app:getLang only for registered dynamic namespaces such as lm-collections.",
27
54
  "source": {
28
55
  "root": "registry/nocobase-i18n",
29
56
  "target": "src/extensions/nocobase-i18n",
package/src/App.tsx CHANGED
@@ -5,7 +5,14 @@ import routerProvider, {
5
5
  CatchAllNavigate,
6
6
  UnsavedChangesNotifier,
7
7
  } from "@refinedev/react-router";
8
- import { UserCreate, UserEdit, UserList, UserShow } from "./pages/users";
8
+ import {
9
+ UserCreate,
10
+ UserEdit,
11
+ UserResourceLayout,
12
+ RoleDetailRoute,
13
+ userRoutes,
14
+ UserShow,
15
+ } from "./pages/users";
9
16
  import { dataProvider } from "./providers/data";
10
17
  import { Login } from "./pages/login";
11
18
  import { Register } from "./pages/register";
@@ -20,7 +27,6 @@ import { TooltipProvider } from "./components/ui/tooltip";
20
27
  import { BrandLogo } from "./components/app-shell/brand";
21
28
  import {
22
29
  AppExtensionProviders,
23
- extensionI18nProvider,
24
30
  extensionResources,
25
31
  extensionRouteElements,
26
32
  } from "./app/extensions";
@@ -31,14 +37,16 @@ import { AclBootstrap } from "./components/access-control/acl-bootstrap";
31
37
  import { ResourceAccessGuard } from "./components/access-control/resource-access-guard";
32
38
  import { NavigateToAccessibleResource } from "./components/access-control/navigate-to-accessible-resource";
33
39
  import { UsersRound } from "lucide-react";
40
+ import { i18nProvider } from "./providers/i18n";
41
+ import { SystemSettingsProvider } from "./providers/system-settings";
34
42
 
35
43
  const coreResources: ResourceProps[] = [
36
44
  {
37
45
  name: "users",
38
- list: "/users",
39
- create: "/users/create",
40
- edit: "/users/edit/:id",
41
- show: "/users/show/:id",
46
+ list: userRoutes.list,
47
+ create: userRoutes.create,
48
+ edit: userRoutes.edit,
49
+ show: userRoutes.show,
42
50
  meta: {
43
51
  label: "Users",
44
52
  singularLabel: "User",
@@ -65,24 +73,25 @@ function App() {
65
73
  <BrowserRouter basename={basename || undefined}>
66
74
  <ThemeProvider>
67
75
  <TooltipProvider>
68
- <Refine
69
- dataProvider={dataProvider}
70
- notificationProvider={useNotificationProvider()}
71
- routerProvider={routerProvider}
72
- authProvider={authProvider}
73
- accessControlProvider={accessControlProvider}
74
- i18nProvider={extensionI18nProvider}
75
- resources={[...coreResources, ...extensionResources]}
76
- options={{
77
- syncWithLocation: true,
78
- warnWhenUnsavedChanges: true,
79
- disableTelemetry: true,
80
- title: {
81
- text: "NocoBase",
82
- icon: <BrandLogo className="size-14 rounded-2xl" />,
83
- },
84
- }}
85
- >
76
+ <SystemSettingsProvider>
77
+ <Refine
78
+ dataProvider={dataProvider}
79
+ notificationProvider={useNotificationProvider()}
80
+ routerProvider={routerProvider}
81
+ authProvider={authProvider}
82
+ accessControlProvider={accessControlProvider}
83
+ i18nProvider={i18nProvider}
84
+ resources={[...coreResources, ...extensionResources]}
85
+ options={{
86
+ syncWithLocation: true,
87
+ warnWhenUnsavedChanges: true,
88
+ disableTelemetry: true,
89
+ title: {
90
+ text: "NocoBase",
91
+ icon: <BrandLogo className="size-14 rounded-2xl" />,
92
+ },
93
+ }}
94
+ >
86
95
  <Routes>
87
96
  <Route
88
97
  element={
@@ -101,15 +110,10 @@ function App() {
101
110
  }
102
111
  >
103
112
  <Route index element={<NavigateToAccessibleResource />} />
104
- <Route path="/users">
105
- <Route
106
- index
107
- element={
108
- <ResourceAccessGuard resource="users" action="list">
109
- <UserList />
110
- </ResourceAccessGuard>
111
- }
112
- />
113
+ <Route
114
+ path="/users"
115
+ element={<UserResourceLayout />}
116
+ >
113
117
  <Route
114
118
  path="create"
115
119
  element={
@@ -126,6 +130,14 @@ function App() {
126
130
  </ResourceAccessGuard>
127
131
  }
128
132
  />
133
+ <Route
134
+ path="roles/:roleName"
135
+ element={
136
+ <ResourceAccessGuard resource="roles" action="show">
137
+ <RoleDetailRoute returnTo="list" />
138
+ </ResourceAccessGuard>
139
+ }
140
+ />
129
141
  <Route
130
142
  path="show/:id"
131
143
  element={
@@ -133,7 +145,24 @@ function App() {
133
145
  <UserShow />
134
146
  </ResourceAccessGuard>
135
147
  }
136
- />
148
+ >
149
+ <Route
150
+ path="edit"
151
+ element={
152
+ <ResourceAccessGuard resource="users" action="edit">
153
+ <UserEdit returnTo="show" />
154
+ </ResourceAccessGuard>
155
+ }
156
+ />
157
+ <Route
158
+ path="roles/:roleName"
159
+ element={
160
+ <ResourceAccessGuard resource="roles" action="show">
161
+ <RoleDetailRoute returnTo="show" />
162
+ </ResourceAccessGuard>
163
+ }
164
+ />
165
+ </Route>
137
166
  </Route>
138
167
  {extensionRouteElements}
139
168
  <Route path="*" element={<ErrorComponent />} />
@@ -159,7 +188,8 @@ function App() {
159
188
  <Toaster />
160
189
  <UnsavedChangesNotifier />
161
190
  <DocumentTitleHandler appName="NocoBase" />
162
- </Refine>
191
+ </Refine>
192
+ </SystemSettingsProvider>
163
193
  </TooltipProvider>
164
194
  </ThemeProvider>
165
195
  </BrowserRouter>
@@ -1,4 +1,4 @@
1
- import type { I18nProvider, ResourceProps } from "@refinedev/core";
1
+ import type { ResourceProps } from "@refinedev/core";
2
2
  import type { ComponentType, PropsWithChildren, ReactElement } from "react";
3
3
 
4
4
  export type AppExtension = {
@@ -7,5 +7,4 @@ export type AppExtension = {
7
7
  routes?: ReactElement;
8
8
  Provider?: ComponentType<PropsWithChildren>;
9
9
  UserMenuItems?: ComponentType;
10
- i18nProvider?: I18nProvider;
11
10
  };
@@ -18,20 +18,6 @@ export const extensionRouteElements = appExtensions
18
18
  .map((extension) => extension.routes)
19
19
  .filter((routes): routes is NonNullable<typeof routes> => Boolean(routes));
20
20
 
21
- const i18nExtensions = appExtensions.filter(
22
- (extension) => extension.i18nProvider
23
- );
24
-
25
- if (i18nExtensions.length > 1) {
26
- console.warn(
27
- `Multiple app extensions provide i18nProvider (${i18nExtensions
28
- .map((extension) => extension.id)
29
- .join(", ")}). Using ${i18nExtensions[0].id}.`
30
- );
31
- }
32
-
33
- export const extensionI18nProvider = i18nExtensions[0]?.i18nProvider;
34
-
35
21
  export const extensionUserMenuItems = appExtensions
36
22
  .filter((extension) => extension.UserMenuItems)
37
23
  .map((extension) => ({
@@ -3,9 +3,12 @@
3
3
  import { Fragment, useMemo } from "react";
4
4
  import { Home } from "lucide-react";
5
5
  import {
6
+ type IResourceItem,
6
7
  matchResourceFromRoute,
7
8
  useBreadcrumb,
9
+ useGetToPath,
8
10
  useLink,
11
+ useParsed,
9
12
  useResourceParams,
10
13
  useTranslate,
11
14
  useUserFriendlyName,
@@ -24,9 +27,42 @@ export function Breadcrumb() {
24
27
  const Link = useLink();
25
28
  const { breadcrumbs } = useBreadcrumb();
26
29
  const { resources } = useResourceParams();
30
+ const { pathname } = useParsed();
31
+ const getToPath = useGetToPath();
27
32
  const translate = useTranslate();
28
33
  const getUserFriendlyName = useUserFriendlyName();
29
34
  const rootRouteResource = matchResourceFromRoute("/", resources);
35
+ const resolvedBreadcrumbs = useMemo(() => {
36
+ if (breadcrumbs.length > 0) return breadcrumbs;
37
+
38
+ const closestMatch = findClosestRouteMatch(pathname, resources);
39
+ if (!closestMatch.resource) return breadcrumbs;
40
+
41
+ const items: Array<{ label: string; href?: string }> = [
42
+ {
43
+ label:
44
+ closestMatch.resource.meta?.label ?? closestMatch.resource.name,
45
+ href: getToPath({ resource: closestMatch.resource, action: "list" }),
46
+ },
47
+ ];
48
+ const nestedAction = getNestedRouteAction(
49
+ pathname,
50
+ closestMatch.matchedSegmentCount
51
+ ) ??
52
+ (closestMatch.action && closestMatch.action !== "list"
53
+ ? closestMatch.action
54
+ : undefined);
55
+ if (nestedAction) {
56
+ items.push({
57
+ label: translate(
58
+ `buttons.${nestedAction}`,
59
+ nestedAction.charAt(0).toUpperCase() + nestedAction.slice(1)
60
+ ),
61
+ });
62
+ }
63
+
64
+ return items;
65
+ }, [breadcrumbs, getToPath, pathname, resources, translate]);
30
66
 
31
67
  const breadCrumbItems = useMemo(() => {
32
68
  const list: {
@@ -47,7 +83,7 @@ export function Breadcrumb() {
47
83
  ),
48
84
  });
49
85
 
50
- for (const { label, href } of breadcrumbs) {
86
+ for (const { label, href } of resolvedBreadcrumbs) {
51
87
  const matchingResource = resources.find((resource) => {
52
88
  const metaLabel = resource.meta?.label;
53
89
  return (
@@ -80,9 +116,9 @@ export function Breadcrumb() {
80
116
 
81
117
  return list;
82
118
  }, [
83
- breadcrumbs,
84
119
  getUserFriendlyName,
85
120
  Link,
121
+ resolvedBreadcrumbs,
86
122
  resources,
87
123
  rootRouteResource,
88
124
  translate,
@@ -114,4 +150,33 @@ export function Breadcrumb() {
114
150
  );
115
151
  }
116
152
 
153
+ function findClosestRouteMatch(
154
+ pathname: string | undefined,
155
+ resources: IResourceItem[]
156
+ ) {
157
+ const segments = pathname?.split("/").filter(Boolean) ?? [];
158
+
159
+ for (let length = segments.length; length >= 0; length -= 1) {
160
+ const route = `/${segments.slice(0, length).join("/")}`;
161
+ const match = matchResourceFromRoute(route, resources);
162
+ if (match.resource) {
163
+ return { ...match, matchedSegmentCount: length };
164
+ }
165
+ }
166
+
167
+ return { resource: undefined, matchedSegmentCount: 0 };
168
+ }
169
+
170
+ function getNestedRouteAction(
171
+ pathname: string | undefined,
172
+ matchedSegmentCount: number
173
+ ) {
174
+ const segments = pathname?.split("/").filter(Boolean) ?? [];
175
+ const action = segments[matchedSegmentCount];
176
+
177
+ return action && ["create", "edit", "show", "clone"].includes(action)
178
+ ? action
179
+ : undefined;
180
+ }
181
+
117
182
  Breadcrumb.displayName = "Breadcrumb";
@@ -218,7 +218,7 @@ function SidebarDropdownItem({
218
218
  selectedKey,
219
219
  Link,
220
220
  }: MenuItemProps & { Link: ReturnType<typeof useLink> }) {
221
- const isSelected = item.key === selectedKey;
221
+ const isSelected = isTreeItemSelected(item, selectedKey);
222
222
  const displayName = useMenuItemLabel(item);
223
223
 
224
224
  return (
@@ -239,7 +239,7 @@ function SidebarDropdownItem({
239
239
  }
240
240
 
241
241
  function SidebarItemLink({ item, selectedKey }: MenuItemProps) {
242
- const isSelected = item.key === selectedKey;
242
+ const isSelected = isTreeItemSelected(item, selectedKey);
243
243
 
244
244
  return <SidebarButton item={item} isSelected={isSelected} asLink={true} />;
245
245
  }
@@ -11,12 +11,13 @@ import { cn } from "@/lib/utils";
11
11
 
12
12
  type ListViewProps = PropsWithChildren<{
13
13
  className?: string;
14
+ resource?: string;
14
15
  }>;
15
16
 
16
- export function ListView({ children, className }: ListViewProps) {
17
+ export function ListView({ children, className, resource }: ListViewProps) {
17
18
  return (
18
19
  <div className={cn("flex flex-col", "gap-6", className)}>
19
- <ListViewHeader />
20
+ <ListViewHeader resource={resource} />
20
21
  {children}
21
22
  </div>
22
23
  );
@@ -1,24 +1,24 @@
1
- import * as React from "react"
2
- import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
1
+ import * as React from "react";
2
+ import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
3
3
 
4
- import { cn } from "@/lib/utils"
5
- import { Button } from "@/components/ui/button"
6
- import { XIcon } from "lucide-react"
4
+ import { cn } from "@/lib/utils";
5
+ import { Button } from "@/components/ui/button";
6
+ import { XIcon } from "lucide-react";
7
7
 
8
8
  function Sheet({ ...props }: SheetPrimitive.Root.Props) {
9
- return <SheetPrimitive.Root data-slot="sheet" {...props} />
9
+ return <SheetPrimitive.Root data-slot="sheet" {...props} />;
10
10
  }
11
11
 
12
12
  function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
13
- return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
13
+ return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
14
14
  }
15
15
 
16
16
  function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
17
- return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
17
+ return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
18
18
  }
19
19
 
20
20
  function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
21
- return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
21
+ return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
22
22
  }
23
23
 
24
24
  function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
@@ -31,7 +31,7 @@ function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
31
31
  )}
32
32
  {...props}
33
33
  />
34
- )
34
+ );
35
35
  }
36
36
 
37
37
  function SheetContent({
@@ -39,10 +39,12 @@ function SheetContent({
39
39
  children,
40
40
  side = "right",
41
41
  showCloseButton = true,
42
+ closeLabel = "Close",
42
43
  ...props
43
44
  }: SheetPrimitive.Popup.Props & {
44
- side?: "top" | "right" | "bottom" | "left"
45
- showCloseButton?: boolean
45
+ side?: "top" | "right" | "bottom" | "left";
46
+ showCloseButton?: boolean;
47
+ closeLabel?: string;
46
48
  }) {
47
49
  return (
48
50
  <SheetPortal>
@@ -68,14 +70,13 @@ function SheetContent({
68
70
  />
69
71
  }
70
72
  >
71
- <XIcon
72
- />
73
- <span className="sr-only">Close</span>
73
+ <XIcon />
74
+ <span className="sr-only">{closeLabel}</span>
74
75
  </SheetPrimitive.Close>
75
76
  )}
76
77
  </SheetPrimitive.Popup>
77
78
  </SheetPortal>
78
- )
79
+ );
79
80
  }
80
81
 
81
82
  function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -85,7 +86,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
85
86
  className={cn("flex flex-col gap-0.5 p-4", className)}
86
87
  {...props}
87
88
  />
88
- )
89
+ );
89
90
  }
90
91
 
91
92
  function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -95,7 +96,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
95
96
  className={cn("mt-auto flex flex-col gap-2 p-4", className)}
96
97
  {...props}
97
98
  />
98
- )
99
+ );
99
100
  }
100
101
 
101
102
  function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
@@ -108,7 +109,7 @@ function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
108
109
  )}
109
110
  {...props}
110
111
  />
111
- )
112
+ );
112
113
  }
113
114
 
114
115
  function SheetDescription({
@@ -121,7 +122,7 @@ function SheetDescription({
121
122
  className={cn("text-sm text-muted-foreground", className)}
122
123
  {...props}
123
124
  />
124
- )
125
+ );
125
126
  }
126
127
 
127
128
  export {
@@ -133,4 +134,4 @@ export {
133
134
  SheetFooter,
134
135
  SheetTitle,
135
136
  SheetDescription,
136
- }
137
+ };
@@ -58,6 +58,7 @@ const unwrapPayload = (
58
58
 
59
59
  export class NocoBaseClient {
60
60
  private readonly apiOrigin?: string;
61
+ private runtimeLocale?: string;
61
62
 
62
63
  constructor(
63
64
  private readonly apiUrl = API_URL,
@@ -117,7 +118,7 @@ export class NocoBaseClient {
117
118
  }
118
119
 
119
120
  getLocale() {
120
- return this.getStoredLocale() ?? getBrowserLocale();
121
+ return this.getStoredLocale() ?? this.runtimeLocale ?? getBrowserLocale();
121
122
  }
122
123
 
123
124
  setLocale(locale?: string | null) {
@@ -126,6 +127,10 @@ export class NocoBaseClient {
126
127
  else localStorage.removeItem(NOCOBASE_LOCALE_KEY);
127
128
  }
128
129
 
130
+ setRuntimeLocale(locale?: string | null) {
131
+ this.runtimeLocale = locale || undefined;
132
+ }
133
+
129
134
  buildUrl(endpoint: string, query?: Record<string, QueryValue>) {
130
135
  const base = `${this.apiUrl.replace(/\/$/, "")}/${endpoint.replace(
131
136
  /^\//,
@@ -1,11 +1,63 @@
1
1
  export const starter = {
2
+ "shell.workspace": "AI application workspace",
3
+ "shell.footer.freedom": "AI builds freely.",
4
+ "shell.footer.reliability": "NocoBase keeps it reliable.",
5
+ "auth.signOut": "Sign out",
6
+ "auth.signingOut": "Signing out...",
7
+ "buttons.apply": "Apply",
8
+ "buttons.clear": "Clear",
9
+ "buttons.undo": "Undo",
10
+ "pages.error.title": "Page not found.",
11
+ "pages.error.description": "The page you're looking for does not exist.",
12
+ "pages.error.backHome": "Back to homepage",
13
+ "table.filter.text.placeholder": "Filter by...",
14
+ "table.filter.numeric.placeholder": "Filter by...",
15
+ "table.filter.combobox.placeholder": "Select...",
16
+ "table.filter.combobox.search": "Search...",
17
+ "table.filter.combobox.noResults": "Results not found.",
18
+ "table.filter.operator.placeholder": "Search operator...",
19
+ "table.filter.operator.noResults": "No operator found.",
20
+ "table.filter.operator.eq": "Equals",
21
+ "table.filter.operator.ne": "Not equals",
22
+ "table.filter.operator.lt": "Less than",
23
+ "table.filter.operator.gt": "Greater than",
24
+ "table.filter.operator.lte": "Less than or equal",
25
+ "table.filter.operator.gte": "Greater than or equal",
26
+ "table.filter.operator.in": "Includes in an array",
27
+ "table.filter.operator.nin": "Not includes in an array",
28
+ "table.filter.operator.ina": "Includes in an array (case sensitive)",
29
+ "table.filter.operator.nina": "Not includes in an array (case sensitive)",
30
+ "table.filter.operator.contains": "Contains",
31
+ "table.filter.operator.ncontains": "Not contains",
32
+ "table.filter.operator.containss": "Contains (case sensitive)",
33
+ "table.filter.operator.ncontainss": "Not contains (case sensitive)",
34
+ "table.filter.operator.between": "Between",
35
+ "table.filter.operator.nbetween": "Not between",
36
+ "table.filter.operator.null": "Is null",
37
+ "table.filter.operator.nnull": "Is not null",
38
+ "table.filter.operator.startswith": "Starts with",
39
+ "table.filter.operator.nstartswith": "Not starts with",
40
+ "table.filter.operator.startswiths": "Starts with (case sensitive)",
41
+ "table.filter.operator.nstartswiths": "Not starts with (case sensitive)",
42
+ "table.filter.operator.endswith": "Ends with",
43
+ "table.filter.operator.nendswith": "Not ends with",
44
+ "table.filter.operator.endswiths": "Ends with (case sensitive)",
45
+ "table.filter.operator.nendswiths": "Not ends with (case sensitive)",
46
+ "table.filter.operator.eqs": "Equals (case sensitive)",
47
+ "table.filter.operator.nes": "Not equals (case sensitive)",
2
48
  "buttons.create": "Create",
3
49
  "buttons.edit": "Edit",
4
50
  "buttons.show": "View",
5
51
  "buttons.delete": "Delete",
6
52
  "buttons.refresh": "Refresh",
7
53
  "buttons.cancel": "Cancel",
54
+ "buttons.close": "Close",
8
55
  "buttons.confirm": "Are you sure?",
56
+ "unsavedChanges.title": "Discard unsaved changes?",
57
+ "unsavedChanges.description":
58
+ "Your changes have not been saved. Leaving now will discard them.",
59
+ "unsavedChanges.stay": "Keep editing",
60
+ "unsavedChanges.leave": "Discard changes",
9
61
  "views.create.title": "Create {{resource}}",
10
62
  "views.create.description":
11
63
  "Add a new {{resource}} to your NocoBase workspace.",
@@ -52,6 +104,14 @@ export const app = {
52
104
  "users.actions.view": "View user",
53
105
  "users.actions.edit": "Edit user",
54
106
  "users.actions.delete": "Delete user",
107
+ "users.drawer.create.title": "Create user",
108
+ "users.drawer.create.description":
109
+ "Add a user who can sign in to this NocoBase application.",
110
+ "users.drawer.edit.title": "Edit user",
111
+ "users.drawer.edit.description":
112
+ "Update this user's identity and contact information.",
113
+ "users.drawer.show.description":
114
+ "Review this user's identity, contact information, and roles.",
55
115
  "users.form.create.submit": "Create user",
56
116
  "users.form.create.submitting": "Creating...",
57
117
  "users.form.edit.submit": "Save changes",
@@ -75,8 +135,29 @@ export const app = {
75
135
  "users.detail.timestamps": "Timestamps",
76
136
  "users.detail.noRoles": "No assigned roles",
77
137
  "users.detail.unnamed": "Unnamed user",
138
+ "users.detail.loadError.title": "Unable to load user",
139
+ "users.detail.loadError.description":
140
+ "The user may no longer exist, or you may not have permission to view it.",
78
141
  "users.ai.table": "Users table",
79
142
  "users.ai.detail": "User details",
80
143
  "users.ai.createForm": "Create user form",
81
144
  "users.ai.editForm": "Edit user form",
145
+ "common.yes": "Yes",
146
+ "common.no": "No",
147
+ "roles.drawer.description": "Review the role associated with this user.",
148
+ "roles.fields.name": "Name",
149
+ "roles.fields.title": "Title",
150
+ "roles.fields.description": "Description",
151
+ "roles.fields.default": "Default role",
152
+ "roles.fields.hidden": "Hidden",
153
+ "roles.fields.allowConfigure": "Can configure",
154
+ "roles.fields.createdAt": "Created at",
155
+ "roles.fields.updatedAt": "Updated at",
156
+ "roles.detail.identity": "Role identity",
157
+ "roles.detail.settings": "Settings",
158
+ "roles.detail.actions": "Allowed actions",
159
+ "roles.detail.timestamps": "Timestamps",
160
+ "roles.detail.unavailable":
161
+ "Additional role details are unavailable. Showing the relation data included with the user.",
162
+ "roles.ai.detail": "Role details",
82
163
  } as const;