@uipkge/nuxt 0.1.21 → 0.1.23
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/bin/cli.mjs +169 -0
- package/dist/module.d.mts +12 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +64 -28
- package/dist/runtime/composables/useI18nowLocales.d.ts +2 -2
- package/dist/runtime/plugin.client.js +11 -3
- package/dist/runtime/plugin.js +1 -3
- package/dist/runtime/plugin.sync-only.client.d.ts +15 -0
- package/dist/runtime/plugin.sync-only.client.js +124 -0
- package/package.json +7 -7
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from 'child_process'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
const PKG_DIR = path.resolve(import.meta.dirname, '..')
|
|
7
|
+
const TARGET_DIR = process.cwd()
|
|
8
|
+
const distDir = path.join(PKG_DIR, 'dist')
|
|
9
|
+
|
|
10
|
+
const isInstalled = fs.existsSync(distDir)
|
|
11
|
+
|
|
12
|
+
function run(cmd, opts = {}) {
|
|
13
|
+
console.log(`> ${cmd}`)
|
|
14
|
+
execSync(cmd, { stdio: 'inherit', ...opts })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function exists(file) {
|
|
18
|
+
return fs.existsSync(path.join(TARGET_DIR, file))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log('\n🚀 Setting up @uipkge/nuxt in', TARGET_DIR, '\n')
|
|
22
|
+
|
|
23
|
+
if (isInstalled) {
|
|
24
|
+
console.log('📦 @uipkge/nuxt already built, skipping build step')
|
|
25
|
+
} else {
|
|
26
|
+
console.log('📦 Building @uipkge/nuxt...')
|
|
27
|
+
run('npm run build', { cwd: PKG_DIR })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. Install @nuxtjs/i18n
|
|
31
|
+
console.log('\n📥 Installing @nuxtjs/i18n...')
|
|
32
|
+
run('npm install @nuxtjs/i18n@latest', { cwd: TARGET_DIR })
|
|
33
|
+
|
|
34
|
+
// 3. Link local @uipkge/nuxt only if not already installed
|
|
35
|
+
if (!isInstalled) {
|
|
36
|
+
console.log('\n🔗 Linking @uipkge/nuxt...')
|
|
37
|
+
run('npm link', { cwd: PKG_DIR })
|
|
38
|
+
run('npm link @uipkge/nuxt', { cwd: TARGET_DIR })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 4. Create i18n folder structure
|
|
42
|
+
const i18nDir = path.join(TARGET_DIR, 'app/i18n/locales')
|
|
43
|
+
if (!fs.existsSync(i18nDir)) {
|
|
44
|
+
console.log('\n📁 Creating i18n folders...')
|
|
45
|
+
fs.mkdirSync(i18nDir, { recursive: true })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 5. Create i18n.config.ts
|
|
49
|
+
const i18nConfigPath = path.join(TARGET_DIR, 'app/i18n/i18n.config.ts')
|
|
50
|
+
if (!exists('app/i18n/i18n.config.ts')) {
|
|
51
|
+
console.log('📝 Creating i18n.config.ts...')
|
|
52
|
+
fs.writeFileSync(i18nConfigPath, `export default defineI18nConfig(() => ({
|
|
53
|
+
numberFormats: {
|
|
54
|
+
en: { currency: { style: 'currency', currency: 'USD' } },
|
|
55
|
+
es: { currency: { style: 'currency', currency: 'EUR' } },
|
|
56
|
+
fr: { currency: { style: 'currency', currency: 'EUR' } },
|
|
57
|
+
ar: { currency: { style: 'currency', currency: 'SAR' } },
|
|
58
|
+
he: { currency: { style: 'currency', currency: 'ILS' } },
|
|
59
|
+
de: { currency: { style: 'currency', currency: 'EUR' } },
|
|
60
|
+
},
|
|
61
|
+
datetimeFormats: {
|
|
62
|
+
en: { short: { year: 'numeric', month: 'short', day: 'numeric' } },
|
|
63
|
+
es: { short: { year: 'numeric', month: 'short', day: 'numeric' } },
|
|
64
|
+
fr: { short: { year: 'numeric', month: 'short', day: 'numeric' } },
|
|
65
|
+
ar: { short: { year: 'numeric', month: 'short', day: 'numeric' } },
|
|
66
|
+
he: { short: { year: 'numeric', month: 'short', day: 'numeric' } },
|
|
67
|
+
de: { short: { year: 'numeric', month: 'short', day: 'numeric' } },
|
|
68
|
+
},
|
|
69
|
+
}))
|
|
70
|
+
`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 6. Create locale files
|
|
74
|
+
const locales = {
|
|
75
|
+
en: { nav_features: 'Features', nav_docs: 'Docs', nav_pricing: 'Pricing', hero_title: 'Translate faster, ship with confidence', hero_subtitle: 'i18now keeps your translations in sync as you code.', footer_tagline: 'Built for developers who ship fast.' },
|
|
76
|
+
es: { nav_features: 'Características', nav_docs: 'Docs', nav_pricing: 'Precios', hero_title: 'Traduce más rápido, envía con confianza', hero_subtitle: 'i18now mantiene tus traducciones sincronizadas.', footer_tagline: 'Construido para desarrolladores rápidos.' },
|
|
77
|
+
}
|
|
78
|
+
for (const [code, messages] of Object.entries(locales)) {
|
|
79
|
+
const localePath = path.join(i18nDir, `${code}.json`)
|
|
80
|
+
if (!fs.existsSync(localePath)) {
|
|
81
|
+
const data = {
|
|
82
|
+
nav: { features: messages.nav_features, docs: messages.nav_docs, pricing: messages.nav_pricing },
|
|
83
|
+
hero: { title: messages.hero_title, subtitle: messages.hero_subtitle },
|
|
84
|
+
footer: { tagline: messages.footer_tagline }
|
|
85
|
+
}
|
|
86
|
+
fs.writeFileSync(localePath, JSON.stringify(data, null, 2))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 7. Update nuxt.config.ts
|
|
91
|
+
const nuxtConfigPath = path.join(TARGET_DIR, 'nuxt.config.ts')
|
|
92
|
+
console.log('\n⚙️ Updating nuxt.config.ts...')
|
|
93
|
+
const nuxtConfigContent = `export default defineNuxtConfig({
|
|
94
|
+
compatibilityDate: '2025-07-15',
|
|
95
|
+
devtools: { enabled: true },
|
|
96
|
+
modules: ['@nuxtjs/i18n', '@uipkge/nuxt'],
|
|
97
|
+
i18n: {
|
|
98
|
+
defaultLocale: 'en',
|
|
99
|
+
locales: [
|
|
100
|
+
{ code: 'en', file: 'en.json' },
|
|
101
|
+
{ code: 'es', file: 'es.json' },
|
|
102
|
+
{ code: 'fr', file: 'fr.json' },
|
|
103
|
+
{ code: 'ar', file: 'ar.json' },
|
|
104
|
+
{ code: 'he', file: 'he.json' },
|
|
105
|
+
{ code: 'de', file: 'de.json' },
|
|
106
|
+
],
|
|
107
|
+
langDir: 'app/i18n/locales/',
|
|
108
|
+
strategy: 'no_prefix',
|
|
109
|
+
detectBrowserLanguage: { useCookie: true, cookieKey: 'i18n_locale' },
|
|
110
|
+
vueI18n: './app/i18n/i18n.config.ts',
|
|
111
|
+
},
|
|
112
|
+
i18now: {
|
|
113
|
+
projectId: process.env.I18NOW_PROJECT_ID ?? '',
|
|
114
|
+
apiKey: process.env.I18NOW_API_KEY ?? '',
|
|
115
|
+
host: process.env.I18NOW_HOST ?? 'https://i18now.com',
|
|
116
|
+
cdnUrl: process.env.I18NOW_CDN_URL ?? 'https://cdn.i18now.com',
|
|
117
|
+
environment: process.env.I18NOW_ENV ?? 'dev',
|
|
118
|
+
syncIn: ['development'],
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
`
|
|
122
|
+
fs.writeFileSync(nuxtConfigPath, nuxtConfigContent)
|
|
123
|
+
|
|
124
|
+
// 8. Create test page
|
|
125
|
+
const pagesDir = path.join(TARGET_DIR, 'app/pages')
|
|
126
|
+
if (!fs.existsSync(pagesDir)) {
|
|
127
|
+
fs.mkdirSync(pagesDir, { recursive: true })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const indexPagePath = path.join(pagesDir, 'index.vue')
|
|
131
|
+
if (!fs.existsSync(indexPagePath)) {
|
|
132
|
+
console.log('📄 Creating test page...')
|
|
133
|
+
fs.writeFileSync(indexPagePath, `<script setup>
|
|
134
|
+
const { t, locale, setLocale, te, n, d } = useI18now()
|
|
135
|
+
|
|
136
|
+
const locales = [
|
|
137
|
+
{ code: 'en', label: 'English', flag: '🇬🇧' },
|
|
138
|
+
{ code: 'es', label: 'Español', flag: '🇪🇸' },
|
|
139
|
+
{ code: 'fr', label: 'Français', flag: '🇫🇷' },
|
|
140
|
+
{ code: 'ar', label: 'العربية', flag: '🇸🇦' },
|
|
141
|
+
{ code: 'he', label: 'עברית', flag: '🇮🇱' },
|
|
142
|
+
{ code: 'de', label: 'Deutsch', flag: '🇩🇪' },
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
const dir = computed(() => {
|
|
146
|
+
try {
|
|
147
|
+
return new Intl.Locale(locale.value).textInfo?.direction ?? 'ltr'
|
|
148
|
+
} catch {
|
|
149
|
+
return 'ltr'
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
<template>
|
|
155
|
+
<div :dir="dir">
|
|
156
|
+
<h1>{{ t('hero.title', 'Hello World') }}</h1>
|
|
157
|
+
<p>{{ t('hero.subtitle', 'Welcome to i18now') }}</p>
|
|
158
|
+
<select v-model="locale">
|
|
159
|
+
<option v-for="l in locales" :key="l.code" :value="l.code">
|
|
160
|
+
{{ l.flag }} {{ l.label }}
|
|
161
|
+
</option>
|
|
162
|
+
</select>
|
|
163
|
+
</div>
|
|
164
|
+
</template>
|
|
165
|
+
`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log('\n✅ Setup complete!')
|
|
169
|
+
console.log('Run: npm run dev\n')
|
package/dist/module.d.mts
CHANGED
|
@@ -15,6 +15,18 @@ interface ModuleOptions {
|
|
|
15
15
|
* Default: 'en'
|
|
16
16
|
*/
|
|
17
17
|
locale?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Controls which features are active:
|
|
20
|
+
* - 'full' (default): key sync in dev + CDN translation loading
|
|
21
|
+
* - 'sync-only': key sync in dev only — you manage translation loading yourself
|
|
22
|
+
* - 'cdn-only': CDN translation loading only — no key sync
|
|
23
|
+
*/
|
|
24
|
+
mode?: "full" | "sync-only" | "cdn-only";
|
|
25
|
+
/**
|
|
26
|
+
* Shorthand for mode: when false, equivalent to mode: 'sync-only'.
|
|
27
|
+
* Ignored if `mode` is explicitly set.
|
|
28
|
+
*/
|
|
29
|
+
fetchTranslations?: boolean;
|
|
18
30
|
}
|
|
19
31
|
declare const _default: nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
20
32
|
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -11,25 +11,43 @@ const module$1 = defineNuxtModule({
|
|
|
11
11
|
cdnUrl: "https://cdn.i18now.com",
|
|
12
12
|
environment: "dev",
|
|
13
13
|
syncIn: ["development"],
|
|
14
|
-
locale: "en"
|
|
14
|
+
locale: "en",
|
|
15
|
+
mode: "full",
|
|
16
|
+
fetchTranslations: true
|
|
15
17
|
},
|
|
16
18
|
async setup(options, nuxt) {
|
|
17
19
|
const resolver = createResolver(import.meta.url);
|
|
20
|
+
const mode = options.mode ?? (options.fetchTranslations === false ? "sync-only" : "full");
|
|
21
|
+
const enableSync = mode === "full" || mode === "sync-only";
|
|
22
|
+
const enableFetch = mode === "full" || mode === "cdn-only";
|
|
18
23
|
if (!options.projectId) {
|
|
19
|
-
console.warn(
|
|
24
|
+
console.warn(
|
|
25
|
+
"[i18now] projectId is required. The module will not work until it is set."
|
|
26
|
+
);
|
|
20
27
|
}
|
|
21
|
-
if (!options.apiKey) {
|
|
22
|
-
console.warn(
|
|
28
|
+
if (enableSync && !options.apiKey) {
|
|
29
|
+
console.warn(
|
|
30
|
+
"[i18now] apiKey is required for key sync. Sync will not work until it is set."
|
|
31
|
+
);
|
|
23
32
|
}
|
|
24
|
-
if (options.cdnUrl && !options.cdnUrl.startsWith("http")) {
|
|
25
|
-
console.warn(
|
|
33
|
+
if (enableFetch && options.cdnUrl && !options.cdnUrl.startsWith("http")) {
|
|
34
|
+
console.warn(
|
|
35
|
+
"[i18now] cdnUrl should be a valid HTTP(S) URL. Got: " + options.cdnUrl
|
|
36
|
+
);
|
|
26
37
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
if (enableSync) {
|
|
39
|
+
const dangerousEnvs = (options.syncIn ?? []).filter(
|
|
40
|
+
(e) => ["production", "staging", "prod", "stage"].includes(e.toLowerCase())
|
|
41
|
+
);
|
|
42
|
+
if (dangerousEnvs.length > 0) {
|
|
43
|
+
console.error(
|
|
44
|
+
`[i18now] DANGER: syncIn includes "${dangerousEnvs.join('", "')}" \u2014 key sync is enabled in a production-like environment. This will expose your API key to users and flood i18now with production traffic. syncIn should only contain "development".`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (import.meta.dev) {
|
|
49
|
+
console.log(
|
|
50
|
+
`[i18now] mode: ${mode} (sync: ${enableSync}, fetch: ${enableFetch})`
|
|
33
51
|
);
|
|
34
52
|
}
|
|
35
53
|
nuxt.options.runtimeConfig.public.i18now = {
|
|
@@ -39,16 +57,18 @@ const module$1 = defineNuxtModule({
|
|
|
39
57
|
locale: options.locale,
|
|
40
58
|
// Dev-only fields — only included when the sync client plugin is active.
|
|
41
59
|
// In production builds these are undefined and the client plugin is not registered.
|
|
42
|
-
...nuxt.options.dev ? {
|
|
60
|
+
...nuxt.options.dev && enableSync ? {
|
|
43
61
|
apiKey: options.apiKey,
|
|
44
62
|
host: options.host,
|
|
45
63
|
syncIn: options.syncIn
|
|
46
64
|
} : {}
|
|
47
65
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
if (enableFetch) {
|
|
67
|
+
addPlugin({
|
|
68
|
+
src: resolver.resolve("./runtime/plugin"),
|
|
69
|
+
mode: "server"
|
|
70
|
+
});
|
|
71
|
+
}
|
|
52
72
|
addImports([
|
|
53
73
|
{
|
|
54
74
|
name: "useI18now",
|
|
@@ -62,18 +82,34 @@ const module$1 = defineNuxtModule({
|
|
|
62
82
|
}
|
|
63
83
|
]);
|
|
64
84
|
if (nuxt.options.dev) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
if (enableSync && enableFetch) {
|
|
86
|
+
addPlugin({
|
|
87
|
+
src: resolver.resolve("./runtime/plugin.client"),
|
|
88
|
+
mode: "client"
|
|
89
|
+
});
|
|
90
|
+
} else if (enableSync) {
|
|
91
|
+
addPlugin({
|
|
92
|
+
src: resolver.resolve("./runtime/plugin.sync-only.client"),
|
|
93
|
+
mode: "client"
|
|
94
|
+
});
|
|
95
|
+
} else if (enableFetch) {
|
|
96
|
+
addPlugin({
|
|
97
|
+
src: resolver.resolve("./runtime/plugin.cdn.client"),
|
|
98
|
+
mode: "client"
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (enableSync) {
|
|
102
|
+
addPlugin({
|
|
103
|
+
src: resolver.resolve("./runtime/plugin.suppress-warnings")
|
|
104
|
+
});
|
|
105
|
+
}
|
|
72
106
|
} else {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
107
|
+
if (enableFetch) {
|
|
108
|
+
addPlugin({
|
|
109
|
+
src: resolver.resolve("./runtime/plugin.cdn.client"),
|
|
110
|
+
mode: "client"
|
|
111
|
+
});
|
|
112
|
+
}
|
|
77
113
|
}
|
|
78
114
|
}
|
|
79
115
|
});
|
|
@@ -26,8 +26,8 @@ export interface I18nowLocale {
|
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
export declare function useI18nowLocales(): {
|
|
29
|
-
locales:
|
|
30
|
-
sourceLocale:
|
|
29
|
+
locales: any;
|
|
30
|
+
sourceLocale: any;
|
|
31
31
|
pending: any;
|
|
32
32
|
error: any;
|
|
33
33
|
refresh: any;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineNuxtPlugin, useRuntimeConfig } from "#app";
|
|
2
2
|
function createI18nowSyncer(options) {
|
|
3
|
-
const { endpoint,
|
|
3
|
+
const { endpoint, extraBody, debug = false, maxTracked = 2e3, maxRetries = 3 } = options;
|
|
4
4
|
const existingKeys = /* @__PURE__ */ new Set();
|
|
5
5
|
const synced = /* @__PURE__ */ new Set();
|
|
6
6
|
const retries = /* @__PURE__ */ new Map();
|
|
@@ -14,7 +14,7 @@ function createI18nowSyncer(options) {
|
|
|
14
14
|
fetch(endpoint, {
|
|
15
15
|
method: "POST",
|
|
16
16
|
headers: { "Content-Type": "application/json" },
|
|
17
|
-
body: JSON.stringify({
|
|
17
|
+
body: JSON.stringify({ keys, ...extraBody })
|
|
18
18
|
}).then((res) => {
|
|
19
19
|
if (!res.ok) {
|
|
20
20
|
if (debug) {
|
|
@@ -47,6 +47,14 @@ function createI18nowSyncer(options) {
|
|
|
47
47
|
console.error(`[i18now] Invalid key skipped: "${key}" \u2014 keys must be dotted paths (e.g. "common.save")`);
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
+
if (key.length > 100) {
|
|
51
|
+
console.error(`[i18now] Key "${key.slice(0, 20)}..." exceeds 100 character limit. Skipped.`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (value.length > 1e3) {
|
|
55
|
+
console.error(`[i18now] Value for key "${key}" exceeds 1,000 character limit (${value.length} chars). Skipped.`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
50
58
|
synced.add(key);
|
|
51
59
|
pending.set(key, value);
|
|
52
60
|
if (!flushTimer) flushTimer = setTimeout(flush, 0);
|
|
@@ -67,7 +75,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
67
75
|
const i18nGlobal = nuxtApp.$i18n ?? nuxtApp.vueApp.config.globalProperties.$i18n;
|
|
68
76
|
const syncer = createI18nowSyncer({
|
|
69
77
|
endpoint: `${host}/api/v1/projects/${projectId}/sync`,
|
|
70
|
-
apiKey,
|
|
78
|
+
extraBody: { apiKey },
|
|
71
79
|
debug: import.meta.dev
|
|
72
80
|
});
|
|
73
81
|
const locale = i18nGlobal?.locale?.value ?? i18nGlobal?.locale ?? configLocale;
|
package/dist/runtime/plugin.js
CHANGED
|
@@ -22,9 +22,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
22
22
|
}
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
if (i18n) i18n.mergeLocaleMessage(locale, messages);
|
|
27
|
-
nuxtApp.payload.i18nowCdnKeys = Object.keys(messages);
|
|
25
|
+
if (i18n) i18n.mergeLocaleMessage(locale, data);
|
|
28
26
|
} catch (err) {
|
|
29
27
|
if (import.meta.dev) {
|
|
30
28
|
console.warn(`[i18now] Failed to load locale '${locale}' from CDN:`, err);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync-only client plugin — key sync + $t patching, but NO mergeLocaleMessage.
|
|
3
|
+
*
|
|
4
|
+
* CDN is fetched only to populate existingKeys (for smart deduplication).
|
|
5
|
+
* Translations are NOT merged into vue-i18n — the user manages loading themselves.
|
|
6
|
+
*/
|
|
7
|
+
declare const _default: (nuxtApp: unknown) => Promise<{
|
|
8
|
+
provide: {
|
|
9
|
+
i18nowSync: {
|
|
10
|
+
syncKey: (key: string, value: string) => void;
|
|
11
|
+
existingKeys: Set<string>;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
} | undefined>;
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useRuntimeConfig } from "#app";
|
|
2
|
+
function createI18nowSyncer(options) {
|
|
3
|
+
const { endpoint, extraBody, debug = false, maxTracked = 2e3, maxRetries = 3 } = options;
|
|
4
|
+
const existingKeys = /* @__PURE__ */ new Set();
|
|
5
|
+
const synced = /* @__PURE__ */ new Set();
|
|
6
|
+
const retries = /* @__PURE__ */ new Map();
|
|
7
|
+
const pending = /* @__PURE__ */ new Map();
|
|
8
|
+
let flushTimer = null;
|
|
9
|
+
function flush() {
|
|
10
|
+
flushTimer = null;
|
|
11
|
+
if (pending.size === 0) return;
|
|
12
|
+
const keys = Array.from(pending.entries()).map(([key, value]) => ({ key, value }));
|
|
13
|
+
pending.clear();
|
|
14
|
+
fetch(endpoint, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json" },
|
|
17
|
+
body: JSON.stringify({ keys, ...extraBody })
|
|
18
|
+
}).then((res) => {
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
if (debug) {
|
|
21
|
+
res.json().then((data) => console.warn(`[i18now] Sync failed (${res.status}):`, data?.error ?? data?.statusMessage ?? data)).catch(() => console.warn(`[i18now] Sync failed with status ${res.status}`));
|
|
22
|
+
}
|
|
23
|
+
throw Object.assign(new Error(`${res.status}`), { status: res.status });
|
|
24
|
+
}
|
|
25
|
+
}).catch((err) => {
|
|
26
|
+
const status = err.status ?? 0;
|
|
27
|
+
const retryable = status === 0 || status === 429 || status >= 500;
|
|
28
|
+
for (const { key } of keys) {
|
|
29
|
+
if (!retryable) {
|
|
30
|
+
retries.delete(key);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const attempts = (retries.get(key) ?? 0) + 1;
|
|
34
|
+
if (attempts < maxRetries) {
|
|
35
|
+
retries.set(key, attempts);
|
|
36
|
+
synced.delete(key);
|
|
37
|
+
} else {
|
|
38
|
+
retries.delete(key);
|
|
39
|
+
if (debug) console.warn(`[i18now] Gave up syncing "${key}" after ${maxRetries} attempts.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function syncKey(key, value) {
|
|
45
|
+
if (synced.has(key) || synced.size >= maxTracked) return;
|
|
46
|
+
if (!value || !/^[\w-]+(?:\.[\w-]+)*$/.test(key)) {
|
|
47
|
+
console.error(`[i18now] Invalid key skipped: "${key}" \u2014 keys must be dotted paths (e.g. "common.save")`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (key.length > 100) {
|
|
51
|
+
console.error(`[i18now] Key "${key.slice(0, 20)}..." exceeds 100 character limit. Skipped.`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (value.length > 1e3) {
|
|
55
|
+
console.error(`[i18now] Value for key "${key}" exceeds 1,000 character limit (${value.length} chars). Skipped.`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
synced.add(key);
|
|
59
|
+
pending.set(key, value);
|
|
60
|
+
if (!flushTimer) flushTimer = setTimeout(flush, 0);
|
|
61
|
+
}
|
|
62
|
+
function setExistingKeys(keys) {
|
|
63
|
+
existingKeys.clear();
|
|
64
|
+
for (const key of keys) existingKeys.add(key);
|
|
65
|
+
}
|
|
66
|
+
return { syncKey, existingKeys, setExistingKeys };
|
|
67
|
+
}
|
|
68
|
+
export default defineNuxtPlugin(async (nuxtApp) => {
|
|
69
|
+
const { projectId, apiKey, host, cdnUrl, environment, syncIn, locale: configLocale } = useRuntimeConfig().public.i18now;
|
|
70
|
+
if (!syncIn.includes(process.env.NODE_ENV ?? "production")) return;
|
|
71
|
+
if (!projectId) {
|
|
72
|
+
console.warn("[i18now] projectId is not set \u2014 key sync disabled.");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const i18nGlobal = nuxtApp.$i18n ?? nuxtApp.vueApp.config.globalProperties.$i18n;
|
|
76
|
+
const syncer = createI18nowSyncer({
|
|
77
|
+
endpoint: `${host}/api/v1/projects/${projectId}/sync`,
|
|
78
|
+
extraBody: { apiKey },
|
|
79
|
+
debug: import.meta.dev
|
|
80
|
+
});
|
|
81
|
+
const locale = i18nGlobal?.locale?.value ?? i18nGlobal?.locale ?? configLocale;
|
|
82
|
+
async function loadExistingKeys(targetLocale) {
|
|
83
|
+
try {
|
|
84
|
+
const url = `${cdnUrl}/${projectId}/publish/${environment}/${targetLocale}.json`;
|
|
85
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
|
|
86
|
+
if (res.ok) {
|
|
87
|
+
const data = await res.json();
|
|
88
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) {
|
|
89
|
+
syncer.setExistingKeys(Object.keys(data));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
syncer.setExistingKeys([]);
|
|
94
|
+
} catch {
|
|
95
|
+
syncer.setExistingKeys([]);
|
|
96
|
+
if (import.meta.dev) {
|
|
97
|
+
console.warn("[i18now] Could not fetch existing keys from CDN \u2014 all keys will be synced (backend deduplicates).");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
let currentLocale = locale;
|
|
102
|
+
await loadExistingKeys(locale);
|
|
103
|
+
const originalGlobalT = nuxtApp.vueApp.config.globalProperties.$t;
|
|
104
|
+
if (originalGlobalT) {
|
|
105
|
+
nuxtApp.vueApp.config.globalProperties.$t = function(key, defaultValue, ...rest) {
|
|
106
|
+
const result = defaultValue !== void 0 ? originalGlobalT.call(this, key, defaultValue) : originalGlobalT.call(this, key);
|
|
107
|
+
const defaultStr = typeof defaultValue === "string" ? defaultValue : typeof rest[0] === "string" ? rest[0] : void 0;
|
|
108
|
+
if (!syncer.existingKeys.has(key) && defaultStr !== void 0) {
|
|
109
|
+
syncer.syncKey(key, defaultStr);
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
nuxtApp.hook("i18n:beforeLocaleSwitch", async ({ newLocale }) => {
|
|
115
|
+
if (newLocale === currentLocale) return;
|
|
116
|
+
currentLocale = newLocale;
|
|
117
|
+
await loadExistingKeys(newLocale);
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
provide: {
|
|
121
|
+
i18nowSync: { syncKey: syncer.syncKey, existingKeys: syncer.existingKeys }
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipkge/nuxt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"@uipkge/nuxt": "./bin/cli.mjs"
|
|
8
|
+
},
|
|
6
9
|
"exports": {
|
|
7
10
|
".": {
|
|
8
11
|
"types": "./dist/module.d.mts",
|
|
@@ -11,11 +14,9 @@
|
|
|
11
14
|
},
|
|
12
15
|
"main": "./dist/module.mjs",
|
|
13
16
|
"types": "./dist/module.d.mts",
|
|
14
|
-
"engines": {
|
|
15
|
-
"node": ">=18.0.0"
|
|
16
|
-
},
|
|
17
17
|
"files": [
|
|
18
|
-
"dist"
|
|
18
|
+
"dist",
|
|
19
|
+
"bin"
|
|
19
20
|
],
|
|
20
21
|
"dependencies": {
|
|
21
22
|
"@nuxt/kit": "^3.0.0"
|
|
@@ -36,8 +37,7 @@
|
|
|
36
37
|
"@nuxt/module-builder": "^1.0.0",
|
|
37
38
|
"happy-dom": "^17.0.0",
|
|
38
39
|
"nuxt": "^3.0.0",
|
|
39
|
-
"vitest": "^4.0.0"
|
|
40
|
-
"@uipkge/core": "0.1.1"
|
|
40
|
+
"vitest": "^4.0.0"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "nuxt-module-build build",
|