next-language-selector 0.2.7 β†’ 0.3.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
@@ -5,15 +5,16 @@
5
5
  ![security score](https://socket.dev/api/badge/npm/package/next-language-selector?style=flat-square)
6
6
  ![license](https://img.shields.io/npm/l/next-language-selector?color=gray&style=flat-square)
7
7
 
8
- A lightweight, configurable language selector for Next.js (App Router & Pages Router).
9
- Efficiently manages the `NEXT_LOCALE` cookie and works seamlessly with `next-intl` or any other i18n solution.
8
+ A lightweight, unstyled language selector for Next.js (App Router & Pages Router).
9
+ Manages the `NEXT_LOCALE` cookie and works with `next-intl` or any i18n solution.
10
10
 
11
11
  ## Key Features
12
12
 
13
- - πŸš€ **Next.js Native**: Built specifically for the Next.js ecosystem.
14
- - πŸͺΆ **Zero Dependencies**: Keeps your bundle size minimal.
15
- - 🎨 **Fully Customizable**: Use built-in styles or your own UI (Shadcn UI, Radix, etc.) via render props.
16
- - πŸͺ **Cookie-based**: Automatically syncs with the `NEXT_LOCALE` cookie.
13
+ - **Next.js Native**: Built for the Next.js ecosystem (App Router & Pages Router).
14
+ - **Zero Dependencies**: No runtime deps β€” just React.
15
+ - **Unstyled by default**: Bring your own CSS, Tailwind, Shadcn, Radix β€” no style conflicts.
16
+ - **Cookie-based**: Reads and writes `NEXT_LOCALE` automatically.
17
+ - **Secure**: Cookie injection-safe, `SameSite=Lax` out of the box.
17
18
 
18
19
  ## Installation
19
20
 
@@ -25,7 +26,7 @@ npm install next-language-selector
25
26
 
26
27
  ## Basic Usage
27
28
 
28
- Just drop the component into your Footer or Navbar. It handles cookie updates and local state out of the box.
29
+ Drop the component into your Footer or Navbar. It handles cookie sync and state out of the box.
29
30
 
30
31
  ```tsx
31
32
  import { LanguageSelector } from "next-language-selector";
@@ -41,17 +42,55 @@ export default function Footer() {
41
42
  <LanguageSelector
42
43
  locales={locales}
43
44
  defaultLocale="en"
44
- activeColor="#3b82f6" // optional
45
- isDropdown={false} // Renders as a list of buttons
46
45
  />
47
46
  </footer>
48
47
  );
49
48
  }
50
49
  ```
51
50
 
52
- ## Custom UI (e.g., Shadcn UI / Headless)
51
+ ## Styling
53
52
 
54
- Use the `renderCustom` prop to take full control over the rendering while keeping the cookie management logic.
53
+ The component is **unstyled by default**. Use `className` / `itemClassName` props or target the `data-active` attribute:
54
+
55
+ ```css
56
+ /* Plain CSS */
57
+ .lang-btn {
58
+ background: none;
59
+ border: none;
60
+ cursor: pointer;
61
+ opacity: 0.5;
62
+ }
63
+
64
+ .lang-btn[data-active="true"] {
65
+ opacity: 1;
66
+ font-weight: 600;
67
+ border-bottom: 2px solid currentColor;
68
+ }
69
+ ```
70
+
71
+ ```tsx
72
+ <LanguageSelector
73
+ locales={locales}
74
+ defaultLocale="en"
75
+ className="flex gap-2"
76
+ itemClassName="lang-btn"
77
+ />
78
+ ```
79
+
80
+ With Tailwind:
81
+
82
+ ```tsx
83
+ <LanguageSelector
84
+ locales={locales}
85
+ defaultLocale="en"
86
+ className="flex items-center gap-3"
87
+ itemClassName="text-sm text-gray-400 data-[active=true]:text-black data-[active=true]:font-semibold"
88
+ />
89
+ ```
90
+
91
+ ## Custom UI
92
+
93
+ Use the `renderCustom` prop to take full control over rendering while keeping the cookie logic.
55
94
 
56
95
  ```tsx
57
96
  <LanguageSelector
@@ -63,11 +102,7 @@ Use the `renderCustom` prop to take full control over the rendering while keepin
63
102
  <button
64
103
  key={lang.code}
65
104
  onClick={() => onChange(lang.code)}
66
- className={
67
- currentLocale === lang.code
68
- ? "text-blue-600 font-bold"
69
- : "text-gray-500"
70
- }
105
+ className={currentLocale === lang.code ? "font-bold" : "opacity-50"}
71
106
  >
72
107
  {lang.flag} {lang.name}
73
108
  </button>
@@ -77,9 +112,20 @@ Use the `renderCustom` prop to take full control over the rendering while keepin
77
112
  />
78
113
  ```
79
114
 
80
- ## Setup with next-intl (Middleware)
115
+ ## Dropdown mode
116
+
117
+ ```tsx
118
+ <LanguageSelector
119
+ locales={locales}
120
+ defaultLocale="en"
121
+ isDropdown
122
+ className="border rounded px-2 py-1"
123
+ />
124
+ ```
125
+
126
+ ## Setup with next-intl
81
127
 
82
- To make sure Next.js detects the language from the cookie set by this component, update your `middleware.ts` or `proxy.ts`:
128
+ Update your `middleware.ts` to read the cookie set by this component:
83
129
 
84
130
  ```typescript
85
131
  import createMiddleware from "next-intl/middleware";
@@ -90,23 +136,33 @@ export default createMiddleware({
90
136
  localeCookie: {
91
137
  name: "NEXT_LOCALE",
92
138
  path: "/",
93
- maxAge: 31536000, // 1 year
139
+ maxAge: 31536000,
94
140
  },
95
141
  });
96
142
  ```
97
143
 
98
144
  ## Props
99
145
 
100
- | Prop | Type | Default | Description |
101
- | :-------------- | :--------------- | :------------ | :---------------------------------------------------------- |
102
- | `locales` | `LocaleConfig[]` | **Required** | Array of `{ name, code, flag }` objects |
103
- | `defaultLocale` | `string` | **Required** | Initial language code |
104
- | `isDropdown` | `boolean` | `false` | Toggle between `<select>` and a list of buttons |
105
- | `autoReload` | `boolean` | `true` | Trigger reload on cookie change |
106
- | `cookieName` | `string` | `NEXT_LOCALE` | Name of the cookie to store the selected language |
107
- | `activeColor` | `string` | `red` | Underline color for the active language (non-dropdown mode) |
108
- | `className` | `string` | - | CSS class for the wrapper element |
109
- | `renderCustom` | `Function` | - | Render prop for custom UI logic |
146
+ | Prop | Type | Default | Description |
147
+ | :-------------- | :--------------- | :------------- | :--------------------------------------------------- |
148
+ | `locales` | `LocaleConfig[]` | **Required** | Array of `{ name, code, flag? }` objects |
149
+ | `defaultLocale` | `string` | **Required** | Fallback locale code |
150
+ | `isDropdown` | `boolean` | `false` | Render as `<select>` instead of buttons |
151
+ | `autoReload` | `boolean` | `true` | Reload page after cookie change |
152
+ | `cookieName` | `string` | `NEXT_LOCALE` | Cookie name to store the selected locale |
153
+ | `className` | `string` | - | CSS class for the wrapper `<div>` or `<select>` |
154
+ | `itemClassName` | `string` | - | CSS class for each `<button>` or `<option>` |
155
+ | `renderCustom` | `Function` | - | Render prop for fully custom UI |
156
+
157
+ ### `LocaleConfig`
158
+
159
+ ```ts
160
+ interface LocaleConfig {
161
+ name: string; // Display name, e.g. "English"
162
+ code: string; // Locale code, e.g. "en"
163
+ flag?: string; // Optional emoji flag, e.g. "πŸ‡ΊπŸ‡Έ"
164
+ }
165
+ ```
110
166
 
111
167
  ## License
112
168
 
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var u=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var k=Object.getOwnPropertyNames;var y=Object.prototype.hasOwnProperty;var v=(t,o)=>{for(var a in o)u(t,a,{get:o[a],enumerable:!0})},E=(t,o,a,c)=>{if(o&&typeof o=="object"||typeof o=="function")for(let n of k(o))!y.call(t,n)&&n!==a&&u(t,n,{get:()=>o[n],enumerable:!(c=x(o,n))||c.enumerable});return t};var b=t=>E(u({},"__esModule",{value:!0}),t);var N={};v(N,{LanguageSelector:()=>h,setLocaleCookie:()=>i});module.exports=b(N);var p=require("react");var i=(t,o="NEXT_LOCALE",a=!0)=>{typeof document>"u"||(document.cookie=`${o}=${t}; max-age=31536000; path=/`,a&&window.location.reload())};var r=require("react/jsx-runtime");function f(t){let{locales:o,defaultLocale:a,cookieName:c="NEXT_LOCALE",isDropdown:n=!1,autoReload:L=!0,renderCustom:g,className:m,activeColor:C="red"}=t,[s,d]=(0,p.useState)(a);(0,p.useEffect)(()=>{let e=document.cookie.split("; ").find(S=>S.startsWith(`${c}=`))?.split("=")[1];e&&d(e)},[c]);let l=e=>{d(e),i(e,c,L)};return g?(0,r.jsx)(r.Fragment,{children:g({locales:o,currentLocale:s,onChange:l})}):n?(0,r.jsx)("select",{value:s,onChange:e=>l(e.target.value),className:m,children:o.map(e=>(0,r.jsxs)("option",{value:e.code,children:[e.flag," ",e.name]},e.code))}):(0,r.jsx)("div",{style:{display:"flex",gap:"8px",alignItems:"center"},className:m,children:o.map(e=>(0,r.jsxs)("button",{onClick:()=>l(e.code),style:{cursor:"pointer",background:"none",border:"none",borderBottom:s===e.code?`1px solid ${C}`:"1px solid transparent",color:s===e.code?"inherit":"gray",fontSize:"12px",padding:"2px 4px"},children:[e.flag," ",e.name]},e.code))})}var h=f;0&&(module.exports={LanguageSelector,setLocaleCookie});
1
+ "use strict";var m=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var E=Object.getOwnPropertyNames;var x=Object.prototype.hasOwnProperty;var h=(t,o)=>{for(var r in o)m(t,r,{get:o[r],enumerable:!0})},P=(t,o,r,a)=>{if(o&&typeof o=="object"||typeof o=="function")for(let n of E(o))!x.call(t,n)&&n!==r&&m(t,n,{get:()=>o[n],enumerable:!(a=v(o,n))||a.enumerable});return t};var b=t=>P(m({},"__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",r=!0)=>{if(typeof document>"u")return;let a=encodeURIComponent(o),n=encodeURIComponent(t);document.cookie=`${a}=${n}; max-age=31536000; path=/; SameSite=Lax`,r&&window.location.reload()};var c=require("react/jsx-runtime");function k(t){let{locales:o,defaultLocale:r,cookieName:a="NEXT_LOCALE",isDropdown:n=!1,autoReload:p=!0,renderCustom:d,className:f,itemClassName:g}=t,[N,R]=(0,s.useState)(!1),[l,L]=(0,s.useState)(r);(0,s.useEffect)(()=>{let e=encodeURIComponent(a),C=document.cookie.split("; ").find(y=>y.startsWith(`${e}=`)),S=C?decodeURIComponent(C.split("=").slice(1).join("=")):null;S&&L(S),R(!0)},[a]);let u=(0,s.useCallback)(e=>{L(e),i(e,a,p)},[a,p]);return N?d?(0,c.jsx)(c.Fragment,{children:d({locales:o,currentLocale:l,onChange:u})}):n?(0,c.jsx)("select",{value:l,onChange:e=>u(e.target.value),className:f,children:o.map(e=>(0,c.jsxs)("option",{value:e.code,className:g,children:[e.flag," ",e.name]},e.code))}):(0,c.jsx)("div",{className:f,children:o.map(e=>(0,c.jsxs)("button",{onClick:()=>u(e.code),"data-active":l===e.code,"aria-pressed":l===e.code,className:g,children:[e.flag," ",e.name]},e.code))}):null}var w=k;0&&(module.exports={LanguageSelector,setLocaleCookie});
package/dist/index.d.mts CHANGED
@@ -12,8 +12,8 @@ interface LanguageSelectorProps {
12
12
  defaultLocale: string;
13
13
  isDropdown?: boolean;
14
14
  cookieName?: string;
15
- activeColor?: string;
16
15
  className?: string;
16
+ itemClassName?: string;
17
17
  autoReload?: boolean;
18
18
  renderCustom?: (props: {
19
19
  locales: LocaleConfig[];
@@ -22,6 +22,6 @@ interface LanguageSelectorProps {
22
22
  }) => ReactNode;
23
23
  }
24
24
 
25
- declare const LanguageSelector: (props: LanguageSelectorProps) => React.JSX.Element;
25
+ declare const LanguageSelector: (props: LanguageSelectorProps) => React.JSX.Element | null;
26
26
 
27
27
  export { LanguageSelector, type LanguageSelectorProps, type LocaleConfig, setLocaleCookie };
package/dist/index.d.ts CHANGED
@@ -12,8 +12,8 @@ interface LanguageSelectorProps {
12
12
  defaultLocale: string;
13
13
  isDropdown?: boolean;
14
14
  cookieName?: string;
15
- activeColor?: string;
16
15
  className?: string;
16
+ itemClassName?: string;
17
17
  autoReload?: boolean;
18
18
  renderCustom?: (props: {
19
19
  locales: LocaleConfig[];
@@ -22,6 +22,6 @@ interface LanguageSelectorProps {
22
22
  }) => ReactNode;
23
23
  }
24
24
 
25
- declare const LanguageSelector: (props: LanguageSelectorProps) => React.JSX.Element;
25
+ declare const LanguageSelector: (props: LanguageSelectorProps) => React.JSX.Element | null;
26
26
 
27
27
  export { LanguageSelector, type LanguageSelectorProps, type LocaleConfig, setLocaleCookie };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{useEffect as S,useState as x}from"react";var s=(r,o="NEXT_LOCALE",a=!0)=>{typeof document>"u"||(document.cookie=`${o}=${r}; max-age=31536000; path=/`,a&&window.location.reload())};import{Fragment as k,jsx as i,jsxs as g}from"react/jsx-runtime";function m(r){let{locales:o,defaultLocale:a,cookieName:n="NEXT_LOCALE",isDropdown:d=!1,autoReload:f=!0,renderCustom:p,className:l,activeColor:L="red"}=r,[t,u]=x(a);S(()=>{let e=document.cookie.split("; ").find(C=>C.startsWith(`${n}=`))?.split("=")[1];e&&u(e)},[n]);let c=e=>{u(e),s(e,n,f)};return p?i(k,{children:p({locales:o,currentLocale:t,onChange:c})}):d?i("select",{value:t,onChange:e=>c(e.target.value),className:l,children:o.map(e=>g("option",{value:e.code,children:[e.flag," ",e.name]},e.code))}):i("div",{style:{display:"flex",gap:"8px",alignItems:"center"},className:l,children:o.map(e=>g("button",{onClick:()=>c(e.code),style:{cursor:"pointer",background:"none",border:"none",borderBottom:t===e.code?`1px solid ${L}`:"1px solid transparent",color:t===e.code?"inherit":"gray",fontSize:"12px",padding:"2px 4px"},children:[e.flag," ",e.name]},e.code))})}var X=m;export{X as LanguageSelector,s as setLocaleCookie};
1
+ import{useCallback as v,useEffect as E,useState as C}from"react";var l=(n,t="NEXT_LOCALE",c=!0)=>{if(typeof document>"u")return;let o=encodeURIComponent(t),r=encodeURIComponent(n);document.cookie=`${o}=${r}; max-age=31536000; path=/; SameSite=Lax`,c&&window.location.reload()};import{Fragment as x,jsx as i,jsxs as S}from"react/jsx-runtime";function k(n){let{locales:t,defaultLocale:c,cookieName:o="NEXT_LOCALE",isDropdown:r=!1,autoReload:u=!0,renderCustom:m,className:p,itemClassName:d}=n,[N,R]=C(!1),[a,f]=C(c);E(()=>{let e=encodeURIComponent(o),g=document.cookie.split("; ").find(y=>y.startsWith(`${e}=`)),L=g?decodeURIComponent(g.split("=").slice(1).join("=")):null;L&&f(L),R(!0)},[o]);let s=v(e=>{f(e),l(e,o,u)},[o,u]);return N?m?i(x,{children:m({locales:t,currentLocale:a,onChange:s})}):r?i("select",{value:a,onChange:e=>s(e.target.value),className:p,children:t.map(e=>S("option",{value:e.code,className:d,children:[e.flag," ",e.name]},e.code))}):i("div",{className:p,children:t.map(e=>S("button",{onClick:()=>s(e.code),"data-active":a===e.code,"aria-pressed":a===e.code,className:d,children:[e.flag," ",e.name]},e.code))}):null}var A=k;export{A as LanguageSelector,l as setLocaleCookie};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-language-selector",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "description": "Configurable language selector for Next.js",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -10,23 +10,21 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.mjs",
12
12
  "require": "./dist/index.cjs"
13
- },
14
- "./selector": {
15
- "types": "./dist/selector.d.ts",
16
- "import": "./dist/selector.mjs",
17
- "require": "./dist/selector.cjs"
18
13
  }
19
14
  },
20
15
  "scripts": {
21
16
  "build": "tsup",
22
17
  "dev": "tsup --watch",
23
- "lint": "tsc --noEmit"
18
+ "lint": "tsc --noEmit",
19
+ "test": "vitest",
20
+ "test:run": "vitest run",
21
+ "test:coverage": "npx vitest run --coverage"
24
22
  },
25
23
  "files": [
26
24
  "dist"
27
25
  ],
28
26
  "peerDependencies": {
29
- "next": "^13.5.11 || ^14.0.0 || ^15.0.0 || ^16.0.0",
27
+ "next": "^15.0.0 || ^16.0.0",
30
28
  "react": "^18.0.0 || ^19.0.0"
31
29
  },
32
30
  "keywords": [
@@ -50,10 +48,16 @@
50
48
  "url": "git+https://github.com/kirilinsky/next-language-selector.git"
51
49
  },
52
50
  "devDependencies": {
51
+ "@testing-library/jest-dom": "^6.9.1",
52
+ "@testing-library/react": "^16.3.2",
53
+ "@testing-library/user-event": "^14.6.1",
53
54
  "@types/react": "^19.2.14",
55
+ "@vitest/coverage-v8": "^4.1.2",
56
+ "happy-dom": "^20.8.9",
54
57
  "tslib": "^2.8.1",
55
58
  "tsup": "^8.5.1",
56
- "typescript": "^5.9.3"
59
+ "typescript": "^5.9.3",
60
+ "vitest": "^4.1.2"
57
61
  },
58
62
  "sideEffects": false
59
63
  }