Skip to content

Commit 63ab528

Browse files
authored
Merge pull request #1 from unlibra/develop
Develop
2 parents ce3c1b6 + 3b28155 commit 63ab528

19 files changed

+9464
-1
lines changed

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
node-version: [18, 20]
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Use Node.js ${{ matrix.node-version }}
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version: ${{ matrix.node-version }}
22+
23+
- name: Install dependencies
24+
run: npm ci
25+
26+
- name: Type check
27+
run: npm run type-check
28+
29+
- name: Lint
30+
run: npm run lint
31+
32+
- name: Test
33+
run: npm run test
34+
35+
- name: Build
36+
run: npm run build

.github/workflows/publish.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Publish to npm
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
id-token: write
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: '20'
20+
registry-url: 'https://registry.npmjs.org'
21+
22+
- name: Install dependencies
23+
run: npm ci
24+
25+
- name: Type check
26+
run: npm run type-check
27+
28+
- name: Lint
29+
run: npm run lint
30+
31+
- name: Test
32+
run: npm run test
33+
34+
- name: Build
35+
run: npm run build
36+
37+
- name: Publish to npm
38+
run: npm publish --provenance --access public
39+
env:
40+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Dependencies
2+
node_modules/
3+
4+
# Build outputs
5+
dist/
6+
*.tsbuildinfo
7+
8+
# Environment files
9+
.env
10+
.env.local
11+
.env.*.local
12+
13+
# IDE
14+
.vscode/
15+
.idea/
16+
*.swp
17+
*.swo
18+
*~
19+
20+
# OS
21+
.DS_Store
22+
Thumbs.db
23+
24+
# Logs
25+
*.log
26+
npm-debug.log*
27+
yarn-debug.log*
28+
yarn-error.log*
29+
pnpm-debug.log*
30+
31+
# Testing
32+
coverage/
33+
.nyc_output/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 unlibra
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.

README.md

Lines changed: 258 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,258 @@
1-
# next-i18n-tiny
1+
# next-i18n-tiny
2+
3+
[![npm version](https://img.shields.io/npm/v/next-i18n-tiny.svg)](https://www.npmjs.com/package/next-i18n-tiny)
4+
<!-- [![npm downloads](https://img.shields.io/npm/dm/next-i18n-tiny.svg)](https://www.npmjs.com/package/next-i18n-tiny) -->
5+
[![CI](https://github.com/unlibra/next-i18n-tiny/workflows/CI/badge.svg)](https://github.com/unlibra/next-i18n-tiny/actions)
6+
[![License](https://img.shields.io/npm/l/next-i18n-tiny.svg)](https://github.com/unlibra/next-i18n-tiny/blob/main/LICENSE)
7+
[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
8+
9+
Type-safe, zero-dependency i18n library for Next.js App Router with React Server Components support.
10+
11+
Inspired by next-intl, designed for simplicity and type safety.
12+
13+
## Features
14+
15+
- **Type-safe**: Full TypeScript support with **automatic type inference** - autocomplete for `messages.site.name`, `t('common.title')`, and all nested keys
16+
- **Zero dependencies**: No external i18n libraries needed
17+
- **Server Components**: Native RSC support
18+
- **Simple API**: Single configuration, minimal boilerplate
19+
- **Small**: Minimal bundle size
20+
- **No global state**: Pure function factory pattern
21+
22+
## Installation
23+
24+
```bash
25+
npm install next-i18n-tiny
26+
# or
27+
pnpm add next-i18n-tiny
28+
# or
29+
yarn add next-i18n-tiny
30+
```
31+
32+
## Usage
33+
34+
### Project Structure
35+
36+
```
37+
your-app/
38+
├── app/
39+
│ └── [locale]/
40+
│ ├── layout.tsx
41+
│ └── page.tsx
42+
├── messages/
43+
│ ├── en.ts
44+
│ └── ja.ts
45+
├── i18n.ts
46+
└── proxy.ts
47+
```
48+
49+
### Minimal Setup
50+
51+
**1. Create message files**
52+
53+
```typescript
54+
// messages/en.ts
55+
export default {
56+
common: {
57+
title: "My Site",
58+
description: "Welcome to my site"
59+
},
60+
nav: {
61+
home: "Home",
62+
about: "About"
63+
}
64+
}
65+
```
66+
67+
```typescript
68+
// messages/ja.ts
69+
export default {
70+
common: {
71+
title: "マイサイト",
72+
description: "サイトへようこそ"
73+
},
74+
nav: {
75+
home: "ホーム",
76+
about: "概要"
77+
}
78+
}
79+
```
80+
81+
**2. Define i18n instance**
82+
83+
Place this file anywhere in your project (`i18n.ts`, `lib/i18n.ts`, etc.)
84+
85+
```typescript
86+
// i18n.ts
87+
import { define } from 'next-i18n-tiny'
88+
import enMessages from '@/messages/en'
89+
import jaMessages from '@/messages/ja'
90+
91+
export type Locale = 'ja' | 'en'
92+
const locales: Locale[] = ['ja', 'en']
93+
const defaultLocale: Locale = 'ja'
94+
95+
const { client, server, Link, Provider } = define({
96+
locales,
97+
defaultLocale,
98+
messages: { ja: jaMessages, en: enMessages }
99+
})
100+
101+
export { Link, Provider }
102+
export const { useMessages, useTranslations, useLocale } = client
103+
export const { getMessages, getTranslations } = server
104+
```
105+
106+
**3. Setup Proxy** (Next.js 16+)
107+
108+
> For Next.js 15 or earlier, use `middleware.ts` instead. See [official migration guide](https://nextjs.org/docs/messages/middleware-to-proxy).
109+
110+
```typescript
111+
// proxy.ts
112+
import { NextRequest, NextResponse } from 'next/server'
113+
114+
const locales = ['ja', 'en']
115+
const defaultLocale = 'ja'
116+
117+
export default function proxy(request: NextRequest) {
118+
const { pathname } = request.nextUrl
119+
120+
// Check if pathname already has a locale
121+
const hasLocale = locales.some(
122+
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
123+
)
124+
125+
if (hasLocale) return
126+
127+
// Redirect to default locale
128+
request.nextUrl.pathname = `/${defaultLocale}${pathname}`
129+
return NextResponse.redirect(request.nextUrl)
130+
}
131+
132+
export const config = {
133+
matcher: ['/((?!api|_next|.*\\..*).*)']
134+
}
135+
```
136+
137+
**4. Use in Layout**
138+
139+
```typescript
140+
// app/[locale]/layout.tsx
141+
import { Provider, getMessages, type Locale } from '@/i18n'
142+
143+
export default async function Layout({ children, params }) {
144+
const { locale } = await params
145+
const messages = await getMessages(locale)
146+
147+
return (
148+
<Provider locale={locale} messages={messages}>
149+
{children}
150+
</Provider>
151+
)
152+
}
153+
```
154+
155+
**5. Use in Components**
156+
157+
```typescript
158+
// Server Component
159+
import { getMessages, getTranslations, type Locale } from '@/i18n'
160+
161+
export async function ServerComponent({ locale }: { locale: Locale }) {
162+
/* Direct object access */
163+
const messages = await getMessages(locale)
164+
/* Function call */
165+
const t = await getTranslations(locale)
166+
167+
return (
168+
<div>
169+
<h1>{messages.common.title}</h1>
170+
{/* ^^^^^ Auto-complete */}
171+
<p>{t('common.description')}</p>
172+
{/* ^^^^^^^^^^^^^^^^^^ Auto-complete */}
173+
</div>
174+
)
175+
}
176+
```
177+
178+
```typescript
179+
// Client Component
180+
'use client'
181+
import { Link, useMessages, useTranslations } from '@/i18n'
182+
183+
export function ClientComponent() {
184+
/* Direct object access */
185+
const messages = useMessages()
186+
/* Function call */
187+
const t = useTranslations()
188+
189+
return (
190+
<div>
191+
<h1>{messages.common.title}</h1>
192+
{/* ^^^^^ Auto-complete */}
193+
<Link href="/about">{t('nav.about')}</Link>
194+
{/* ^^^^^^^^^ Auto-complete */}
195+
</div>
196+
)
197+
}
198+
```
199+
200+
That's it! **Types are automatically inferred** - no manual type annotations needed.
201+
202+
**Two ways to access translations:**
203+
204+
- `messages.common.title` - Direct object access (simple and clear)
205+
- `t('common.title')` - Function call (useful for dynamic keys)
206+
207+
Both are fully typed with autocomplete. Use whichever you prefer!
208+
209+
## API Reference
210+
211+
### `define(config)`
212+
213+
Defines an i18n instance with automatic type inference.
214+
215+
**Parameters:**
216+
217+
- `config.locales` - Array of supported locales
218+
- `config.defaultLocale` - Default locale
219+
- `config.messages` - Messages object keyed by locale
220+
221+
**Returns:**
222+
223+
```typescript
224+
{
225+
Provider, // Context provider component
226+
Link, // Next.js Link with locale handling
227+
server: {
228+
getMessages, // Get messages object
229+
getTranslations // Get translation function
230+
},
231+
client: {
232+
useMessages, // Get messages object (hook)
233+
useTranslations, // Get translation function (hook)
234+
useLocale // Get current locale (hook)
235+
}
236+
}
237+
```
238+
239+
## Technical Notes
240+
241+
### Message Serialization
242+
243+
This library uses `JSON.parse(JSON.stringify())` to convert ES module namespace objects to plain objects, ensuring React Server Components compatibility.
244+
245+
### Link Component
246+
247+
The `Link` component automatically handles both string paths and Next.js `UrlObject`:
248+
249+
```typescript
250+
<Link href="/about">About</Link>
251+
<Link href={{ pathname: '/search', query: { q: 'test' } }}>Search</Link>
252+
```
253+
254+
Both will have the locale prefix automatically added based on the current locale.
255+
256+
## License
257+
258+
MIT

0 commit comments

Comments
 (0)