@uipkge/nuxt 0.1.13 → 0.1.15
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 +8 -8
- package/dist/module.json +1 -1
- package/dist/module.mjs +17 -11
- package/dist/runtime/composables/useI18n.d.ts +4 -2
- package/dist/runtime/composables/useI18n.js +2 -2
- package/dist/runtime/plugin.client.js +28 -7
- package/dist/runtime/plugin.js +9 -2
- package/dist/runtime/plugin.suppress-warnings.js +1 -1
- package/package.json +18 -14
- package/dist/runtime/server/sync.d.ts +0 -2
- package/dist/runtime/server/sync.js +0 -48
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Nuxt module for [i18now](https://i18now.com) — loads translations from the i18
|
|
|
8
8
|
- Reloads translations on locale switch (client-side)
|
|
9
9
|
- In development: detects missing translation keys and syncs them to i18now automatically
|
|
10
10
|
- Suppresses vue-i18n missing-key warnings in dev — missing keys are expected until published
|
|
11
|
-
- API key is
|
|
11
|
+
- API key is only present in development builds — never shipped to production
|
|
12
12
|
- Zero bundle impact in production — dev-only code is tree-shaken at build time
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
@@ -47,7 +47,7 @@ export default defineNuxtConfig({
|
|
|
47
47
|
| Option | Type | Default | Description |
|
|
48
48
|
|---------------|------------|----------------------------|-----------------------------------------------------------------------------|
|
|
49
49
|
| `projectId` | `string` | — | **Required.** Your i18now project ID. |
|
|
50
|
-
| `apiKey` | `string` | — | **Required.** Your i18now API key.
|
|
50
|
+
| `apiKey` | `string` | — | **Required.** Your i18now API key. Only used in dev builds. |
|
|
51
51
|
| `host` | `string` | `https://i18now.com` | i18now server URL. Set to `http://localhost:3220` for local development. |
|
|
52
52
|
| `cdnUrl` | `string` | `https://cdn.i18now.com` | CDN base URL where published translations are served. |
|
|
53
53
|
| `environment` | `string` | `'dev'` | Which published environment to load translations from (`dev`, `stage`, `prod`). |
|
|
@@ -71,24 +71,24 @@ The server plugin fetches the published translation JSON from the CDN (`{cdnUrl}
|
|
|
71
71
|
|
|
72
72
|
### Development
|
|
73
73
|
|
|
74
|
-
The client plugin intercepts every `$t()` and `
|
|
74
|
+
The client plugin intercepts every `$t()` and `useI18now().t()` call. When a key is missing from the CDN snapshot it batches that key (with its default value) and POSTs it directly to the i18now sync endpoint.
|
|
75
75
|
|
|
76
76
|
```
|
|
77
77
|
Browser $t('key', 'Default text')
|
|
78
78
|
→ client plugin detects missing key
|
|
79
|
-
→ POST /api/
|
|
80
|
-
→
|
|
81
|
-
→ POST i18now /api/v1/projects/:id/sync
|
|
79
|
+
→ POST i18now /api/v1/projects/:id/sync { apiKey, keys }
|
|
80
|
+
→ i18now creates the key with source value
|
|
82
81
|
```
|
|
83
82
|
|
|
84
|
-
The `
|
|
83
|
+
The `useI18now` composable is auto-imported by the module. Use it in place of `useI18n` from vue-i18n — it adds sync detection on top of the standard `t()` function.
|
|
85
84
|
|
|
86
85
|
Vue-i18n's missing-key and fallback warnings are suppressed automatically in dev. Keys are expected to be absent until they are published from the i18now dashboard, at which point they load from CDN and no longer trigger warnings.
|
|
87
86
|
|
|
88
87
|
## Security
|
|
89
88
|
|
|
90
|
-
- `apiKey` is
|
|
89
|
+
- `apiKey` is only included in `runtimeConfig.public` during **development** builds (`nuxt.options.dev`). In production, the API key is never serialised into the client bundle or `__NUXT__` payload.
|
|
91
90
|
- The `syncIn` option defaults to `['development']`. **Never add `'production'` or `'staging'`** — doing so would expose your API key to end users and flood your project with traffic.
|
|
91
|
+
- The client sync plugin is only registered in dev mode — it is completely absent from production builds.
|
|
92
92
|
|
|
93
93
|
## Local development against a local i18now server
|
|
94
94
|
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -21,8 +21,11 @@ const module$1 = defineNuxtModule({
|
|
|
21
21
|
if (!options.apiKey) {
|
|
22
22
|
console.warn("[i18now] apiKey is required. The module will not work until it is set.");
|
|
23
23
|
}
|
|
24
|
+
if (options.cdnUrl && !options.cdnUrl.startsWith("http")) {
|
|
25
|
+
console.warn("[i18now] cdnUrl should be a valid HTTP(S) URL. Got: " + options.cdnUrl);
|
|
26
|
+
}
|
|
24
27
|
const dangerousEnvs = (options.syncIn ?? []).filter(
|
|
25
|
-
(e) => ["production", "staging", "prod", "stage"].includes(e)
|
|
28
|
+
(e) => ["production", "staging", "prod", "stage"].includes(e.toLowerCase())
|
|
26
29
|
);
|
|
27
30
|
if (dangerousEnvs.length > 0) {
|
|
28
31
|
console.error(
|
|
@@ -31,17 +34,26 @@ const module$1 = defineNuxtModule({
|
|
|
31
34
|
}
|
|
32
35
|
nuxt.options.runtimeConfig.public.i18now = {
|
|
33
36
|
projectId: options.projectId,
|
|
34
|
-
apiKey: options.apiKey,
|
|
35
|
-
host: options.host,
|
|
36
37
|
cdnUrl: options.cdnUrl,
|
|
37
38
|
environment: options.environment,
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
locale: options.locale,
|
|
40
|
+
// Dev-only fields — only included when the sync client plugin is active.
|
|
41
|
+
// In production builds these are undefined and the client plugin is not registered.
|
|
42
|
+
...nuxt.options.dev ? {
|
|
43
|
+
apiKey: options.apiKey,
|
|
44
|
+
host: options.host,
|
|
45
|
+
syncIn: options.syncIn
|
|
46
|
+
} : {}
|
|
40
47
|
};
|
|
41
48
|
addPlugin({
|
|
42
49
|
src: resolver.resolve("./runtime/plugin"),
|
|
43
50
|
mode: "server"
|
|
44
51
|
});
|
|
52
|
+
addImports({
|
|
53
|
+
name: "useI18now",
|
|
54
|
+
as: "useI18now",
|
|
55
|
+
from: resolver.resolve("./runtime/composables/useI18n")
|
|
56
|
+
});
|
|
45
57
|
if (nuxt.options.dev) {
|
|
46
58
|
addPlugin({
|
|
47
59
|
src: resolver.resolve("./runtime/plugin.client"),
|
|
@@ -50,12 +62,6 @@ const module$1 = defineNuxtModule({
|
|
|
50
62
|
addPlugin({
|
|
51
63
|
src: resolver.resolve("./runtime/plugin.suppress-warnings")
|
|
52
64
|
});
|
|
53
|
-
addImports({
|
|
54
|
-
name: "useI18n",
|
|
55
|
-
as: "useI18n",
|
|
56
|
-
from: resolver.resolve("./runtime/composables/useI18n"),
|
|
57
|
-
priority: 2
|
|
58
|
-
});
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
});
|
|
@@ -3,9 +3,11 @@ type UseI18nOptions = Parameters<typeof _useI18n>[0];
|
|
|
3
3
|
type UseI18nReturn = ReturnType<typeof _useI18n>;
|
|
4
4
|
/**
|
|
5
5
|
* Drop-in replacement for useI18n() that wraps t() to sync missing keys to i18now.
|
|
6
|
-
*
|
|
6
|
+
* Use this instead of useI18n() — auto-imported by the @uipkge/nuxt module.
|
|
7
|
+
*
|
|
8
|
+
* Migration: find-replace `useI18n()` → `useI18now()` in your components.
|
|
7
9
|
*/
|
|
8
|
-
export declare function
|
|
10
|
+
export declare function useI18now(options?: UseI18nOptions): UseI18nReturn & {
|
|
9
11
|
te: (key: string, locale?: string) => boolean;
|
|
10
12
|
};
|
|
11
13
|
export {};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useNuxtApp } from "#app";
|
|
2
2
|
import { useI18n as _useI18n } from "vue-i18n";
|
|
3
|
-
export function
|
|
3
|
+
export function useI18now(options) {
|
|
4
4
|
const i18n = _useI18n({ useScope: "global", ...options });
|
|
5
5
|
const nuxtApp = useNuxtApp();
|
|
6
6
|
const originalT = i18n.t.bind(i18n);
|
|
7
7
|
const patchedT = (key, defaultValue, ...rest) => {
|
|
8
|
-
const result =
|
|
8
|
+
const result = defaultValue !== void 0 ? originalT(key, defaultValue) : originalT(key);
|
|
9
9
|
const sync = nuxtApp.$i18nowSync;
|
|
10
10
|
const defaultStr = typeof defaultValue === "string" ? defaultValue : typeof rest[0] === "string" ? rest[0] : void 0;
|
|
11
11
|
if (sync && !sync.existingKeys.has(key) && defaultStr !== void 0) {
|
|
@@ -67,22 +67,43 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
67
67
|
debug: import.meta.dev
|
|
68
68
|
});
|
|
69
69
|
const locale = i18nGlobal?.locale?.value ?? i18nGlobal?.locale ?? configLocale;
|
|
70
|
+
let loadLocaleController = null;
|
|
70
71
|
async function loadLocale(targetLocale) {
|
|
72
|
+
if (loadLocaleController) loadLocaleController.abort();
|
|
73
|
+
loadLocaleController = new AbortController();
|
|
74
|
+
const { signal } = loadLocaleController;
|
|
71
75
|
try {
|
|
72
76
|
const url = `${cdnUrl}/${projectId}/publish/${environment}/${targetLocale}.json`;
|
|
73
|
-
const
|
|
77
|
+
const timeout = setTimeout(() => loadLocaleController?.abort(), 5e3);
|
|
78
|
+
const res = await fetch(url, { signal });
|
|
79
|
+
clearTimeout(timeout);
|
|
74
80
|
if (res.ok) {
|
|
75
81
|
const data = await res.json();
|
|
82
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
83
|
+
if (import.meta.dev) {
|
|
84
|
+
console.warn(`[i18now] CDN response for locale '${targetLocale}' is not a valid object. Skipping.`);
|
|
85
|
+
}
|
|
86
|
+
syncer.setExistingKeys([]);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
76
89
|
syncer.setExistingKeys(Object.keys(data));
|
|
77
90
|
i18nGlobal?.setLocaleMessage(targetLocale, data);
|
|
78
|
-
} else
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
} else {
|
|
92
|
+
syncer.setExistingKeys([]);
|
|
93
|
+
if (import.meta.dev) {
|
|
94
|
+
console.warn(
|
|
95
|
+
`[i18now] Could not load translations from CDN (${res.status}). All keys will be sent to sync \u2014 this is safe, the backend deduplicates them. URL: ${url}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
82
98
|
}
|
|
83
99
|
} catch (err) {
|
|
100
|
+
syncer.setExistingKeys([]);
|
|
84
101
|
if (import.meta.dev) {
|
|
85
|
-
|
|
102
|
+
const isTimeout = err instanceof DOMException && err.name === "AbortError";
|
|
103
|
+
console.warn(
|
|
104
|
+
`[i18now] CDN fetch ${isTimeout ? "timed out" : "failed"} for locale '${targetLocale}'${isTimeout ? "" : ":"}`,
|
|
105
|
+
...isTimeout ? [] : [err]
|
|
106
|
+
);
|
|
86
107
|
}
|
|
87
108
|
}
|
|
88
109
|
}
|
|
@@ -90,7 +111,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
90
111
|
const originalGlobalT = nuxtApp.vueApp.config.globalProperties.$t;
|
|
91
112
|
if (originalGlobalT) {
|
|
92
113
|
nuxtApp.vueApp.config.globalProperties.$t = function(key, defaultValue, ...rest) {
|
|
93
|
-
const result =
|
|
114
|
+
const result = defaultValue !== void 0 ? originalGlobalT.call(this, key, defaultValue) : originalGlobalT.call(this, key);
|
|
94
115
|
const defaultStr = typeof defaultValue === "string" ? defaultValue : typeof rest[0] === "string" ? rest[0] : void 0;
|
|
95
116
|
if (!syncer.existingKeys.has(key) && defaultStr !== void 0) {
|
|
96
117
|
syncer.syncKey(key, defaultStr);
|
package/dist/runtime/plugin.js
CHANGED
|
@@ -6,7 +6,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
6
6
|
async function loadLocale(locale) {
|
|
7
7
|
try {
|
|
8
8
|
const url = `${cdnUrl}/${projectId}/publish/${environment}/${locale}.json`;
|
|
9
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(
|
|
9
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
|
|
10
10
|
if (!res.ok) {
|
|
11
11
|
if (import.meta.dev) {
|
|
12
12
|
console.warn(
|
|
@@ -15,7 +15,14 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
15
15
|
}
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
|
-
const
|
|
18
|
+
const data = await res.json();
|
|
19
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
20
|
+
if (import.meta.dev) {
|
|
21
|
+
console.warn(`[i18now] CDN response for locale '${locale}' is not a valid object. Skipping.`);
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const messages = data;
|
|
19
26
|
if (i18n) i18n.setLocaleMessage(locale, messages);
|
|
20
27
|
} catch (err) {
|
|
21
28
|
if (import.meta.dev) {
|
|
@@ -4,7 +4,7 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
4
4
|
let symbolGlobal;
|
|
5
5
|
if (!directGlobal) {
|
|
6
6
|
const provides = nuxtApp.vueApp._context?.provides ?? {};
|
|
7
|
-
const i18nSym = Object.getOwnPropertySymbols(provides).find((s) => s.toString()
|
|
7
|
+
const i18nSym = Object.getOwnPropertySymbols(provides).find((s) => s.toString().startsWith("Symbol(vue-i18n"));
|
|
8
8
|
const i18nInstance = i18nSym ? provides[i18nSym] : void 0;
|
|
9
9
|
symbolGlobal = i18nInstance?.global;
|
|
10
10
|
}
|
package/package.json
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipkge/nuxt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
|
+
"types": "./dist/module.d.mts",
|
|
8
9
|
"import": "./dist/module.mjs"
|
|
9
10
|
}
|
|
10
11
|
},
|
|
11
12
|
"main": "./dist/module.mjs",
|
|
13
|
+
"types": "./dist/module.d.mts",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
12
17
|
"files": [
|
|
13
18
|
"dist"
|
|
14
19
|
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "nuxt-module-build build",
|
|
17
|
-
"dev": "nuxt-module-build prepare",
|
|
18
|
-
"dev:playground": "nuxi dev playground",
|
|
19
|
-
"prepack": "nuxt-module-build build",
|
|
20
|
-
"test": "vitest run",
|
|
21
|
-
"test:watch": "vitest",
|
|
22
|
-
"test:coverage": "vitest run --coverage",
|
|
23
|
-
"release": "vitest run && nuxt-module-build build && pnpm publish --access public --no-git-checks"
|
|
24
|
-
},
|
|
25
20
|
"dependencies": {
|
|
26
21
|
"@nuxt/kit": "^3.0.0"
|
|
27
22
|
},
|
|
@@ -39,9 +34,18 @@
|
|
|
39
34
|
},
|
|
40
35
|
"devDependencies": {
|
|
41
36
|
"@nuxt/module-builder": "^1.0.0",
|
|
42
|
-
"@uipkge/core": "workspace:*",
|
|
43
37
|
"happy-dom": "^17.0.0",
|
|
44
38
|
"nuxt": "^3.0.0",
|
|
45
|
-
"vitest": "^4.0.0"
|
|
39
|
+
"vitest": "^4.0.0",
|
|
40
|
+
"@uipkge/core": "0.1.1"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "nuxt-module-build build",
|
|
44
|
+
"dev": "nuxt-module-build prepare",
|
|
45
|
+
"dev:playground": "nuxi dev playground",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"test:coverage": "vitest run --coverage",
|
|
49
|
+
"release": "vitest run && nuxt-module-build build && pnpm publish --access public --no-git-checks"
|
|
46
50
|
}
|
|
47
|
-
}
|
|
51
|
+
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { createError, defineEventHandler, readBody } from "h3";
|
|
2
|
-
export default defineEventHandler(async (event) => {
|
|
3
|
-
const config = useRuntimeConfig(event);
|
|
4
|
-
const publicConfig = config.public?.i18now;
|
|
5
|
-
const env = process.env.NODE_ENV ?? "production";
|
|
6
|
-
if (!publicConfig?.syncIn?.includes(env)) {
|
|
7
|
-
throw createError({ statusCode: 403, statusMessage: "Key sync is disabled in this environment" });
|
|
8
|
-
}
|
|
9
|
-
if (!publicConfig.projectId || !publicConfig.host) {
|
|
10
|
-
throw createError({ statusCode: 500, statusMessage: "i18now module is not configured" });
|
|
11
|
-
}
|
|
12
|
-
const privateConfig = config.i18now;
|
|
13
|
-
const apiKey = privateConfig?.apiKey;
|
|
14
|
-
if (!apiKey) {
|
|
15
|
-
throw createError({ statusCode: 500, statusMessage: "i18now apiKey is not configured" });
|
|
16
|
-
}
|
|
17
|
-
let body;
|
|
18
|
-
try {
|
|
19
|
-
body = await readBody(event);
|
|
20
|
-
} catch {
|
|
21
|
-
throw createError({ statusCode: 400, statusMessage: "Invalid request body" });
|
|
22
|
-
}
|
|
23
|
-
if (!Array.isArray(body?.keys)) {
|
|
24
|
-
throw createError({ statusCode: 400, statusMessage: 'Request body must contain a "keys" array' });
|
|
25
|
-
}
|
|
26
|
-
const validKeys = body.keys.filter(
|
|
27
|
-
(item) => item !== null && typeof item === "object" && typeof item.key === "string" && item.key.length > 0 && typeof item.value === "string" && item.value.length > 0
|
|
28
|
-
);
|
|
29
|
-
if (validKeys.length === 0) {
|
|
30
|
-
return { synced: 0 };
|
|
31
|
-
}
|
|
32
|
-
if (validKeys.length > 100) {
|
|
33
|
-
throw createError({ statusCode: 400, statusMessage: "Maximum 100 keys per request" });
|
|
34
|
-
}
|
|
35
|
-
const res = await fetch(
|
|
36
|
-
`${publicConfig.host}/api/v1/projects/${publicConfig.projectId}/sync`,
|
|
37
|
-
{
|
|
38
|
-
method: "POST",
|
|
39
|
-
headers: { "Content-Type": "application/json" },
|
|
40
|
-
body: JSON.stringify({ apiKey, keys: validKeys })
|
|
41
|
-
}
|
|
42
|
-
);
|
|
43
|
-
const data = await res.json();
|
|
44
|
-
if (!res.ok) {
|
|
45
|
-
throw createError({ statusCode: res.status, data });
|
|
46
|
-
}
|
|
47
|
-
return data;
|
|
48
|
-
});
|