next-language-selector 0.3.1 → 0.4.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/README.md CHANGED
@@ -9,6 +9,19 @@
9
9
  A lightweight, unstyled language selector for Next.js (App Router & Pages Router).
10
10
  Manages the `NEXT_LOCALE` cookie and works with `next-intl` or any i18n solution.
11
11
 
12
+ **[Live demo →](https://kirilinsky.github.io/next-language-selector/)**
13
+
14
+ - [Installation](#installation)
15
+ - [Basic Usage](#basic-usage)
16
+ - [Styling](#styling)
17
+ - [Custom UI](#custom-ui)
18
+ - [Dropdown mode](#dropdown-mode)
19
+ - [Setup with next-intl](#setup-with-next-intl)
20
+ - [Without full reloads](#without-full-reloads)
21
+ - [Props](#props)
22
+ - [`setLocaleCookie` utility](#setlocalecookie-utility)
23
+ - [SSR & hydration](#ssr--hydration)
24
+
12
25
  ## Key Features
13
26
 
14
27
  - **Next.js Native**: Built for the Next.js ecosystem (App Router & Pages Router).
@@ -142,15 +155,41 @@ export default createMiddleware({
142
155
  });
143
156
  ```
144
157
 
158
+ ## Without full reloads
159
+
160
+ By default the page reloads after a locale change so the server picks up the new cookie. For a smoother UX combine `autoReload={false}` with the `onChange` callback and the Next.js router:
161
+
162
+ ```tsx
163
+ "use client";
164
+
165
+ import { useRouter } from "next/navigation";
166
+ import { LanguageSelector } from "next-language-selector";
167
+
168
+ export function LocaleSwitch() {
169
+ const router = useRouter();
170
+ return (
171
+ <LanguageSelector
172
+ locales={locales}
173
+ defaultLocale="en"
174
+ autoReload={false}
175
+ onChange={() => router.refresh()}
176
+ />
177
+ );
178
+ }
179
+ ```
180
+
181
+ `onChange` fires with the selected code before the cookie is written (and before the reload when `autoReload` is on) — also handy for analytics.
182
+
145
183
  ## Props
146
184
 
147
- | Prop | Type | Default | Description |
148
- | :-------------- | :--------------- | :------------- | :--------------------------------------------------- |
149
- | `locales` | `LocaleConfig[]` | **Required** | Array of `{ name, code, flag? }` objects |
150
- | `defaultLocale` | `string` | **Required** | Fallback locale code |
151
- | `isDropdown` | `boolean` | `false` | Render as `<select>` instead of buttons |
152
- | `autoReload` | `boolean` | `true` | Reload page after cookie change |
153
- | `cookieName` | `string` | `NEXT_LOCALE` | Cookie name to store the selected locale |
185
+ | Prop | Type | Default | Description |
186
+ | :-------------- | :----------------------- | :------------- | :--------------------------------------------------- |
187
+ | `locales` | `LocaleConfig[]` | **Required** | Array of `{ name, code, flag? }` objects |
188
+ | `defaultLocale` | `string` | **Required** | Fallback locale code |
189
+ | `isDropdown` | `boolean` | `false` | Render as `<select>` instead of buttons |
190
+ | `autoReload` | `boolean` | `true` | Reload page after cookie change |
191
+ | `onChange` | `(code: string) => void` | - | Called on selection, before cookie write/reload |
192
+ | `cookieName` | `string` | `NEXT_LOCALE` | Cookie name to store the selected locale |
154
193
  | `className` | `string` | - | CSS class for the wrapper `<div>` or `<select>` |
155
194
  | `itemClassName` | `string` | - | CSS class for each `<button>` or `<option>` |
156
195
  | `renderCustom` | `Function` | - | Render prop for fully custom UI |
@@ -165,6 +204,26 @@ interface LocaleConfig {
165
204
  }
166
205
  ```
167
206
 
207
+ ## `setLocaleCookie` utility
208
+
209
+ The cookie writer is exported separately — useful if you want to switch the locale from your own code (a settings page, a keyboard shortcut, etc.) without rendering the component:
210
+
211
+ ```ts
212
+ import { setLocaleCookie } from "next-language-selector";
213
+
214
+ // setLocaleCookie(locale, cookieName?, autoReload?)
215
+ setLocaleCookie("de"); // sets NEXT_LOCALE=de and reloads
216
+ setLocaleCookie("de", "MY_LOCALE", false); // custom cookie, no reload
217
+ ```
218
+
219
+ The name and value are URI-encoded (cookie-injection safe), written with `max-age=31536000; path=/; SameSite=Lax`. On the server it is a no-op.
220
+
221
+ ## SSR & hydration
222
+
223
+ The component renders `null` until it has mounted and read the cookie on the client, so server and client markup never mismatch. Expect the selector to appear only after hydration — reserve space with CSS if layout shift matters. Malformed or unknown cookie values are ignored and `defaultLocale` is used.
224
+
225
+ Buttons are rendered with `type="button"`, so placing the selector inside a `<form>` won't trigger submits.
226
+
168
227
  ## License
169
228
 
170
229
  MIT
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var f=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var E=Object.getOwnPropertyNames;var x=Object.prototype.hasOwnProperty;var h=(t,o)=>{for(var a in o)f(t,a,{get:o[a],enumerable:!0})},P=(t,o,a,n)=>{if(o&&typeof o=="object"||typeof o=="function")for(let c of E(o))!x.call(t,c)&&c!==a&&f(t,c,{get:()=>o[c],enumerable:!(n=v(o,c))||n.enumerable});return t};var b=t=>P(f({},"__esModule",{value:!0}),t);var I={};h(I,{LanguageSelector:()=>w,setLocaleCookie:()=>i});module.exports=b(I);var s=require("react");var i=(t,o="NEXT_LOCALE",a=!0)=>{if(typeof document>"u")return;let n=encodeURIComponent(o),c=encodeURIComponent(t);document.cookie=`${n}=${c}; max-age=31536000; path=/; SameSite=Lax`,a&&window.location.reload()};var r=require("react/jsx-runtime");function N(t){let{locales:o,defaultLocale:a,cookieName:n="NEXT_LOCALE",isDropdown:c=!1,autoReload:g=!0,renderCustom:L,className:C,itemClassName:S}=t,[R,y]=(0,s.useState)(!1),[l,u]=(0,s.useState)(a);(0,s.useEffect)(()=>{let e=encodeURIComponent(n),k=document.cookie.split("; ").find(d=>d.startsWith(`${e}=`)),p=k?decodeURIComponent(k.split("=").slice(1).join("=")):null;p&&o.some(d=>d.code===p)?u(p):u(a),y(!0)},[n,a,o]);let m=(0,s.useCallback)(e=>{u(e),i(e,n,g)},[n,g]);return R?L?(0,r.jsx)(r.Fragment,{children:L({locales:o,currentLocale:l,onChange:m})}):c?(0,r.jsx)("select",{value:l,onChange:e=>m(e.target.value),className:C,children:o.map(e=>(0,r.jsxs)("option",{value:e.code,className:S,children:[e.flag," ",e.name]},e.code))}):(0,r.jsx)("div",{className:C,children:o.map(e=>(0,r.jsxs)("button",{onClick:()=>m(e.code),"data-active":l===e.code,"aria-pressed":l===e.code,className:S,children:[e.flag," ",e.name]},e.code))}):null}var w=N;0&&(module.exports={LanguageSelector,setLocaleCookie});
1
+ "use strict";var p=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var v=Object.prototype.hasOwnProperty;var x=(t,o)=>{for(var n in o)p(t,n,{get:o[n],enumerable:!0})},b=(t,o,n,a)=>{if(o&&typeof o=="object"||typeof o=="function")for(let r of h(o))!v.call(t,r)&&r!==n&&p(t,r,{get:()=>o[r],enumerable:!(a=R(o,r))||a.enumerable});return t};var P=t=>b(p({},"__esModule",{value:!0}),t);var w={};x(w,{LanguageSelector:()=>X,setLocaleCookie:()=>u});module.exports=P(w);var s=require("react");var S=(t="NEXT_LOCALE")=>{if(typeof document>"u")return null;let o=encodeURIComponent(t),n=document.cookie.split(";").map(a=>a.trim()).find(a=>a.startsWith(`${o}=`));if(!n)return null;try{return decodeURIComponent(n.slice(o.length+1))}catch{return null}},u=(t,o="NEXT_LOCALE",n=!0)=>{if(typeof document>"u")return;let a=encodeURIComponent(o),r=encodeURIComponent(t);document.cookie=`${a}=${r}; max-age=31536000; path=/; SameSite=Lax`,n&&window.location.reload()};var c=require("react/jsx-runtime");function k(t){let{locales:o,defaultLocale:n,cookieName:a="NEXT_LOCALE",isDropdown:r=!1,autoReload:d=!0,onChange:f,renderCustom:g,className:L,itemClassName:C}=t,[y,N]=(0,s.useState)(!1),[l,i]=(0,s.useState)(n);(0,s.useEffect)(()=>{let e=S(a);e&&o.some(E=>E.code===e)?i(e):i(n),N(!0)},[a,n,o]);let m=(0,s.useCallback)(e=>{i(e),f?.(e),u(e,a,d)},[a,d,f]);return y?g?(0,c.jsx)(c.Fragment,{children:g({locales:o,currentLocale:l,onChange:m})}):r?(0,c.jsx)("select",{value:l,onChange:e=>m(e.target.value),className:L,children:o.map(e=>(0,c.jsxs)("option",{value:e.code,className:C,children:[e.flag," ",e.name]},e.code))}):(0,c.jsx)("div",{className:L,children:o.map(e=>(0,c.jsxs)("button",{type:"button",onClick:()=>m(e.code),"data-active":l===e.code,"aria-pressed":l===e.code,className:C,children:[e.flag," ",e.name]},e.code))}):null}var X=k;0&&(module.exports={LanguageSelector,setLocaleCookie});
package/dist/index.d.mts CHANGED
@@ -15,6 +15,12 @@ interface LanguageSelectorProps {
15
15
  className?: string;
16
16
  itemClassName?: string;
17
17
  autoReload?: boolean;
18
+ /**
19
+ * Called with the selected locale code after the internal state updates,
20
+ * before the cookie is written (and before the reload when autoReload is on).
21
+ * Use for analytics, router navigation or other side effects.
22
+ */
23
+ onChange?: (code: string) => void;
18
24
  renderCustom?: (props: {
19
25
  locales: LocaleConfig[];
20
26
  currentLocale: string;
package/dist/index.d.ts CHANGED
@@ -15,6 +15,12 @@ interface LanguageSelectorProps {
15
15
  className?: string;
16
16
  itemClassName?: string;
17
17
  autoReload?: boolean;
18
+ /**
19
+ * Called with the selected locale code after the internal state updates,
20
+ * before the cookie is written (and before the reload when autoReload is on).
21
+ * Use for analytics, router navigation or other side effects.
22
+ */
23
+ onChange?: (code: string) => void;
18
24
  renderCustom?: (props: {
19
25
  locales: LocaleConfig[];
20
26
  currentLocale: string;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{useCallback as v,useEffect as E,useState as S}from"react";var m=(c,o="NEXT_LOCALE",a=!0)=>{if(typeof document>"u")return;let t=encodeURIComponent(o),r=encodeURIComponent(c);document.cookie=`${t}=${r}; max-age=31536000; path=/; SameSite=Lax`,a&&window.location.reload()};import{Fragment as x,jsx as p,jsxs as k}from"react/jsx-runtime";function N(c){let{locales:o,defaultLocale:a,cookieName:t="NEXT_LOCALE",isDropdown:r=!1,autoReload:d=!0,renderCustom:f,className:g,itemClassName:L}=c,[R,y]=S(!1),[n,s]=S(a);E(()=>{let e=encodeURIComponent(t),C=document.cookie.split("; ").find(u=>u.startsWith(`${e}=`)),i=C?decodeURIComponent(C.split("=").slice(1).join("=")):null;i&&o.some(u=>u.code===i)?s(i):s(a),y(!0)},[t,a,o]);let l=v(e=>{s(e),m(e,t,d)},[t,d]);return R?f?p(x,{children:f({locales:o,currentLocale:n,onChange:l})}):r?p("select",{value:n,onChange:e=>l(e.target.value),className:g,children:o.map(e=>k("option",{value:e.code,className:L,children:[e.flag," ",e.name]},e.code))}):p("div",{className:g,children:o.map(e=>k("button",{onClick:()=>l(e.code),"data-active":n===e.code,"aria-pressed":n===e.code,className:L,children:[e.flag," ",e.name]},e.code))}):null}var A=N;export{A as LanguageSelector,m as setLocaleCookie};
1
+ import{useCallback as R,useEffect as h,useState as C}from"react";var L=(a="NEXT_LOCALE")=>{if(typeof document>"u")return null;let o=encodeURIComponent(a),n=document.cookie.split(";").map(t=>t.trim()).find(t=>t.startsWith(`${o}=`));if(!n)return null;try{return decodeURIComponent(n.slice(o.length+1))}catch{return null}},u=(a,o="NEXT_LOCALE",n=!0)=>{if(typeof document>"u")return;let t=encodeURIComponent(o),c=encodeURIComponent(a);document.cookie=`${t}=${c}; max-age=31536000; path=/; SameSite=Lax`,n&&window.location.reload()};import{Fragment as v,jsx as i,jsxs as S}from"react/jsx-runtime";function k(a){let{locales:o,defaultLocale:n,cookieName:t="NEXT_LOCALE",isDropdown:c=!1,autoReload:m=!0,onChange:p,renderCustom:d,className:f,itemClassName:g}=a,[y,N]=C(!1),[r,s]=C(n);h(()=>{let e=L(t);e&&o.some(E=>E.code===e)?s(e):s(n),N(!0)},[t,n,o]);let l=R(e=>{s(e),p?.(e),u(e,t,m)},[t,m,p]);return y?d?i(v,{children:d({locales:o,currentLocale:r,onChange:l})}):c?i("select",{value:r,onChange:e=>l(e.target.value),className:f,children:o.map(e=>S("option",{value:e.code,className:g,children:[e.flag," ",e.name]},e.code))}):i("div",{className:f,children:o.map(e=>S("button",{type:"button",onClick:()=>l(e.code),"data-active":r===e.code,"aria-pressed":r===e.code,className:g,children:[e.flag," ",e.name]},e.code))}):null}var O=k;export{O as LanguageSelector,u as setLocaleCookie};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-language-selector",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Configurable language selector for Next.js",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -18,7 +18,8 @@
18
18
  "lint": "tsc --noEmit",
19
19
  "test": "vitest",
20
20
  "test:run": "vitest run",
21
- "test:coverage": "npx vitest run --coverage"
21
+ "test:coverage": "npx vitest run --coverage",
22
+ "demo:build": "esbuild demo/main.tsx --bundle --minify --jsx=automatic --outfile=dist-demo/main.js && cp demo/index.html demo/og.png dist-demo/"
22
23
  },
23
24
  "files": [
24
25
  "dist"
@@ -39,7 +40,7 @@
39
40
  ],
40
41
  "author": "Kirilinsky (https://github.com/kirilinsky)",
41
42
  "license": "MIT",
42
- "homepage": "https://github.com/kirilinsky/next-language-selector#readme",
43
+ "homepage": "https://kirilinsky.github.io/next-language-selector/",
43
44
  "bugs": {
44
45
  "url": "https://github.com/kirilinsky/next-language-selector/issues"
45
46
  },
@@ -52,13 +53,22 @@
52
53
  "@testing-library/react": "^16.3.2",
53
54
  "@testing-library/user-event": "^14.6.1",
54
55
  "@types/react": "^19.2.14",
56
+ "@types/react-dom": "^19.2.3",
55
57
  "@vitest/coverage-v8": "^4.1.2",
58
+ "esbuild": "^0.28.1",
56
59
  "happy-dom": "^20.8.9",
60
+ "react": "^19.2.7",
61
+ "react-dom": "^19.2.7",
57
62
  "tslib": "^2.8.1",
58
63
  "tsup": "^8.5.1",
59
64
  "typescript": "^5.9.3",
60
65
  "vitest": "^4.1.2"
61
66
  },
62
67
  "sideEffects": false,
63
- "packageManager": "pnpm@10.32.1"
64
- }
68
+ "packageManager": "pnpm@10.32.1",
69
+ "pnpm": {
70
+ "onlyBuiltDependencies": [
71
+ "esbuild"
72
+ ]
73
+ }
74
+ }