@wervt/nuxt 0.3.10 → 0.3.12
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/dist/brand.d.ts +6 -0
- package/dist/brand.js +3 -1
- package/dist/content.js +34 -0
- package/dist/module.js +10 -18
- package/dist/runtime/email/components.d.ts +18 -0
- package/dist/runtime/email/components.js +15 -4
- package/dist/runtime/email/invite.d.ts +13 -0
- package/dist/runtime/email/invite.js +30 -14
- package/dist/runtime/i18n.d.ts +3 -0
- package/dist/runtime/i18n.js +3 -0
- package/dist/runtime/shared/i18n.d.ts +28 -0
- package/dist/runtime/shared/i18n.js +54 -0
- package/dist/runtime/shared/messages.d.ts +15 -0
- package/dist/runtime/shared/messages.js +33 -0
- package/package.json +6 -2
package/dist/brand.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Locale } from "./runtime/shared/i18n.js";
|
|
1
2
|
export interface BrandOptions {
|
|
2
3
|
/**
|
|
3
4
|
* The product name the login shows ("Sign in to …"). Defaults to the app's `app.head.title` up
|
|
@@ -30,6 +31,11 @@ export interface BrandOptions {
|
|
|
30
31
|
* them by default. Providers the server has no keys for stay hidden either way.
|
|
31
32
|
*/
|
|
32
33
|
signIn?: SignInMethod[];
|
|
34
|
+
/**
|
|
35
|
+
* The language of the login, the code mail and the consent page (`'de'`). Without it they
|
|
36
|
+
* follow the visitor's browser: German for a German preference, English otherwise.
|
|
37
|
+
*/
|
|
38
|
+
lang?: Locale;
|
|
33
39
|
}
|
|
34
40
|
export declare const SIGN_IN_METHODS: readonly ['google', 'github', 'discord', 'twitch', 'spotify', 'passkey', 'email'];
|
|
35
41
|
export type SignInMethod = (typeof SIGN_IN_METHODS)[number];
|
package/dist/brand.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { headIcon } from "./preview.js";
|
|
2
|
+
import { isLocale } from "./runtime/shared/i18n.js";
|
|
2
3
|
// src/brand.ts
|
|
3
4
|
const SIGN_IN_METHODS = [
|
|
4
5
|
"google",
|
|
@@ -44,7 +45,8 @@ function brandManifest(option, head, app, favicon) {
|
|
|
44
45
|
icon: pick(custom.icon, mailIcon(head) ?? headIcon(head) ?? favicon),
|
|
45
46
|
color: color && HEX_COLOR.test(color) ? color : void 0,
|
|
46
47
|
...custom.securedByWervt === false && { securedByWervt: false },
|
|
47
|
-
...signIn && { signIn }
|
|
48
|
+
...signIn && { signIn },
|
|
49
|
+
...isLocale(custom.lang) && { lang: custom.lang }
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
export { HEX_COLOR, MAIL_IMAGE, SIGN_IN_METHODS, brandManifest, signInMethods, titleName };
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/content.ts
|
|
2
|
+
/**
|
|
3
|
+
* Nuxt Content on wervt: SQLite is a native module (or `node:sqlite`, newer than the isolate's
|
|
4
|
+
* Deno), so production builds keep content in the app's Postgres. `{{DATABASE_URL}}` is expanded
|
|
5
|
+
* at runtime (Nitro's envExpansion).
|
|
6
|
+
*/
|
|
7
|
+
const CONTENT_DATABASE = {
|
|
8
|
+
type: "postgres",
|
|
9
|
+
url: "{{DATABASE_URL}}"
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Our options for @nuxt/content, as a module dependency: applied before Content's setup, which
|
|
13
|
+
* picks its database adapter then, whatever the order of the modules. Setting them in our own
|
|
14
|
+
* setup came too late when Content came first, e.g. from a layer like Docus. An override, not a
|
|
15
|
+
* default: SQLite can't work in the isolate, whatever the app sets.
|
|
16
|
+
*/
|
|
17
|
+
function contentDependency() {
|
|
18
|
+
return {
|
|
19
|
+
optional: true,
|
|
20
|
+
overrides: { database: CONTENT_DATABASE }
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The bundle's manifest (wervt-bundle.json). An app with Nuxt Content needs a database even
|
|
25
|
+
* without Drizzle migrations, which are how the control plane otherwise knows it wants one.
|
|
26
|
+
*/
|
|
27
|
+
function nuxtBundleManifest({ content }) {
|
|
28
|
+
return {
|
|
29
|
+
version: 2,
|
|
30
|
+
framework: "nuxt",
|
|
31
|
+
...content ? { database: true } : {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export { CONTENT_DATABASE, contentDependency, nuxtBundleManifest };
|
package/dist/module.js
CHANGED
|
@@ -2,8 +2,10 @@ import { setupStudioPreview } from "./studio-preview.js";
|
|
|
2
2
|
import { PROCESS_ENV_TYPES } from "./env-types.js";
|
|
3
3
|
import { dependencyNames, ogImageDefaults, ogImageWarning } from "./og-image.js";
|
|
4
4
|
import { iconDefaults, iconSources, iconWarning } from "./icons.js";
|
|
5
|
+
import { CONTENT_DATABASE, contentDependency, nuxtBundleManifest } from "./content.js";
|
|
5
6
|
import { findRouteTools } from "./route-tools.js";
|
|
6
7
|
import { localFiles, previewManifest } from "./preview.js";
|
|
8
|
+
import { LOCALES } from "./runtime/shared/i18n.js";
|
|
7
9
|
import { MAIL_IMAGE, SIGN_IN_METHODS, brandManifest } from "./brand.js";
|
|
8
10
|
import { findShapeRoutes, shapeRoutesTypes, shapesManifest } from "./shapes.js";
|
|
9
11
|
import { BUNDLE_MANIFEST } from "@wervt/core";
|
|
@@ -16,10 +18,6 @@ import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
|
16
18
|
import evlog from "evlog/nuxt";
|
|
17
19
|
import { DENO_INJECT, denoPlugins } from "@wervt/nitro/build";
|
|
18
20
|
// src/module.ts
|
|
19
|
-
const CONTENT_DATABASE = {
|
|
20
|
-
type: "postgres",
|
|
21
|
-
url: "{{DATABASE_URL}}"
|
|
22
|
-
};
|
|
23
21
|
var module_default = defineNuxtModule({
|
|
24
22
|
meta: {
|
|
25
23
|
name: "@wervt/nuxt",
|
|
@@ -35,7 +33,8 @@ var module_default = defineNuxtModule({
|
|
|
35
33
|
"nuxt-og-image": {
|
|
36
34
|
optional: true,
|
|
37
35
|
defaults: ogImageDefaults()
|
|
38
|
-
}
|
|
36
|
+
},
|
|
37
|
+
"@nuxt/content": contentDependency()
|
|
39
38
|
};
|
|
40
39
|
},
|
|
41
40
|
async setup(options, nuxt) {
|
|
@@ -71,16 +70,10 @@ var module_default = defineNuxtModule({
|
|
|
71
70
|
}
|
|
72
71
|
}
|
|
73
72
|
addServerPlugin(resolver.resolve("./runtime/server/agent-token-plugin"));
|
|
74
|
-
const
|
|
75
|
-
if (useContentDatabase) {
|
|
76
|
-
const options = nuxt.options;
|
|
77
|
-
options.content = {
|
|
78
|
-
...options.content,
|
|
79
|
-
database: CONTENT_DATABASE
|
|
80
|
-
};
|
|
81
|
-
}
|
|
73
|
+
const usesContent = () => !nuxt.options.dev && hasNuxtModule("@nuxt/content", nuxt);
|
|
82
74
|
nuxt.hook("nitro:config", (config) => {
|
|
83
75
|
const options = config;
|
|
76
|
+
if (!nuxt.options.dev) options.serveStatic = false;
|
|
84
77
|
options.externals = {
|
|
85
78
|
...options.externals,
|
|
86
79
|
inline: [
|
|
@@ -97,7 +90,7 @@ var module_default = defineNuxtModule({
|
|
|
97
90
|
exclude: [exclude].flat().map((pattern) => typeof pattern === "string" ? pattern : exceptRuntime(pattern))
|
|
98
91
|
}
|
|
99
92
|
};
|
|
100
|
-
if (
|
|
93
|
+
if (usesContent()) {
|
|
101
94
|
options.experimental = {
|
|
102
95
|
...options.experimental,
|
|
103
96
|
envExpansion: true
|
|
@@ -193,10 +186,8 @@ var module_default = defineNuxtModule({
|
|
|
193
186
|
};
|
|
194
187
|
await mkdir(nitroOptions.output.serverDir, { recursive: true });
|
|
195
188
|
await writeFile(join(nitroOptions.output.serverDir, "wervt-tasks.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
framework: "nuxt"
|
|
199
|
-
}, null, 2)}\n`);
|
|
189
|
+
const bundle = nuxtBundleManifest({ content: usesContent() });
|
|
190
|
+
await writeFile(join(dirname(nitroOptions.output.serverDir), BUNDLE_MANIFEST), `${JSON.stringify(bundle, null, 2)}\n`);
|
|
200
191
|
});
|
|
201
192
|
});
|
|
202
193
|
}
|
|
@@ -402,6 +393,7 @@ var module_default = defineNuxtModule({
|
|
|
402
393
|
} : false)) if (!existsSync(join(publicDir, file))) logger.warn(`The login branding names \`${file}\`, which isn't in public/.`);
|
|
403
394
|
if (brand?.icon && !MAIL_IMAGE.test(brand.icon)) logger.warn(`The login branding's icon \`${brand.icon}\` won't show in the sign-in mail: Gmail and Outlook need PNG or JPEG. Set \`wervt.brand.icon\` to one.`);
|
|
404
395
|
if (typeof options.brand === "object" && options.brand.color && !brand?.color) logger.warn("`wervt.brand.color` must be a hex color like `#ff5a1f`; it is ignored.");
|
|
396
|
+
if (typeof options.brand === "object" && options.brand.lang && !brand?.lang) logger.warn(`\`wervt.brand.lang\` must be one of ${LOCALES.join(", ")}; the login follows the browser instead.`);
|
|
405
397
|
if (typeof options.brand === "object" && options.brand.signIn) {
|
|
406
398
|
const unknown = options.brand.signIn.filter((m) => !SIGN_IN_METHODS.includes(m));
|
|
407
399
|
if (unknown.length) logger.warn(`\`wervt.brand.signIn\` ignores ${unknown.map((m) => `\`${m}\``).join(", ")}; it knows ${SIGN_IN_METHODS.join(", ")}.`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Locale } from "../shared/i18n.js";
|
|
1
2
|
import { InjectionKey, PropType } from "vue";
|
|
2
3
|
/** The template's subject, handed to renderEmail() while rendering. */
|
|
3
4
|
export declare const SUBJECT_KEY: InjectionKey<(subject: string) => void>;
|
|
@@ -84,6 +85,14 @@ export declare const WervtEmail: import("vue").DefineComponent<import("vue").Ext
|
|
|
84
85
|
type: PropType<EmailBrand | null>;
|
|
85
86
|
default: null;
|
|
86
87
|
};
|
|
88
|
+
/**
|
|
89
|
+
* The language of the frame's own texts ("Sent by …") and the document. English unless set;
|
|
90
|
+
* the template's own texts are up to the template.
|
|
91
|
+
*/
|
|
92
|
+
lang: {
|
|
93
|
+
type: PropType<Locale>;
|
|
94
|
+
default: string;
|
|
95
|
+
};
|
|
87
96
|
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
88
97
|
[key: string]: any;
|
|
89
98
|
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
@@ -117,12 +126,21 @@ export declare const WervtEmail: import("vue").DefineComponent<import("vue").Ext
|
|
|
117
126
|
type: PropType<EmailBrand | null>;
|
|
118
127
|
default: null;
|
|
119
128
|
};
|
|
129
|
+
/**
|
|
130
|
+
* The language of the frame's own texts ("Sent by …") and the document. English unless set;
|
|
131
|
+
* the template's own texts are up to the template.
|
|
132
|
+
*/
|
|
133
|
+
lang: {
|
|
134
|
+
type: PropType<Locale>;
|
|
135
|
+
default: string;
|
|
136
|
+
};
|
|
120
137
|
}>> & Readonly<{}>, {
|
|
121
138
|
app: string;
|
|
122
139
|
preview: string;
|
|
123
140
|
footer: string;
|
|
124
141
|
reason: string;
|
|
125
142
|
brand: EmailBrand | null;
|
|
143
|
+
lang: "de" | "en";
|
|
126
144
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
127
145
|
export declare const EmailHeading: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
128
146
|
/** A short label above the heading, in the landing page's mono eyebrow style. */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isLocale, translator } from "../shared/i18n.js";
|
|
2
|
+
import { mailMessages } from "../shared/messages.js";
|
|
1
3
|
import { wervtInstance } from "../shared/instance.js";
|
|
2
4
|
import { defineComponent, h, inject } from "vue";
|
|
3
5
|
// src/runtime/email/components.ts
|
|
@@ -102,11 +104,19 @@ const WervtEmail = defineComponent({
|
|
|
102
104
|
brand: {
|
|
103
105
|
type: Object,
|
|
104
106
|
default: null
|
|
107
|
+
},
|
|
108
|
+
/**
|
|
109
|
+
* The language of the frame's own texts ("Sent by …") and the document. English unless set;
|
|
110
|
+
* the template's own texts are up to the template.
|
|
111
|
+
*/
|
|
112
|
+
lang: {
|
|
113
|
+
type: String,
|
|
114
|
+
default: "en"
|
|
105
115
|
}
|
|
106
116
|
},
|
|
107
117
|
setup(props, { slots }) {
|
|
108
118
|
inject(SUBJECT_KEY, void 0)?.(props.subject);
|
|
109
|
-
return () => h("html", { lang: "en" }, [h("head", [
|
|
119
|
+
return () => h("html", { lang: isLocale(props.lang) ? props.lang : "en" }, [h("head", [
|
|
110
120
|
h("meta", { charset: "utf-8" }),
|
|
111
121
|
h("meta", {
|
|
112
122
|
name: "viewport",
|
|
@@ -304,7 +314,8 @@ function accentLine(color) {
|
|
|
304
314
|
}
|
|
305
315
|
function footer(props) {
|
|
306
316
|
const { brand } = props;
|
|
307
|
-
const
|
|
317
|
+
const say = translator(mailMessages, isLocale(props.lang) ? props.lang : "en");
|
|
318
|
+
const sentBy = props.footer || (brand ? say("sentBy", { app: brand.name }) : props.app === "wervt" ? say("sentBy", { app: "wervt" }) : say("sentByVia", { app: props.app }));
|
|
308
319
|
const tile = brand ? mailIcon(brand) : `${t.assets}/mark-tile.png`;
|
|
309
320
|
const small = {
|
|
310
321
|
fontFamily: t.font,
|
|
@@ -367,7 +378,7 @@ function footer(props) {
|
|
|
367
378
|
href: instance.site,
|
|
368
379
|
class: "wv-muted",
|
|
369
380
|
style: link
|
|
370
|
-
}, "
|
|
381
|
+
}, say("securedBy"))] : [h("a", {
|
|
371
382
|
href: instance.site,
|
|
372
383
|
class: "wv-muted",
|
|
373
384
|
style: link
|
|
@@ -375,7 +386,7 @@ function footer(props) {
|
|
|
375
386
|
href: `${instance.docs}/getting-started/introduction`,
|
|
376
387
|
class: "wv-muted",
|
|
377
388
|
style: link
|
|
378
|
-
}, "
|
|
389
|
+
}, say("docs"))] : []])
|
|
379
390
|
])])]);
|
|
380
391
|
}
|
|
381
392
|
function separator() {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Locale } from "../shared/i18n.js";
|
|
2
|
+
import { PropType } from "vue";
|
|
1
3
|
/**
|
|
2
4
|
* An invitation to an app: who invited, where to go, and that signing in with this address is
|
|
3
5
|
* all it takes. Send it with `sendMail({ to, template: InviteEmail, props: { by, app, url } })`.
|
|
@@ -33,6 +35,11 @@ export declare const InviteEmail: import("vue").DefineComponent<import("vue").Ex
|
|
|
33
35
|
type: StringConstructor;
|
|
34
36
|
default: string;
|
|
35
37
|
};
|
|
38
|
+
/** The language of the invitation's own texts; `message`, `action` and `note` stay as given. */
|
|
39
|
+
lang: {
|
|
40
|
+
type: PropType<Locale>;
|
|
41
|
+
default: string;
|
|
42
|
+
};
|
|
36
43
|
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
37
44
|
[key: string]: any;
|
|
38
45
|
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
@@ -66,8 +73,14 @@ export declare const InviteEmail: import("vue").DefineComponent<import("vue").Ex
|
|
|
66
73
|
type: StringConstructor;
|
|
67
74
|
default: string;
|
|
68
75
|
};
|
|
76
|
+
/** The language of the invitation's own texts; `message`, `action` and `note` stay as given. */
|
|
77
|
+
lang: {
|
|
78
|
+
type: PropType<Locale>;
|
|
79
|
+
default: string;
|
|
80
|
+
};
|
|
69
81
|
}>> & Readonly<{}>, {
|
|
70
82
|
message: string;
|
|
71
83
|
action: string;
|
|
72
84
|
note: string;
|
|
85
|
+
lang: "de" | "en";
|
|
73
86
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isLocale, translator } from "../shared/i18n.js";
|
|
2
|
+
import { mailMessages } from "../shared/messages.js";
|
|
1
3
|
import { EmailButton, EmailHeading, EmailMuted, EmailText, WervtEmail } from "./components.js";
|
|
2
4
|
import { defineComponent, h } from "vue";
|
|
3
5
|
// src/runtime/email/invite.ts
|
|
@@ -37,23 +39,37 @@ const InviteEmail = defineComponent({
|
|
|
37
39
|
note: {
|
|
38
40
|
type: String,
|
|
39
41
|
default: ""
|
|
42
|
+
},
|
|
43
|
+
/** The language of the invitation's own texts; `message`, `action` and `note` stay as given. */
|
|
44
|
+
lang: {
|
|
45
|
+
type: String,
|
|
46
|
+
default: "en"
|
|
40
47
|
}
|
|
41
48
|
},
|
|
42
49
|
setup(props) {
|
|
43
|
-
return () =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
h(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
return () => {
|
|
51
|
+
const lang = isLocale(props.lang) ? props.lang : "en";
|
|
52
|
+
const t = translator(mailMessages, lang);
|
|
53
|
+
const who = {
|
|
54
|
+
by: props.by,
|
|
55
|
+
app: props.app
|
|
56
|
+
};
|
|
57
|
+
return h(WervtEmail, {
|
|
58
|
+
subject: t("inviteSubject", who),
|
|
59
|
+
app: props.app,
|
|
60
|
+
preview: props.message || t("invitePreview", who),
|
|
61
|
+
reason: t("inviteReason", who),
|
|
62
|
+
lang
|
|
63
|
+
}, () => [
|
|
64
|
+
h(EmailHeading, { eyebrow: t("inviteEyebrow") }, () => t("inviteHeading", who)),
|
|
65
|
+
h(EmailText, () => [t("inviteText", who), props.message].filter(Boolean).join(" ")),
|
|
66
|
+
h(EmailButton, {
|
|
67
|
+
href: props.url,
|
|
68
|
+
color: "neutral"
|
|
69
|
+
}, () => props.action || t("inviteAction", who)),
|
|
70
|
+
h(EmailMuted, () => props.note || t("inviteNote"))
|
|
71
|
+
]);
|
|
72
|
+
};
|
|
57
73
|
}
|
|
58
74
|
});
|
|
59
75
|
export { InviteEmail };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Catalog, DEFAULT_LOCALE, LOCALES, Locale, Messages, Translate, catalogProblems, defineMessages, isLocale, negotiateLocale, translator } from "./shared/i18n.js";
|
|
2
|
+
import { mailMessages } from "./shared/messages.js";
|
|
3
|
+
export { Catalog, DEFAULT_LOCALE, LOCALES, Locale, Messages, Translate, catalogProblems, defineMessages, isLocale, mailMessages, negotiateLocale, translator };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { DEFAULT_LOCALE, LOCALES, catalogProblems, defineMessages, isLocale, negotiateLocale, translator } from "./shared/i18n.js";
|
|
2
|
+
import { mailMessages } from "./shared/messages.js";
|
|
3
|
+
export { DEFAULT_LOCALE, LOCALES, catalogProblems, defineMessages, isLocale, mailMessages, negotiateLocale, translator };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const LOCALES: readonly ['en', 'de'];
|
|
2
|
+
export type Locale = (typeof LOCALES)[number];
|
|
3
|
+
export declare const DEFAULT_LOCALE: Locale;
|
|
4
|
+
export declare const isLocale: (value: unknown) => value is Locale;
|
|
5
|
+
/**
|
|
6
|
+
* The best supported language for an `Accept-Language` header: the highest-ranked one there is a
|
|
7
|
+
* catalog for, English when there's none.
|
|
8
|
+
*/
|
|
9
|
+
export declare function negotiateLocale(header: string | null | undefined): Locale;
|
|
10
|
+
/** One locale's texts, with the keys of the English catalog. */
|
|
11
|
+
export type Catalog<M> = { readonly [K in keyof M]: string; };
|
|
12
|
+
/** Every locale's catalog; English is the source of keys and placeholders. */
|
|
13
|
+
export type Messages<M> = {
|
|
14
|
+
en: M;
|
|
15
|
+
} & { [L in Exclude<Locale, 'en'>]: Catalog<M>; };
|
|
16
|
+
/** Checks the catalogs against English at compile time; returns them unchanged. */
|
|
17
|
+
export declare const defineMessages: <const M extends Record<string, string>>(messages: Messages<M>) => Messages<M>;
|
|
18
|
+
type Placeholders<S> = S extends `${string}{${infer K}}${infer Rest}` ? K | Placeholders<Rest> : never;
|
|
19
|
+
type Params<S> = [Placeholders<S>] extends [never] ? [] : [params: Record<Placeholders<S>, string | number>];
|
|
20
|
+
export type Translate<M> = <K extends keyof M & string>(key: K, ...params: Params<M[K]>) => string;
|
|
21
|
+
/** `t(key, params)` for one locale. */
|
|
22
|
+
export declare function translator<M extends Record<string, string>>(messages: Messages<M>, locale: Locale): Translate<M>;
|
|
23
|
+
/**
|
|
24
|
+
* What's wrong with the catalogs, for tests: keys English has and another locale lacks (or the
|
|
25
|
+
* other way round), empty texts and placeholders that differ from English.
|
|
26
|
+
*/
|
|
27
|
+
export declare function catalogProblems(messages: Messages<Record<string, string>>): string[];
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/runtime/shared/i18n.ts
|
|
2
|
+
const LOCALES = ["en", "de"];
|
|
3
|
+
const DEFAULT_LOCALE = "en";
|
|
4
|
+
const isLocale = (value) => typeof value === "string" && LOCALES.includes(value);
|
|
5
|
+
/**
|
|
6
|
+
* The best supported language for an `Accept-Language` header: the highest-ranked one there is a
|
|
7
|
+
* catalog for, English when there's none.
|
|
8
|
+
*/
|
|
9
|
+
function negotiateLocale(header) {
|
|
10
|
+
if (!header) return "en";
|
|
11
|
+
return header.split(",").map((part, index) => {
|
|
12
|
+
const [tag = "", ...params] = part.trim().split(";");
|
|
13
|
+
const q = params.map((p) => p.trim()).find((p) => p.startsWith("q="));
|
|
14
|
+
return {
|
|
15
|
+
lang: tag.trim().toLowerCase().split("-")[0],
|
|
16
|
+
q: q ? Number(q.slice(2)) : 1,
|
|
17
|
+
index
|
|
18
|
+
};
|
|
19
|
+
}).filter((entry) => entry.q > 0).sort((a, b) => b.q - a.q || a.index - b.index).map((entry) => entry.lang).find(isLocale) ?? "en";
|
|
20
|
+
}
|
|
21
|
+
/** Checks the catalogs against English at compile time; returns them unchanged. */
|
|
22
|
+
const defineMessages = (messages) => messages;
|
|
23
|
+
const fill = (text, params) => params ? text.replace(/\{(\w+)\}/g, (all, key) => String(params[key] ?? all)) : text;
|
|
24
|
+
/** `t(key, params)` for one locale. */
|
|
25
|
+
function translator(messages, locale) {
|
|
26
|
+
const catalog = messages[locale];
|
|
27
|
+
return (key, ...params) => fill(catalog[key] ?? messages.en[key], params[0]);
|
|
28
|
+
}
|
|
29
|
+
const placeholders = (text) => [...text.matchAll(/\{(\w+)\}/g)].map((m) => m[1]).sort();
|
|
30
|
+
/**
|
|
31
|
+
* What's wrong with the catalogs, for tests: keys English has and another locale lacks (or the
|
|
32
|
+
* other way round), empty texts and placeholders that differ from English.
|
|
33
|
+
*/
|
|
34
|
+
function catalogProblems(messages) {
|
|
35
|
+
const problems = [];
|
|
36
|
+
const source = messages.en;
|
|
37
|
+
for (const locale of LOCALES) {
|
|
38
|
+
const catalog = messages[locale];
|
|
39
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(source), ...Object.keys(catalog)])) {
|
|
40
|
+
const text = catalog[key];
|
|
41
|
+
if (!(key in source)) problems.push(`${locale}.${key}: not in en`);
|
|
42
|
+
else if (text === void 0) problems.push(`${locale}.${key}: missing`);
|
|
43
|
+
else if (!text.trim()) problems.push(`${locale}.${key}: empty`);
|
|
44
|
+
else {
|
|
45
|
+
const own = placeholders(text).join(",");
|
|
46
|
+
const expected = placeholders(source[key]).join(",");
|
|
47
|
+
if (own !== expected) problems.push(`${locale}.${key}: placeholders ${fmt(own)} differ from en ${fmt(expected)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return problems;
|
|
52
|
+
}
|
|
53
|
+
const fmt = (list) => list.split(",").filter(Boolean).map((p) => `{${p}}`).join(" ");
|
|
54
|
+
export { DEFAULT_LOCALE, LOCALES, catalogProblems, defineMessages, isLocale, negotiateLocale, translator };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Messages } from "./i18n.js";
|
|
2
|
+
export declare const mailMessages: Messages<{
|
|
3
|
+
readonly sentBy: "Sent by {app}";
|
|
4
|
+
readonly sentByVia: "Sent by {app} via wervt";
|
|
5
|
+
readonly securedBy: "Secured by wervt";
|
|
6
|
+
readonly docs: "Docs";
|
|
7
|
+
readonly inviteSubject: "{by} invited you to {app}";
|
|
8
|
+
readonly invitePreview: "Join {app}";
|
|
9
|
+
readonly inviteReason: "You received this email because {by} invited this address.";
|
|
10
|
+
readonly inviteEyebrow: "Invitation";
|
|
11
|
+
readonly inviteHeading: "You're invited to {app}";
|
|
12
|
+
readonly inviteText: "{by} invited you to {app}.";
|
|
13
|
+
readonly inviteAction: "Open {app}";
|
|
14
|
+
readonly inviteNote: "Sign in with this email address to get in.";
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineMessages } from "./i18n.js";
|
|
2
|
+
// src/runtime/shared/messages.ts
|
|
3
|
+
const mailMessages = defineMessages({
|
|
4
|
+
en: {
|
|
5
|
+
sentBy: "Sent by {app}",
|
|
6
|
+
sentByVia: "Sent by {app} via wervt",
|
|
7
|
+
securedBy: "Secured by wervt",
|
|
8
|
+
docs: "Docs",
|
|
9
|
+
inviteSubject: "{by} invited you to {app}",
|
|
10
|
+
invitePreview: "Join {app}",
|
|
11
|
+
inviteReason: "You received this email because {by} invited this address.",
|
|
12
|
+
inviteEyebrow: "Invitation",
|
|
13
|
+
inviteHeading: "You're invited to {app}",
|
|
14
|
+
inviteText: "{by} invited you to {app}.",
|
|
15
|
+
inviteAction: "Open {app}",
|
|
16
|
+
inviteNote: "Sign in with this email address to get in."
|
|
17
|
+
},
|
|
18
|
+
de: {
|
|
19
|
+
sentBy: "Gesendet von {app}",
|
|
20
|
+
sentByVia: "Gesendet von {app} über wervt",
|
|
21
|
+
securedBy: "Gesichert durch wervt",
|
|
22
|
+
docs: "Doku",
|
|
23
|
+
inviteSubject: "{by} hat dich zu {app} eingeladen",
|
|
24
|
+
invitePreview: "Mach mit bei {app}",
|
|
25
|
+
inviteReason: "Du bekommst diese E-Mail, weil {by} diese Adresse eingeladen hat.",
|
|
26
|
+
inviteEyebrow: "Einladung",
|
|
27
|
+
inviteHeading: "Du bist zu {app} eingeladen",
|
|
28
|
+
inviteText: "{by} hat dich zu {app} eingeladen.",
|
|
29
|
+
inviteAction: "{app} öffnen",
|
|
30
|
+
inviteNote: "Melde dich mit dieser E-Mail-Adresse an, um reinzukommen."
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
export { mailMessages };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wervt/nuxt",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Nuxt module for apps hosted on wervt: database, live data, MCP and runtime setup.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"drizzle",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
"types": "./dist/runtime/email/index.d.ts",
|
|
38
38
|
"default": "./dist/runtime/email/index.js"
|
|
39
39
|
},
|
|
40
|
+
"./i18n": {
|
|
41
|
+
"types": "./dist/runtime/i18n.d.ts",
|
|
42
|
+
"default": "./dist/runtime/i18n.js"
|
|
43
|
+
},
|
|
40
44
|
"./anthropic": {
|
|
41
45
|
"types": "./dist/runtime/server/anthropic.d.ts",
|
|
42
46
|
"default": "./dist/runtime/server/anthropic.js"
|
|
@@ -51,7 +55,7 @@
|
|
|
51
55
|
"@valibot/to-json-schema": "^1.8.0",
|
|
52
56
|
"@wervt/client": "^0.0.2",
|
|
53
57
|
"@wervt/core": "^0.0.2",
|
|
54
|
-
"@wervt/nitro": "^0.0.
|
|
58
|
+
"@wervt/nitro": "^0.0.3",
|
|
55
59
|
"@wervt/platform": "^0.0.2",
|
|
56
60
|
"aws4fetch": "^1.0.20",
|
|
57
61
|
"evlog": "^2.34.0",
|