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 +85 -29
- package/dist/index.cjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +1 -1
- package/package.json +13 -9
package/README.md
CHANGED
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|

|
|
6
6
|

|
|
7
7
|
|
|
8
|
-
A lightweight,
|
|
9
|
-
|
|
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
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
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
|
-
|
|
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
|
-
##
|
|
51
|
+
## Styling
|
|
53
52
|
|
|
54
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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,
|
|
139
|
+
maxAge: 31536000,
|
|
94
140
|
},
|
|
95
141
|
});
|
|
96
142
|
```
|
|
97
143
|
|
|
98
144
|
## Props
|
|
99
145
|
|
|
100
|
-
| Prop | Type | Default
|
|
101
|
-
| :-------------- | :--------------- |
|
|
102
|
-
| `locales` | `LocaleConfig[]` | **Required**
|
|
103
|
-
| `defaultLocale` | `string` | **Required**
|
|
104
|
-
| `isDropdown` | `boolean` | `false`
|
|
105
|
-
| `autoReload` | `boolean` | `true`
|
|
106
|
-
| `cookieName` | `string` | `NEXT_LOCALE`
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `renderCustom` | `Function` | -
|
|
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
|
|
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
|
|
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.
|
|
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": "^
|
|
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
|
}
|