@verbaly/sveltekit 0.16.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/LICENSE +21 -0
- package/README.md +122 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aron Soto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/AronSoto/verbaly/develop/assets/logo.png" alt="Verbaly" width="300" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center"><em>SvelteKit SSR integration for Verbaly β per-request locale negotiation and flash-free hydration.</em></p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@verbaly/sveltekit"><img src="https://img.shields.io/npm/v/@verbaly/sveltekit?logo=npm&color=cb3837" alt="npm version" /></a>
|
|
9
|
+
<a href="https://github.com/AronSoto/verbaly/blob/develop/LICENSE"><img src="https://img.shields.io/npm/l/@verbaly/sveltekit?color=blue" alt="MIT" /></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Server-rendered pages arrive already translated in the visitor's language β cookie first, then `Accept-Language`, then your fallback β and the client hydrates with the **same locale and the same catalog**, so there's no flash of untranslated text and no hydration mismatch. Each request gets its **own instance**: no locale leaking between concurrent users.
|
|
15
|
+
|
|
16
|
+
## π Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add verbaly @verbaly/sveltekit @verbaly/svelte @verbaly/vite
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## β‘ Wire it up
|
|
23
|
+
|
|
24
|
+
**1. Vite plugin** (extraction + the `virtual:verbaly` module):
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// vite.config.ts
|
|
28
|
+
import { sveltekit } from '@sveltejs/kit/vite';
|
|
29
|
+
import verbaly from '@verbaly/vite';
|
|
30
|
+
|
|
31
|
+
export default { plugins: [verbaly(), sveltekit()] };
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**2. The lang placeholder** in `src/app.html`:
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<html lang="%verbaly.lang%">
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**3. Server hook** β negotiates the locale per request:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// src/hooks.server.ts
|
|
44
|
+
import { verbalyHandle } from '@verbaly/sveltekit';
|
|
45
|
+
import { locales } from 'virtual:verbaly';
|
|
46
|
+
|
|
47
|
+
export const handle = verbalyHandle({ locales });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// src/app.d.ts
|
|
52
|
+
declare namespace App {
|
|
53
|
+
interface Locals {
|
|
54
|
+
verbalyLocale: string;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**4. Hand the locale to the client**:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// src/routes/+layout.server.ts
|
|
63
|
+
export const load = ({ locals }) => ({ locale: locals.verbalyLocale });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**5. One instance per render, catalog awaited** β this is the no-FOUC guarantee:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// src/routes/+layout.ts
|
|
70
|
+
import { createInstance } from 'virtual:verbaly';
|
|
71
|
+
|
|
72
|
+
export const load = async ({ data }) => {
|
|
73
|
+
const verbaly = createInstance();
|
|
74
|
+
await verbaly.loadLocale(data.locale); // catalog ready BEFORE render
|
|
75
|
+
verbaly.setLocale(data.locale);
|
|
76
|
+
return { ...data, verbaly };
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<!-- src/routes/+layout.svelte -->
|
|
82
|
+
<script>
|
|
83
|
+
import { provideVerbaly } from '@verbaly/svelte';
|
|
84
|
+
|
|
85
|
+
let { data, children } = $props();
|
|
86
|
+
provideVerbaly(data.verbaly);
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
{@render children()}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**6. Switching languages** (client) β persists the cookie the server hook reads:
|
|
93
|
+
|
|
94
|
+
```html
|
|
95
|
+
<script>
|
|
96
|
+
import { switchLocale } from '@verbaly/sveltekit';
|
|
97
|
+
import { useVerbaly } from '@verbaly/svelte';
|
|
98
|
+
|
|
99
|
+
const verbaly = useVerbaly();
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<button on:click={() => switchLocale(verbaly, 'es')}>EspaΓ±ol</button>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## π API
|
|
106
|
+
|
|
107
|
+
| Export | What it does |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `verbalyHandle({ locales, fallback?, cookie? })` | SvelteKit `handle` hook: resolves the request locale (cookie β `Accept-Language` β fallback), sets `event.locals.verbalyLocale`, fills `%verbaly.lang%` in `app.html`. |
|
|
110
|
+
| `switchLocale(instance, locale, { cookie?, maxAge? })` | Awaits the catalog, switches the locale, writes the cookie and syncs `<html lang>`. SSR-safe. |
|
|
111
|
+
| `LOCALE_COOKIE` | Default cookie name (`verbaly-locale`) β shared with core's `localStorage` key. |
|
|
112
|
+
|
|
113
|
+
- `cookie: false` disables cookie reading/writing (pure `Accept-Language`).
|
|
114
|
+
- No dependency on `@sveltejs/kit` β the hook is typed structurally and stays compatible across Kit 2.x.
|
|
115
|
+
|
|
116
|
+
## π Docs
|
|
117
|
+
|
|
118
|
+
Full guide: [verbaly-web.vercel.app/docs/sveltekit](https://verbaly-web.vercel.app/docs/sveltekit)
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
[MIT](https://github.com/AronSoto/verbaly/blob/develop/LICENSE) Β© Aron Soto
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Verbaly } from "verbaly";
|
|
2
|
+
//#region src/index.d.ts
|
|
3
|
+
declare const LOCALE_COOKIE = "verbaly-locale";
|
|
4
|
+
interface HandleEvent {
|
|
5
|
+
request: Request;
|
|
6
|
+
cookies: {
|
|
7
|
+
get(name: string): string | undefined;
|
|
8
|
+
};
|
|
9
|
+
locals: {
|
|
10
|
+
verbalyLocale?: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
interface ResolveOptions {
|
|
14
|
+
transformPageChunk?(input: {
|
|
15
|
+
html: string;
|
|
16
|
+
done: boolean;
|
|
17
|
+
}): string | undefined;
|
|
18
|
+
}
|
|
19
|
+
interface HandleInput {
|
|
20
|
+
event: HandleEvent;
|
|
21
|
+
resolve(event: HandleEvent, opts?: ResolveOptions): Response | Promise<Response>;
|
|
22
|
+
}
|
|
23
|
+
interface VerbalyHandleOptions {
|
|
24
|
+
/** supported locales β pass `locales` from 'virtual:verbaly' */
|
|
25
|
+
locales: string[];
|
|
26
|
+
/** default when nothing matches (defaults to the first locale) */
|
|
27
|
+
fallback?: string;
|
|
28
|
+
/** cookie read for the user's choice; `false` = Accept-Language only */
|
|
29
|
+
cookie?: string | false;
|
|
30
|
+
}
|
|
31
|
+
declare function verbalyHandle(options: VerbalyHandleOptions): ({ event, resolve }: HandleInput) => Response | Promise<Response>;
|
|
32
|
+
interface SwitchLocaleOptions {
|
|
33
|
+
/** cookie written so the next SSR request matches; `false` = don't persist */
|
|
34
|
+
cookie?: string | false;
|
|
35
|
+
/** cookie lifetime in seconds (default 1 year) */
|
|
36
|
+
maxAge?: number;
|
|
37
|
+
}
|
|
38
|
+
declare function switchLocale(instance: Pick<Verbaly, 'loadLocale' | 'setLocale'>, locale: string, options?: SwitchLocaleOptions): Promise<void>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { LOCALE_COOKIE, SwitchLocaleOptions, VerbalyHandleOptions, switchLocale, verbalyHandle };
|
|
41
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { negotiateLocale } from "verbaly";
|
|
2
|
+
//#region src/index.ts
|
|
3
|
+
const LOCALE_COOKIE = "verbaly-locale";
|
|
4
|
+
const LANG_PLACEHOLDER = "%verbaly.lang%";
|
|
5
|
+
const YEAR = 31536e3;
|
|
6
|
+
function verbalyHandle(options) {
|
|
7
|
+
const { locales, cookie = LOCALE_COOKIE } = options;
|
|
8
|
+
if (!Array.isArray(locales) || locales.length === 0) throw new Error("[verbaly] verbalyHandle needs `locales` β pass the `locales` export from 'virtual:verbaly' (requires @verbaly/vite β₯0.16) or list them yourself: verbalyHandle({ locales: [...] })");
|
|
9
|
+
const fallback = options.fallback ?? locales[0];
|
|
10
|
+
return ({ event, resolve }) => {
|
|
11
|
+
let locale;
|
|
12
|
+
if (cookie) {
|
|
13
|
+
const stored = event.cookies.get(cookie);
|
|
14
|
+
if (stored) locale = negotiateLocale(stored, locales, "") || void 0;
|
|
15
|
+
}
|
|
16
|
+
const resolved = locale ?? negotiateLocale(event.request.headers.get("accept-language"), locales, fallback);
|
|
17
|
+
event.locals.verbalyLocale = resolved;
|
|
18
|
+
return resolve(event, { transformPageChunk: ({ html }) => html.replaceAll(LANG_PLACEHOLDER, resolved) });
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function switchLocale(instance, locale, options = {}) {
|
|
22
|
+
const { cookie = LOCALE_COOKIE, maxAge = YEAR } = options;
|
|
23
|
+
await instance.loadLocale(locale);
|
|
24
|
+
instance.setLocale(locale);
|
|
25
|
+
if (typeof document === "undefined") return;
|
|
26
|
+
if (cookie) document.cookie = `${cookie}=${encodeURIComponent(locale)}; path=/; max-age=${maxAge}; samesite=lax`;
|
|
27
|
+
document.documentElement.lang = locale;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { LOCALE_COOKIE, switchLocale, verbalyHandle };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { negotiateLocale } from 'verbaly';\nimport type { Verbaly } from 'verbaly';\n\n// same name as core's localStorage key β one identity per user across channels\nexport const LOCALE_COOKIE = 'verbaly-locale';\nconst LANG_PLACEHOLDER = '%verbaly.lang%';\nconst YEAR = 31536000;\n\n// structural subset of @sveltejs/kit β no runtime or type dependency on kit\ninterface HandleEvent {\n request: Request;\n cookies: { get(name: string): string | undefined };\n locals: { verbalyLocale?: string };\n}\ninterface ResolveOptions {\n transformPageChunk?(input: { html: string; done: boolean }): string | undefined;\n}\ninterface HandleInput {\n event: HandleEvent;\n resolve(event: HandleEvent, opts?: ResolveOptions): Response | Promise<Response>;\n}\n\nexport interface VerbalyHandleOptions {\n /** supported locales β pass `locales` from 'virtual:verbaly' */\n locales: string[];\n /** default when nothing matches (defaults to the first locale) */\n fallback?: string;\n /** cookie read for the user's choice; `false` = Accept-Language only */\n cookie?: string | false;\n}\n\n// server hook: cookie β Accept-Language β fallback, per request.\n// Sets event.locals.verbalyLocale and fills %verbaly.lang% in app.html.\nexport function verbalyHandle(options: VerbalyHandleOptions) {\n const { locales, cookie = LOCALE_COOKIE } = options;\n if (!Array.isArray(locales) || locales.length === 0) {\n throw new Error(\n \"[verbaly] verbalyHandle needs `locales` β pass the `locales` export from 'virtual:verbaly' \" +\n '(requires @verbaly/vite β₯0.16) or list them yourself: verbalyHandle({ locales: [...] })',\n );\n }\n const fallback = options.fallback ?? locales[0]!;\n\n return ({ event, resolve }: HandleInput): Response | Promise<Response> => {\n let locale: string | undefined;\n if (cookie) {\n const stored = event.cookies.get(cookie);\n // '' sentinel = no match β fall through to the header\n if (stored) locale = negotiateLocale(stored, locales, '') || undefined;\n }\n const resolved =\n locale ?? negotiateLocale(event.request.headers.get('accept-language'), locales, fallback);\n\n event.locals.verbalyLocale = resolved;\n return resolve(event, {\n transformPageChunk: ({ html }) => html.replaceAll(LANG_PLACEHOLDER, resolved),\n });\n };\n}\n\nexport interface SwitchLocaleOptions {\n /** cookie written so the next SSR request matches; `false` = don't persist */\n cookie?: string | false;\n /** cookie lifetime in seconds (default 1 year) */\n maxAge?: number;\n}\n\n// client-side switch: catalog first (no flash), then locale, then persistence\nexport async function switchLocale(\n instance: Pick<Verbaly, 'loadLocale' | 'setLocale'>,\n locale: string,\n options: SwitchLocaleOptions = {},\n): Promise<void> {\n const { cookie = LOCALE_COOKIE, maxAge = YEAR } = options;\n await instance.loadLocale(locale);\n instance.setLocale(locale);\n if (typeof document === 'undefined') return; // SSR-safe no-op\n if (cookie) {\n document.cookie = `${cookie}=${encodeURIComponent(locale)}; path=/; max-age=${maxAge}; samesite=lax`;\n }\n document.documentElement.lang = locale;\n}\n"],"mappings":";;AAIA,MAAa,gBAAgB;AAC7B,MAAM,mBAAmB;AACzB,MAAM,OAAO;AA2Bb,SAAgB,cAAc,SAA+B;CAC3D,MAAM,EAAE,SAAS,SAAS,kBAAkB;CAC5C,IAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,QAAQ,WAAW,GAChD,MAAM,IAAI,MACR,oLAEF;CAEF,MAAM,WAAW,QAAQ,YAAY,QAAQ;CAE7C,QAAQ,EAAE,OAAO,cAAyD;EACxE,IAAI;EACJ,IAAI,QAAQ;GACV,MAAM,SAAS,MAAM,QAAQ,IAAI,MAAM;GAEvC,IAAI,QAAQ,SAAS,gBAAgB,QAAQ,SAAS,EAAE,KAAK,KAAA;EAC/D;EACA,MAAM,WACJ,UAAU,gBAAgB,MAAM,QAAQ,QAAQ,IAAI,iBAAiB,GAAG,SAAS,QAAQ;EAE3F,MAAM,OAAO,gBAAgB;EAC7B,OAAO,QAAQ,OAAO,EACpB,qBAAqB,EAAE,WAAW,KAAK,WAAW,kBAAkB,QAAQ,EAC9E,CAAC;CACH;AACF;AAUA,eAAsB,aACpB,UACA,QACA,UAA+B,CAAC,GACjB;CACf,MAAM,EAAE,SAAS,eAAe,SAAS,SAAS;CAClD,MAAM,SAAS,WAAW,MAAM;CAChC,SAAS,UAAU,MAAM;CACzB,IAAI,OAAO,aAAa,aAAa;CACrC,IAAI,QACF,SAAS,SAAS,GAAG,OAAO,GAAG,mBAAmB,MAAM,EAAE,oBAAoB,OAAO;CAEvF,SAAS,gBAAgB,OAAO;AAClC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@verbaly/sveltekit",
|
|
3
|
+
"version": "0.16.0",
|
|
4
|
+
"description": "SvelteKit SSR integration for Verbaly β per-request locale negotiation and flash-free hydration.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"i18n",
|
|
7
|
+
"sveltekit",
|
|
8
|
+
"ssr",
|
|
9
|
+
"hydration",
|
|
10
|
+
"verbaly"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Aron Soto",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/AronSoto/verbaly.git",
|
|
17
|
+
"directory": "packages/sveltekit"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://verbaly-web.vercel.app/docs/sveltekit",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/AronSoto/verbaly/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"sideEffects": false,
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"verbaly": "^0.16.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@sveltejs/kit": "^2.69.2",
|
|
47
|
+
"happy-dom": "^20.10.6",
|
|
48
|
+
"svelte": "^5.56.4",
|
|
49
|
+
"verbaly": "0.16.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsdown",
|
|
53
|
+
"dev": "tsdown --watch",
|
|
54
|
+
"test": "vitest run --passWithNoTests",
|
|
55
|
+
"typecheck": "tsc --noEmit"
|
|
56
|
+
}
|
|
57
|
+
}
|