talizen 0.1.4 → 0.2.0
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/core.d.ts +9 -1
- package/core.js +30 -4
- package/form.js +15 -4
- package/i18n.d.ts +55 -0
- package/i18n.js +62 -0
- package/index.d.ts +27 -0
- package/index.js +1 -0
- package/package.json +9 -1
package/core.d.ts
CHANGED
|
@@ -36,6 +36,14 @@ export declare class TalizenHttpError extends Error {
|
|
|
36
36
|
readonly statusText: string;
|
|
37
37
|
readonly body: string;
|
|
38
38
|
readonly bodyJson: TalizenErrorBody | undefined;
|
|
39
|
-
|
|
39
|
+
readonly method: string | undefined;
|
|
40
|
+
readonly url: string | undefined;
|
|
41
|
+
constructor(status: number, statusText: string, body: string, bodyJson?: TalizenErrorBody, request?: TalizenErrorRequest);
|
|
42
|
+
}
|
|
43
|
+
export interface TalizenErrorRequest {
|
|
44
|
+
method?: string;
|
|
45
|
+
url?: string;
|
|
40
46
|
}
|
|
41
47
|
export declare function requestJson<T>(path: string, init?: RequestInit, config?: TalizenRequestOptions): Promise<T>;
|
|
48
|
+
export declare function stripUrlQuery(url: string): string;
|
|
49
|
+
export declare function normalizeRequestMethod(method: string | undefined): string;
|
package/core.js
CHANGED
|
@@ -38,13 +38,17 @@ export class TalizenHttpError extends Error {
|
|
|
38
38
|
statusText;
|
|
39
39
|
body;
|
|
40
40
|
bodyJson;
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
method;
|
|
42
|
+
url;
|
|
43
|
+
constructor(status, statusText, body, bodyJson, request) {
|
|
44
|
+
super(formatTalizenRequestErrorMessage(status, statusText, body, request));
|
|
43
45
|
this.name = "TalizenHttpError";
|
|
44
46
|
this.status = status;
|
|
45
47
|
this.statusText = statusText;
|
|
46
48
|
this.body = body;
|
|
47
49
|
this.bodyJson = bodyJson;
|
|
50
|
+
this.method = request?.method;
|
|
51
|
+
this.url = request?.url;
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
export async function requestJson(path, init, config) {
|
|
@@ -58,7 +62,12 @@ export async function requestJson(path, init, config) {
|
|
|
58
62
|
if (init?.body != null && !headers.has("content-type")) {
|
|
59
63
|
headers.set("content-type", "application/json");
|
|
60
64
|
}
|
|
61
|
-
const
|
|
65
|
+
const requestUrl = buildTalizenUrl(path, resolved);
|
|
66
|
+
const request = {
|
|
67
|
+
method: normalizeRequestMethod(init?.method),
|
|
68
|
+
url: stripUrlQuery(requestUrl),
|
|
69
|
+
};
|
|
70
|
+
const response = await resolved.fetch(requestUrl, {
|
|
62
71
|
...init,
|
|
63
72
|
headers,
|
|
64
73
|
signal: resolved.signal,
|
|
@@ -66,7 +75,7 @@ export async function requestJson(path, init, config) {
|
|
|
66
75
|
if (!response.ok) {
|
|
67
76
|
const text = await response.text();
|
|
68
77
|
const bodyJson = parseTalizenErrorBody(text);
|
|
69
|
-
throw new TalizenHttpError(response.status, response.statusText, text, bodyJson);
|
|
78
|
+
throw new TalizenHttpError(response.status, response.statusText, text, bodyJson, request);
|
|
70
79
|
}
|
|
71
80
|
const text = await response.text();
|
|
72
81
|
if (text === "") {
|
|
@@ -74,6 +83,23 @@ export async function requestJson(path, init, config) {
|
|
|
74
83
|
}
|
|
75
84
|
return JSON.parse(text);
|
|
76
85
|
}
|
|
86
|
+
export function stripUrlQuery(url) {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = new URL(url);
|
|
89
|
+
parsed.search = "";
|
|
90
|
+
return parsed.toString();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return url.split("?")[0] ?? url;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function normalizeRequestMethod(method) {
|
|
97
|
+
return (method ?? "GET").toUpperCase();
|
|
98
|
+
}
|
|
99
|
+
function formatTalizenRequestErrorMessage(status, statusText, body, request) {
|
|
100
|
+
const requestText = [request?.method, request?.url].filter(Boolean).join(" ");
|
|
101
|
+
return `Talizen request failed: ${requestText} ${status} ${statusText} ${body}`.replace(/\s+/g, " ").trim();
|
|
102
|
+
}
|
|
77
103
|
function getDefaultBaseUrl() {
|
|
78
104
|
if (typeof window !== "undefined" && window.location?.origin) {
|
|
79
105
|
return window.location.origin;
|
package/form.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildCaptchaAnswer, runCaptchaVerification, shouldRetryCaptchaSubmit, } from "./captcha-ui.js";
|
|
2
|
-
import { requestJson, resolveTalizenConfig, TalizenHttpError } from "./core.js";
|
|
2
|
+
import { normalizeRequestMethod, requestJson, resolveTalizenConfig, stripUrlQuery, TalizenHttpError, } from "./core.js";
|
|
3
3
|
export async function submitForm(keyOrToken, payload, options) {
|
|
4
4
|
const formKey = getFormKey(keyOrToken);
|
|
5
5
|
const data = await replaceFiles(formKey, payload, "", options);
|
|
@@ -116,6 +116,10 @@ async function uploadToSignedUrl(target, file, fieldKey, options) {
|
|
|
116
116
|
const resolved = resolveTalizenConfig(options);
|
|
117
117
|
const headers = new Headers();
|
|
118
118
|
const body = file;
|
|
119
|
+
const request = {
|
|
120
|
+
method: normalizeRequestMethod("PUT"),
|
|
121
|
+
url: stripUrlQuery(target.uploadUrl),
|
|
122
|
+
};
|
|
119
123
|
if (file.type) {
|
|
120
124
|
headers.set("content-type", file.type);
|
|
121
125
|
}
|
|
@@ -127,7 +131,7 @@ async function uploadToSignedUrl(target, file, fieldKey, options) {
|
|
|
127
131
|
});
|
|
128
132
|
if (!response.ok) {
|
|
129
133
|
const text = await response.text();
|
|
130
|
-
throw new Error(
|
|
134
|
+
throw new Error(formatUploadError(request, `${response.status} ${response.statusText} ${text}`));
|
|
131
135
|
}
|
|
132
136
|
}
|
|
133
137
|
function uploadWithXhr(target, file, fieldKey, options) {
|
|
@@ -135,6 +139,10 @@ function uploadWithXhr(target, file, fieldKey, options) {
|
|
|
135
139
|
const xhr = new XMLHttpRequest();
|
|
136
140
|
const signal = options.signal;
|
|
137
141
|
const body = file;
|
|
142
|
+
const request = {
|
|
143
|
+
method: normalizeRequestMethod("PUT"),
|
|
144
|
+
url: stripUrlQuery(target.uploadUrl ?? ""),
|
|
145
|
+
};
|
|
138
146
|
xhr.open("PUT", target.uploadUrl ?? "");
|
|
139
147
|
if (file.type) {
|
|
140
148
|
xhr.setRequestHeader("content-type", file.type);
|
|
@@ -150,11 +158,11 @@ function uploadWithXhr(target, file, fieldKey, options) {
|
|
|
150
158
|
resolve();
|
|
151
159
|
return;
|
|
152
160
|
}
|
|
153
|
-
reject(new Error(
|
|
161
|
+
reject(new Error(formatUploadError(request, `${xhr.status} ${xhr.statusText} ${xhr.responseText}`)));
|
|
154
162
|
};
|
|
155
163
|
xhr.onerror = () => {
|
|
156
164
|
cleanup();
|
|
157
|
-
reject(new Error("
|
|
165
|
+
reject(new Error(formatUploadError(request, "network error")));
|
|
158
166
|
};
|
|
159
167
|
xhr.onabort = () => {
|
|
160
168
|
cleanup();
|
|
@@ -166,6 +174,9 @@ function uploadWithXhr(target, file, fieldKey, options) {
|
|
|
166
174
|
xhr.send(body);
|
|
167
175
|
});
|
|
168
176
|
}
|
|
177
|
+
function formatUploadError(request, detail) {
|
|
178
|
+
return `Talizen file upload failed: ${request.method} ${request.url} ${detail}`.replace(/\s+/g, " ").trim();
|
|
179
|
+
}
|
|
169
180
|
async function sha256(file) {
|
|
170
181
|
const subtle = globalThis.crypto?.subtle;
|
|
171
182
|
if (subtle == null) {
|
package/i18n.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* 当前语言运行时信息。由 Talizen 渲染引擎注入到 `globalThis.__TALIZEN_I18N__`
|
|
4
|
+
* (SSR 与浏览器端注入同一份,避免 hydration 不一致)。站点未开启多语言时为空。
|
|
5
|
+
*/
|
|
6
|
+
export interface LocaleRuntime {
|
|
7
|
+
/** 当前生效语言;站点未配置多语言时为 ""。 */
|
|
8
|
+
locale: string;
|
|
9
|
+
/** 站点配置的全部语言。 */
|
|
10
|
+
locales: string[];
|
|
11
|
+
/** 默认语言(其 URL 无前缀)。 */
|
|
12
|
+
defaultLocale: string;
|
|
13
|
+
}
|
|
14
|
+
declare global {
|
|
15
|
+
var __TALIZEN_I18N__: Partial<LocaleRuntime> | undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 读取当前语言运行时信息(当前语言 / 全部语言 / 默认语言)。SSR 与客户端返回一致。
|
|
19
|
+
* 站点未开启多语言时 `locale === ""`。
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* const { locale, locales, defaultLocale } = useLocale()
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function useLocale(): LocaleRuntime;
|
|
26
|
+
/**
|
|
27
|
+
* 给站内路径加语言前缀(默认语言不加前缀)。不传 `locale` 时使用当前语言。
|
|
28
|
+
* 站外链接(协议 / 协议相对 URL)与页内锚点(`#...`)原样返回。
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* localizedPath("/blog") // zh 页面 → "/zh/blog";默认语言 → "/blog"
|
|
32
|
+
* localizedPath("/about", "ja") // → "/ja/about"
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function localizedPath(path: string, locale?: string): string;
|
|
36
|
+
/** 记住用户显式语言选择的 Cookie 名(检测优先级:Cookie > Accept-Language > 默认)。 */
|
|
37
|
+
export declare const LOCALE_COOKIE = "CREGHT_LOCALE";
|
|
38
|
+
export type LinkProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
39
|
+
href: string;
|
|
40
|
+
/**
|
|
41
|
+
* 指定则表示「切换到该语言」:`href` 用该语言前缀,并在点击时写 `CREGHT_LOCALE`
|
|
42
|
+
* Cookie 记住选择。不传则沿用当前语言(普通站内链接)。
|
|
43
|
+
*/
|
|
44
|
+
locale?: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* 语言感知的站内链接,替代裸 `<a>`:自动给 `href` 补当前语言前缀(默认语言不补)。
|
|
48
|
+
* 传 `locale` 表示语言切换(用该语言前缀,且点击时写 `CREGHT_LOCALE` 记住选择)。
|
|
49
|
+
*
|
|
50
|
+
* ```tsx
|
|
51
|
+
* <Link href="/blog">Blog</Link> // 跟随当前语言:zh 页面 → /zh/blog
|
|
52
|
+
* <Link href="/" locale="ja">日本語</Link> // 切到日文并记住
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function Link(props: LinkProps): React.ReactElement;
|
package/i18n.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
function readLocaleRuntime() {
|
|
3
|
+
const d = (typeof globalThis !== "undefined" ? globalThis.__TALIZEN_I18N__ : undefined) ?? {};
|
|
4
|
+
const locales = Array.isArray(d.locales) ? d.locales.filter((l) => typeof l === "string") : [];
|
|
5
|
+
return {
|
|
6
|
+
locale: typeof d.locale === "string" ? d.locale : "",
|
|
7
|
+
locales,
|
|
8
|
+
defaultLocale: typeof d.defaultLocale === "string" ? d.defaultLocale : (locales[0] ?? ""),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 读取当前语言运行时信息(当前语言 / 全部语言 / 默认语言)。SSR 与客户端返回一致。
|
|
13
|
+
* 站点未开启多语言时 `locale === ""`。
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const { locale, locales, defaultLocale } = useLocale()
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function useLocale() {
|
|
20
|
+
return readLocaleRuntime();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 给站内路径加语言前缀(默认语言不加前缀)。不传 `locale` 时使用当前语言。
|
|
24
|
+
* 站外链接(协议 / 协议相对 URL)与页内锚点(`#...`)原样返回。
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* localizedPath("/blog") // zh 页面 → "/zh/blog";默认语言 → "/blog"
|
|
28
|
+
* localizedPath("/about", "ja") // → "/ja/about"
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function localizedPath(path, locale) {
|
|
32
|
+
if (/^([a-z][a-z0-9+.-]*:)?\/\//i.test(path) || path.startsWith("#"))
|
|
33
|
+
return path;
|
|
34
|
+
const rt = readLocaleRuntime();
|
|
35
|
+
const target = locale ?? rt.locale;
|
|
36
|
+
if (!target || target === rt.defaultLocale)
|
|
37
|
+
return path;
|
|
38
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
39
|
+
return p === "/" ? `/${target}` : `/${target}${p}`;
|
|
40
|
+
}
|
|
41
|
+
/** 记住用户显式语言选择的 Cookie 名(检测优先级:Cookie > Accept-Language > 默认)。 */
|
|
42
|
+
export const LOCALE_COOKIE = "CREGHT_LOCALE";
|
|
43
|
+
/**
|
|
44
|
+
* 语言感知的站内链接,替代裸 `<a>`:自动给 `href` 补当前语言前缀(默认语言不补)。
|
|
45
|
+
* 传 `locale` 表示语言切换(用该语言前缀,且点击时写 `CREGHT_LOCALE` 记住选择)。
|
|
46
|
+
*
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <Link href="/blog">Blog</Link> // 跟随当前语言:zh 页面 → /zh/blog
|
|
49
|
+
* <Link href="/" locale="ja">日本語</Link> // 切到日文并记住
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function Link(props) {
|
|
53
|
+
const { href, locale, onClick, ...rest } = props;
|
|
54
|
+
const finalHref = localizedPath(href, locale);
|
|
55
|
+
const handleClick = (event) => {
|
|
56
|
+
if (locale && typeof document !== "undefined") {
|
|
57
|
+
document.cookie = `${LOCALE_COOKIE}=${encodeURIComponent(locale)}; path=/; max-age=31536000; samesite=lax`;
|
|
58
|
+
}
|
|
59
|
+
onClick?.(event);
|
|
60
|
+
};
|
|
61
|
+
return React.createElement("a", { ...rest, href: finalHref, onClick: handleClick });
|
|
62
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from "./core.js";
|
|
|
2
2
|
export * from "./cms.js";
|
|
3
3
|
export * from "./captcha-ui.js";
|
|
4
4
|
export * from "./form.js";
|
|
5
|
+
export * from "./i18n.js";
|
|
5
6
|
type OneOrMany<T> = T | Array<T>;
|
|
6
7
|
export interface MetadataTitle {
|
|
7
8
|
default?: string;
|
|
@@ -72,3 +73,29 @@ export interface Metadata {
|
|
|
72
73
|
openGraph?: OpenGraphMetadata | null;
|
|
73
74
|
icons?: MetadataIcons | null;
|
|
74
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* A single site-level redirect rule, aligned with Next.js `redirects()` semantics.
|
|
78
|
+
*
|
|
79
|
+
* Declare rules in the `redirects` array of `talizen.config.ts`. A matching
|
|
80
|
+
* request is redirected before the page renders, so redirects take precedence
|
|
81
|
+
* over pages and `/public` files.
|
|
82
|
+
*/
|
|
83
|
+
export interface Redirect {
|
|
84
|
+
/**
|
|
85
|
+
* Source path to match. Supports exact matches (`/old-page`) and a trailing
|
|
86
|
+
* wildcard segment (`/blog/*`).
|
|
87
|
+
*/
|
|
88
|
+
source: string;
|
|
89
|
+
/**
|
|
90
|
+
* Redirect target. May be an internal path (`/new-page`), a wildcard
|
|
91
|
+
* backreference (`/posts/*`), an absolute URL (`https://example.com/x`), or a
|
|
92
|
+
* protocol-relative URL (`//example.com/x`). Internal paths keep the original
|
|
93
|
+
* query string.
|
|
94
|
+
*/
|
|
95
|
+
destination: string;
|
|
96
|
+
/**
|
|
97
|
+
* `true` issues a 308 permanent redirect (best for SEO); `false` issues a 307
|
|
98
|
+
* temporary redirect.
|
|
99
|
+
*/
|
|
100
|
+
permanent: boolean;
|
|
101
|
+
}
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talizen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Talizen frontend SDK types for cms, form and core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/talizen/talizen.git"
|
|
10
|
+
},
|
|
7
11
|
"sideEffects": false,
|
|
8
12
|
"main": "./index.js",
|
|
9
13
|
"types": "./index.d.ts",
|
|
@@ -27,6 +31,10 @@
|
|
|
27
31
|
"./form": {
|
|
28
32
|
"types": "./form.d.ts",
|
|
29
33
|
"import": "./form.js"
|
|
34
|
+
},
|
|
35
|
+
"./i18n": {
|
|
36
|
+
"types": "./i18n.d.ts",
|
|
37
|
+
"import": "./i18n.js"
|
|
30
38
|
}
|
|
31
39
|
}
|
|
32
40
|
}
|