@nocobase/portal-template-default 1.0.1 → 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.
- package/README.MD +22 -3
- package/package.json +4 -2
- package/registry/nocobase-acl/components/role-options.ts +7 -0
- package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
- package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
- package/registry/nocobase-acl/demo/components.tsx +32 -11
- package/registry/nocobase-acl/demo/index.tsx +45 -14
- package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
- package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
- package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
- package/registry/nocobase-acl/extension.tsx +1 -1
- package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
- package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
- package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
- package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
- package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
- package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
- package/registry/nocobase-ai/extension.tsx +2 -7
- package/registry/nocobase-i18n/README.md +53 -0
- package/registry/nocobase-i18n/components/index.ts +1 -0
- package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
- package/registry/nocobase-i18n/demo/index.tsx +209 -0
- package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
- package/registry/nocobase-i18n/extension.tsx +46 -0
- package/registry/nocobase-i18n/i18n-provider.ts +9 -0
- package/registry/nocobase-i18n/index.ts +6 -0
- package/registry/nocobase-i18n/locale-store.ts +134 -0
- package/registry/nocobase-i18n/locales/en-US.ts +89 -0
- package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
- package/registry/nocobase-i18n/provider.tsx +32 -0
- package/registry/nocobase-i18n/runtime.ts +118 -0
- package/registry/nocobase-i18n/server-resources.ts +184 -0
- package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
- package/registry.config.json +34 -14
- package/scripts/registry.mjs +3 -0
- package/src/App.tsx +27 -86
- package/src/app/extension.ts +3 -1
- package/src/app/extensions.tsx +21 -0
- package/src/components/access-control/acl-gate.tsx +4 -10
- package/src/components/app-shell/breadcrumb.tsx +39 -3
- package/src/components/app-shell/document-title-handler.tsx +11 -20
- package/src/components/app-shell/header.tsx +18 -3
- package/src/components/app-shell/loading-overlay.tsx +2 -4
- package/src/components/app-shell/loading-state.tsx +15 -0
- package/src/components/app-shell/sidebar.tsx +75 -47
- package/src/components/data-table/data-table-pagination.tsx +27 -7
- package/src/components/data-table/data-table-styles.ts +57 -0
- package/src/components/data-table/data-table.tsx +14 -59
- package/src/components/resources/resource-label.ts +101 -0
- package/src/components/resources/views/create-view.tsx +17 -14
- package/src/components/resources/views/edit-view.tsx +17 -15
- package/src/components/resources/views/list-view.tsx +20 -10
- package/src/components/resources/views/show-view.tsx +20 -18
- package/src/index.tsx +2 -3
- package/src/lib/i18n.ts +161 -0
- package/src/lib/nocobase/client.ts +19 -2
- package/src/locales/en-US.ts +82 -0
- package/src/locales/index.ts +13 -0
- package/src/locales/zh-CN.ts +79 -0
- package/src/pages/users/create.tsx +98 -0
- package/src/pages/users/edit.tsx +92 -0
- package/src/pages/users/form-context.ts +57 -0
- package/src/pages/users/form-fields.tsx +211 -0
- package/src/pages/users/list.tsx +205 -0
- package/src/pages/users/show.tsx +187 -0
- package/src/pages/users/types.ts +20 -0
- package/src/providers/constants.ts +1 -0
- package/src/pages/blog-posts/create.tsx +0 -177
- package/src/pages/blog-posts/edit.tsx +0 -197
- package/src/pages/blog-posts/list.tsx +0 -131
- package/src/pages/blog-posts/show.tsx +0 -81
- package/src/pages/categories/create.tsx +0 -73
- package/src/pages/categories/edit.tsx +0 -73
- package/src/pages/categories/index.ts +0 -4
- package/src/pages/categories/list.tsx +0 -86
- package/src/pages/categories/show.tsx +0 -37
- /package/src/pages/{blog-posts → users}/index.ts +0 -0
|
@@ -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 =
|
|
55
|
-
|
|
56
|
-
|
|
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,
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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({
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 />);
|
package/src/lib/i18n.ts
ADDED
|
@@ -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
|
+
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
API_ORIGIN,
|
|
3
3
|
API_URL,
|
|
4
4
|
NOCOBASE_AUTHENTICATOR,
|
|
5
|
+
NOCOBASE_LOCALE_KEY,
|
|
5
6
|
NOCOBASE_ROLE_KEY,
|
|
6
7
|
NOCOBASE_TOKEN_KEY,
|
|
7
8
|
} from "@/providers/constants";
|
|
@@ -30,7 +31,7 @@ export type NocoBaseRequestOptions = {
|
|
|
30
31
|
unwrap?: "data" | "deep-data" | "none";
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
const
|
|
34
|
+
const getBrowserLocale = () =>
|
|
34
35
|
typeof navigator === "undefined" ? undefined : navigator.language;
|
|
35
36
|
|
|
36
37
|
const getClientTimezone = () => {
|
|
@@ -109,6 +110,22 @@ export class NocoBaseClient {
|
|
|
109
110
|
else localStorage.removeItem(NOCOBASE_ROLE_KEY);
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
getStoredLocale() {
|
|
114
|
+
return typeof localStorage === "undefined"
|
|
115
|
+
? undefined
|
|
116
|
+
: localStorage.getItem(NOCOBASE_LOCALE_KEY) ?? undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getLocale() {
|
|
120
|
+
return this.getStoredLocale() ?? getBrowserLocale();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
setLocale(locale?: string | null) {
|
|
124
|
+
if (typeof localStorage === "undefined") return;
|
|
125
|
+
if (locale) localStorage.setItem(NOCOBASE_LOCALE_KEY, locale);
|
|
126
|
+
else localStorage.removeItem(NOCOBASE_LOCALE_KEY);
|
|
127
|
+
}
|
|
128
|
+
|
|
112
129
|
buildUrl(endpoint: string, query?: Record<string, QueryValue>) {
|
|
113
130
|
const base = `${this.apiUrl.replace(/\/$/, "")}/${endpoint.replace(
|
|
114
131
|
/^\//,
|
|
@@ -149,7 +166,7 @@ export class NocoBaseClient {
|
|
|
149
166
|
| "accept"
|
|
150
167
|
| "body"
|
|
151
168
|
> = {}) {
|
|
152
|
-
const locale =
|
|
169
|
+
const locale = this.getLocale();
|
|
153
170
|
const formData =
|
|
154
171
|
typeof FormData !== "undefined" && body instanceof FormData;
|
|
155
172
|
return {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const starter = {
|
|
2
|
+
"buttons.create": "Create",
|
|
3
|
+
"buttons.edit": "Edit",
|
|
4
|
+
"buttons.show": "View",
|
|
5
|
+
"buttons.delete": "Delete",
|
|
6
|
+
"buttons.refresh": "Refresh",
|
|
7
|
+
"buttons.cancel": "Cancel",
|
|
8
|
+
"buttons.confirm": "Are you sure?",
|
|
9
|
+
"views.create.title": "Create {{resource}}",
|
|
10
|
+
"views.create.description":
|
|
11
|
+
"Add a new {{resource}} to your NocoBase workspace.",
|
|
12
|
+
"views.edit.title": "Edit {{resource}}",
|
|
13
|
+
"views.edit.description":
|
|
14
|
+
"Update this {{resource}} while NocoBase keeps the data consistent.",
|
|
15
|
+
"views.show.description": "Review the record and its NocoBase-managed data.",
|
|
16
|
+
"table.empty.title": "No data to display",
|
|
17
|
+
"table.empty.description": "This table is empty for the time being.",
|
|
18
|
+
"table.pagination.total": "{{count}} row(s)",
|
|
19
|
+
"table.pagination.pageSize": "Rows per page",
|
|
20
|
+
"table.pagination.page": "Page {{current}} of {{total}}",
|
|
21
|
+
"table.pagination.first": "Go to first page",
|
|
22
|
+
"table.pagination.previous": "Go to previous page",
|
|
23
|
+
"table.pagination.next": "Go to next page",
|
|
24
|
+
"table.pagination.last": "Go to last page",
|
|
25
|
+
"notifications.success": "Success",
|
|
26
|
+
"notifications.createSuccess": "Successfully created {{resource}}",
|
|
27
|
+
"notifications.editSuccess": "Successfully updated {{resource}}",
|
|
28
|
+
"notifications.deleteSuccess": "Successfully deleted {{resource}}",
|
|
29
|
+
"users.users": "User",
|
|
30
|
+
Admin: "Admin",
|
|
31
|
+
Root: "Root",
|
|
32
|
+
Member: "Member",
|
|
33
|
+
Anonymous: "Anonymous",
|
|
34
|
+
"Full permissions": "Full permissions",
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
export const app = {
|
|
38
|
+
"resources.users": "Users",
|
|
39
|
+
"resources.user": "User",
|
|
40
|
+
"resources.users.description":
|
|
41
|
+
"Manage the people who can sign in and work in this NocoBase application.",
|
|
42
|
+
"users.fields.id": "ID",
|
|
43
|
+
"users.fields.nickname": "Nickname",
|
|
44
|
+
"users.fields.username": "Username",
|
|
45
|
+
"users.fields.email": "Email",
|
|
46
|
+
"users.fields.phone": "Phone",
|
|
47
|
+
"users.fields.password": "Password",
|
|
48
|
+
"users.fields.roles": "Roles",
|
|
49
|
+
"users.fields.createdAt": "Created at",
|
|
50
|
+
"users.fields.updatedAt": "Updated at",
|
|
51
|
+
"users.fields.actions": "Actions",
|
|
52
|
+
"users.actions.view": "View user",
|
|
53
|
+
"users.actions.edit": "Edit user",
|
|
54
|
+
"users.actions.delete": "Delete user",
|
|
55
|
+
"users.form.create.submit": "Create user",
|
|
56
|
+
"users.form.create.submitting": "Creating...",
|
|
57
|
+
"users.form.edit.submit": "Save changes",
|
|
58
|
+
"users.form.edit.submitting": "Saving...",
|
|
59
|
+
"users.form.cancel": "Cancel",
|
|
60
|
+
"users.form.nickname.placeholder": "Enter a display name",
|
|
61
|
+
"users.form.username.placeholder": "Enter a unique username",
|
|
62
|
+
"users.form.email.placeholder": "Enter an email address",
|
|
63
|
+
"users.form.phone.placeholder": "Enter a phone number",
|
|
64
|
+
"users.form.password.placeholder": "Set an initial password",
|
|
65
|
+
"users.form.password.aiNotice":
|
|
66
|
+
"Passwords are never included in AI page context or Form filler.",
|
|
67
|
+
"users.validation.username": "Username is required",
|
|
68
|
+
"users.validation.email": "Email is required",
|
|
69
|
+
"users.validation.emailFormat": "Enter a valid email address",
|
|
70
|
+
"users.validation.phone": "Phone is required",
|
|
71
|
+
"users.validation.password": "Password is required",
|
|
72
|
+
"users.detail.identity": "Identity",
|
|
73
|
+
"users.detail.contact": "Contact",
|
|
74
|
+
"users.detail.access": "Access",
|
|
75
|
+
"users.detail.timestamps": "Timestamps",
|
|
76
|
+
"users.detail.noRoles": "No assigned roles",
|
|
77
|
+
"users.detail.unnamed": "Unnamed user",
|
|
78
|
+
"users.ai.table": "Users table",
|
|
79
|
+
"users.ai.detail": "User details",
|
|
80
|
+
"users.ai.createForm": "Create user form",
|
|
81
|
+
"users.ai.editForm": "Edit user form",
|
|
82
|
+
} as const;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { registerTranslationResources } from "@/lib/i18n";
|
|
2
|
+
import { app as enUSApp, starter as enUSStarter } from "./en-US";
|
|
3
|
+
import { app as zhCNApp, starter as zhCNStarter } from "./zh-CN";
|
|
4
|
+
|
|
5
|
+
registerTranslationResources("starter", {
|
|
6
|
+
"en-US": enUSStarter,
|
|
7
|
+
"zh-CN": zhCNStarter,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
registerTranslationResources("app", {
|
|
11
|
+
"en-US": enUSApp,
|
|
12
|
+
"zh-CN": zhCNApp,
|
|
13
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const starter = {
|
|
2
|
+
"buttons.create": "新建",
|
|
3
|
+
"buttons.edit": "编辑",
|
|
4
|
+
"buttons.show": "查看",
|
|
5
|
+
"buttons.delete": "删除",
|
|
6
|
+
"buttons.refresh": "刷新",
|
|
7
|
+
"buttons.cancel": "取消",
|
|
8
|
+
"buttons.confirm": "确定要执行此操作吗?",
|
|
9
|
+
"views.create.title": "新建{{resource}}",
|
|
10
|
+
"views.create.description": "在 NocoBase 工作区中添加新的{{resource}}。",
|
|
11
|
+
"views.edit.title": "编辑{{resource}}",
|
|
12
|
+
"views.edit.description": "更新此{{resource}},数据一致性由 NocoBase 保障。",
|
|
13
|
+
"views.show.description": "查看记录及其由 NocoBase 管理的数据。",
|
|
14
|
+
"table.empty.title": "暂无数据",
|
|
15
|
+
"table.empty.description": "当前表格中没有可显示的数据。",
|
|
16
|
+
"table.pagination.total": "共 {{count}} 条",
|
|
17
|
+
"table.pagination.pageSize": "每页行数",
|
|
18
|
+
"table.pagination.page": "第 {{current}} 页,共 {{total}} 页",
|
|
19
|
+
"table.pagination.first": "前往第一页",
|
|
20
|
+
"table.pagination.previous": "前往上一页",
|
|
21
|
+
"table.pagination.next": "前往下一页",
|
|
22
|
+
"table.pagination.last": "前往最后一页",
|
|
23
|
+
"notifications.success": "操作成功",
|
|
24
|
+
"notifications.createSuccess": "已成功创建{{resource}}",
|
|
25
|
+
"notifications.editSuccess": "已成功更新{{resource}}",
|
|
26
|
+
"notifications.deleteSuccess": "已成功删除{{resource}}",
|
|
27
|
+
"users.users": "用户",
|
|
28
|
+
Admin: "管理员",
|
|
29
|
+
Root: "超级管理员",
|
|
30
|
+
Member: "成员",
|
|
31
|
+
Anonymous: "匿名用户",
|
|
32
|
+
"Full permissions": "全部权限",
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
export const app = {
|
|
36
|
+
"resources.users": "用户",
|
|
37
|
+
"resources.user": "用户",
|
|
38
|
+
"resources.users.description": "管理可以登录并使用此 NocoBase 应用的用户。",
|
|
39
|
+
"users.fields.id": "ID",
|
|
40
|
+
"users.fields.nickname": "昵称",
|
|
41
|
+
"users.fields.username": "用户名",
|
|
42
|
+
"users.fields.email": "邮箱",
|
|
43
|
+
"users.fields.phone": "手机号",
|
|
44
|
+
"users.fields.password": "密码",
|
|
45
|
+
"users.fields.roles": "角色",
|
|
46
|
+
"users.fields.createdAt": "创建时间",
|
|
47
|
+
"users.fields.updatedAt": "更新时间",
|
|
48
|
+
"users.fields.actions": "操作",
|
|
49
|
+
"users.actions.view": "查看用户",
|
|
50
|
+
"users.actions.edit": "编辑用户",
|
|
51
|
+
"users.actions.delete": "删除用户",
|
|
52
|
+
"users.form.create.submit": "创建用户",
|
|
53
|
+
"users.form.create.submitting": "正在创建...",
|
|
54
|
+
"users.form.edit.submit": "保存修改",
|
|
55
|
+
"users.form.edit.submitting": "正在保存...",
|
|
56
|
+
"users.form.cancel": "取消",
|
|
57
|
+
"users.form.nickname.placeholder": "输入显示名称",
|
|
58
|
+
"users.form.username.placeholder": "输入唯一用户名",
|
|
59
|
+
"users.form.email.placeholder": "输入邮箱地址",
|
|
60
|
+
"users.form.phone.placeholder": "输入手机号",
|
|
61
|
+
"users.form.password.placeholder": "设置初始密码",
|
|
62
|
+
"users.form.password.aiNotice":
|
|
63
|
+
"密码不会包含在 AI 页面上下文中,Form filler 也无法填写密码。",
|
|
64
|
+
"users.validation.username": "请输入用户名",
|
|
65
|
+
"users.validation.email": "请输入邮箱",
|
|
66
|
+
"users.validation.emailFormat": "请输入有效的邮箱地址",
|
|
67
|
+
"users.validation.phone": "请输入手机号",
|
|
68
|
+
"users.validation.password": "请输入密码",
|
|
69
|
+
"users.detail.identity": "身份信息",
|
|
70
|
+
"users.detail.contact": "联系方式",
|
|
71
|
+
"users.detail.access": "访问权限",
|
|
72
|
+
"users.detail.timestamps": "时间信息",
|
|
73
|
+
"users.detail.noRoles": "尚未分配角色",
|
|
74
|
+
"users.detail.unnamed": "未命名用户",
|
|
75
|
+
"users.ai.table": "用户表格",
|
|
76
|
+
"users.ai.detail": "用户详情",
|
|
77
|
+
"users.ai.createForm": "新建用户表单",
|
|
78
|
+
"users.ai.editForm": "编辑用户表单",
|
|
79
|
+
} as const;
|