├── .eslintrc.js
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── __tests__
└── basic.js
├── babel.config.js
├── example
├── .babelrc
├── .gitignore
├── README.md
├── components
│ ├── namespace.js
│ └── title.js
├── locales
│ ├── de.json
│ ├── en.json
│ └── namespace.json
├── next.config.js
├── package.json
├── pages
│ ├── 404.js
│ ├── [slug]
│ │ └── index.js
│ ├── _app.js
│ ├── dashboard.js
│ ├── index.js
│ └── namespace.js
└── yarn.lock
├── jest.config.js
├── package.json
├── setupTests.js
├── src
├── hooks
│ └── use-i18n.js
├── i18n.js
└── index.js
├── types
├── index.d.ts
├── index.test-d.ts
└── index.test-d.tsx
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parserOptions: {
4 | ecmaVersion: 2020,
5 | sourceType: 'module',
6 | ecmaFeatures: {
7 | jsx: true
8 | }
9 | },
10 | settings: {
11 | react: {
12 | version: 'detect'
13 | }
14 | },
15 | env: {
16 | browser: true,
17 | amd: true,
18 | node: true,
19 | jest: true
20 | },
21 | plugins: ['simple-import-sort'],
22 | extends: [
23 | 'eslint:recommended',
24 | 'plugin:react/recommended',
25 | 'plugin:jsx-a11y/recommended',
26 | 'plugin:prettier/recommended',
27 | 'plugin:react-hooks/recommended'
28 | ],
29 | rules: {
30 | 'prettier/prettier': ['error', {}, { usePrettierrc: true }],
31 | 'react/react-in-jsx-scope': 'off',
32 | 'react/prop-types': 'off',
33 | '@typescript-eslint/explicit-function-return-type': 'off',
34 | 'simple-import-sort/imports': 'error',
35 | 'simple-import-sort/exports': 'error',
36 | 'jsx-a11y/anchor-is-valid': [
37 | 'error',
38 | {
39 | components: ['Link'],
40 | specialLink: ['hrefLeft', 'hrefRight'],
41 | aspects: ['invalidHref', 'preferButton']
42 | }
43 | ]
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 | on:
3 | push:
4 | paths-ignore:
5 | - 'docs/**'
6 | - 'example/**'
7 | - '*.md'
8 | pull_request:
9 | paths-ignore:
10 | - 'docs/**'
11 | - 'example/**'
12 | - '*.md'
13 |
14 | env:
15 | CI: true
16 |
17 | jobs:
18 | build:
19 | runs-on: ${{ matrix.os }}
20 | strategy:
21 | matrix:
22 | node-version: [12.x, 14.x]
23 | os: [ubuntu-latest, windows-latest]
24 | name: Node ${{ matrix.node-version }}
25 |
26 | steps:
27 | - uses: actions/checkout@v2
28 |
29 | - name: Setup node
30 | uses: actions/setup-node@v1
31 | with:
32 | node-version: ${{ matrix.node-version }}
33 |
34 | - name: Install Dependencies
35 | run: yarn
36 |
37 | - name: Tests
38 | run: yarn test
39 |
40 | - name: Build
41 | run: yarn build
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .next
4 | .vscode
5 | coverage
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .vscode
4 | .next
5 | coverage
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "tabWidth": 4,
4 | "printWidth": 100,
5 | "singleQuote": true,
6 | "trailingComma": "none",
7 | "jsxBracketSameLine": true
8 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Dustin Deus
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | next-localization
3 |
4 |
5 |
6 | The minimalistic localization solution for Next.js , powered by Rosetta and Next.js 10 Internationalized Routing .
7 |
8 | ---
9 |
10 | > **Note**: Are you looking for the next generation API Developer Platform? 🔎 Have a look at: [WunderGraph](https://github.com/wundergraph/wundergraph)
11 | Turn your services, databases and 3rd party APIs into a secure unified API in just a few minutes. 🪄
12 |
13 | ---
14 |
15 | ## ✨ Features
16 |
17 | - Supports all rendering modes: (Static) | ● (SSG) | λ (Server).
18 | - Ideal companion to [Next.js 10 Internationalized Routing](https://nextjs.org/blog/next-10#internationalized-routing)
19 | - Less than 1000 bytes – including dependencies!
20 | - Pluralization support
21 | - No build step, No enforced conventions.
22 |
23 | ## Table of Contents
24 |
25 | - [Installation & Setup](#installation--setup)
26 | - [Basic Usage](#basic-usage)
27 | - [Usage with `getStaticProps`](#usage-with-getstaticprops)
28 | - [Redirect to default language](#redirect-to-default-language)
29 | - [Construct correct links](#construct-correct-links)
30 | - [Internationalization](#internationalization)
31 | - [Pluralization](#pluralization)
32 | - [Datetime, Numbers](#datetime-numbers)
33 | - [Access i18n outside React](#access-i18n-outside-react)
34 | - [Performance considerations](#performance-considerations)
35 | - [Other considerations](#other-considerations)
36 |
37 | ## Installation & Setup
38 |
39 | ```
40 | yarn add next-localization
41 | ```
42 |
43 | ## Example
44 |
45 | See [`example`](./example) for full example and locale setup.
46 |
47 | ## Basic Usage
48 |
49 | Your `_app.js`.
50 |
51 | ```js
52 | import { I18nProvider } from 'next-localization';
53 | import { useRouter } from 'next/router';
54 |
55 | export default function MyApp({ Component, pageProps }) {
56 | const router = useRouter();
57 | const { lngDict, ...rest } = pageProps;
58 |
59 | return (
60 |
61 |
62 |
63 | );
64 | }
65 | ```
66 |
67 | Any functional component.
68 |
69 | ```js
70 | import { useI18n } from 'next-localization';
71 | import { useRouter } from 'next/router';
72 | import Link from 'next/link';
73 |
74 | const HomePage = () => {
75 | const router = useRouter();
76 | const i18n = useI18n();
77 | // or
78 | const i18nPlural = i18n.withPlural();
79 | return (
80 | <>
81 |
82 | {i18n.t('title')}}, {i18n.t('welcome', { username })}
83 |
84 | {i18nPlural('products_count', { items: 2 })}
85 |
86 | Change language to (en)
87 |
88 | >
89 | );
90 | };
91 | ```
92 |
93 | ## Usage with [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation)
94 |
95 | Checkout the [full example](example).
96 |
97 | _The same steps works with [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering)._
98 |
99 | ## Redirect to default language
100 |
101 | Built-in with [Next.js 10 Internationalized Routing](https://nextjs.org/docs/advanced-features/i18n-routing#automatic-locale-detection)
102 |
103 | ## Construct correct links
104 |
105 | Built-in with [Next.js 10 Internationalized Routing](https://nextjs.org/docs/advanced-features/i18n-routing#transition-between-locales)
106 |
107 | ## Internationalization
108 |
109 | We rely on the native platform api [`Intl`](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_negotiation). If you need to support older browsers (e.g IE11) use polyfills.
110 |
111 | ### Pluralization
112 |
113 | We provide a small pluralization `i18n.withPlural` utility function. It returns the same `ì18n` interface but handles number values as pluralization. The implementation uses [`Intl.PluralRules`](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules).
114 |
115 | ```js
116 | import { useRouter } from 'next/router';
117 | import { I18nProvider, useI18n } from 'next-localization';
118 |
119 | function Root() {
120 | const router = useRouter();
121 | return (
122 |
133 |
134 |
135 | );
136 | }
137 |
138 | function Child() {
139 | const i18n = useI18n();
140 | const router = useRouter();
141 | const t = i18n.withPlural();
142 | return {t('warning', { birds: 2 })}
; // WARNING: two birds
143 | }
144 | ```
145 |
146 | ### Datetime, Numbers
147 |
148 | Use [`DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat), [`NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) directly or rely on an external library. The integration will look very similiar.
149 |
150 | ```js
151 | import { useRouter } from 'next/router';
152 | import { I18nProvider } from 'next-localization';
153 |
154 | function Root() {
155 | return (
156 |
161 |
162 |
163 | );
164 | }
165 |
166 | function Child() {
167 | const router = useRouter();
168 | const date = new Intl.DateTimeFormat(router.locale).format(new Date());
169 | return {t('copyright', { date })}
; // Copyright: 8/30/2020
170 | }
171 | ```
172 |
173 | ## Access i18n outside React
174 |
175 | If you need access to the `i18n` outside of react or react hooks, you can create a custom `i18n` instance and pass it to the `I18nProvider`.
176 | It's the same interface as `useI18n` returns.
177 |
178 | ```js
179 | import { I18nProvider } from 'next-localization';
180 | import { useRouter } from 'next/router';
181 |
182 | const i18n = I18n({
183 | en: { hello: 'Hello, world!' }
184 | });
185 |
186 | export default function MyApp({ Component, pageProps }) {
187 | const router = useRouter();
188 | const { lngDict, ...rest } = pageProps;
189 |
190 | return (
191 |
192 |
193 |
194 | );
195 | }
196 | ```
197 |
198 | ## Performance considerations
199 |
200 | Don't forget that a locale change will rerender all components under the `I18nProvider` provider.
201 | It's safe to create multiple providers with different language dictionaries. This can be useful if you want to split it into different namespaces.
202 |
203 | [Here](example/pages/namespace.js) you can see an example how to lazy-load a component with a different locale file. Code splitting is ensured by embedding the JSON file via the babel macro [json.macro](https://github.com/ifiokjr/json.macro).
204 |
205 | ## Other considerations
206 |
207 | Depending on your application `next-localization` might not be sufficient to internationalize your application. You still need to consider:
208 |
209 | - Format [times](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) and [numbers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat).
210 |
211 | With some effort those points are very easy to solve and you can still base on a very lightweight localization strategy.
212 |
--------------------------------------------------------------------------------
/__tests__/basic.js:
--------------------------------------------------------------------------------
1 | import { render, waitFor } from '@testing-library/react';
2 | import React, { useEffect } from 'react';
3 |
4 | import { I18n, I18nProvider, useI18n } from './../src/index';
5 |
6 | test('Should render key', () => {
7 | function Root() {
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 | function Child() {
15 | const i18n = useI18n();
16 | return {i18n.t('hello')}
;
17 | }
18 |
19 | const { getByText } = render( );
20 | expect(getByText('Hello, world!')).toBeInTheDocument();
21 | });
22 |
23 | test('Should interpolate key', () => {
24 | function Root() {
25 | return (
26 |
27 |
28 |
29 | );
30 | }
31 | function Child() {
32 | const i18n = useI18n();
33 | return {i18n.t('welcome', { username: 'Bernd' })}
;
34 | }
35 |
36 | const { getByText } = render( );
37 | expect(getByText('Welcome, Bernd!')).toBeInTheDocument();
38 | });
39 |
40 | test('Should print current locale', () => {
41 | function Root() {
42 | return (
43 |
44 |
45 |
46 | );
47 | }
48 | function Child() {
49 | const i18n = useI18n();
50 | return {i18n.locale()}
;
51 | }
52 |
53 | const { getByText } = render( );
54 | expect(getByText('en')).toBeInTheDocument();
55 | });
56 |
57 | test('Should pluralize', () => {
58 | function Root() {
59 | return (
60 |
71 |
72 |
73 | );
74 | }
75 | function Child() {
76 | const i18n = useI18n();
77 | const t = i18n.withPlural();
78 | return {t('warning', { birds: 2, foo: 'bar' })}
;
79 | }
80 |
81 | const { getByText } = render( );
82 | expect(getByText('WARNING: two birds, bar')).toBeInTheDocument();
83 | });
84 |
85 | test('Should fallback to default behaviour when no number is passed', () => {
86 | function Root() {
87 | return (
88 |
99 |
100 |
101 | );
102 | }
103 | function Child() {
104 | const i18n = useI18n();
105 | const t = i18n.withPlural();
106 | return {t('warning', { birds: 'no-number' })}
;
107 | }
108 |
109 | const { getByText } = render( );
110 | expect(getByText('WARNING: no-number')).toBeInTheDocument();
111 | });
112 |
113 | test('Should be able to pass a custom i18n instance', () => {
114 | const i18nInstance = I18n({
115 | en: { hello: 'Hello, world!' }
116 | });
117 | function Root() {
118 | return (
119 |
120 |
121 |
122 | );
123 | }
124 | function Child() {
125 | const i18n = useI18n();
126 | return {i18n.t('hello')}
;
127 | }
128 |
129 | const { getByText } = render( );
130 | expect(getByText('Hello, world!')).toBeInTheDocument();
131 | });
132 |
133 | test('Should be able to pass a custom i18n instance and langDict', () => {
134 | const i18nInstance = I18n();
135 | function Root() {
136 | return (
137 |
141 |
142 |
143 | );
144 | }
145 | function Child() {
146 | const i18n = useI18n();
147 | return {i18n.t('hello')}
;
148 | }
149 |
150 | const { getByText } = render( );
151 | expect(i18nInstance.table('en')).toEqual({ hello: 'Hello, world!' });
152 | expect(getByText('Hello, world!')).toBeInTheDocument();
153 | });
154 |
155 | test('Should be able to change locale', async () => {
156 | const i18nInstance = I18n({
157 | en: { hello: 'Hello, world!' }
158 | });
159 | function Root() {
160 | return (
161 |
162 |
163 |
164 | );
165 | }
166 | function Child() {
167 | const i18n = useI18n();
168 |
169 | useEffect(() => {
170 | i18n.set('de', { hello: 'Hello, Welt!' });
171 | i18n.locale('de');
172 | // eslint-disable-next-line react-hooks/exhaustive-deps
173 | }, []);
174 |
175 | return {i18n.t('hello')}
;
176 | }
177 |
178 | const { getByText } = render( );
179 |
180 | await waitFor(() => {
181 | expect(i18nInstance.locale()).toEqual('de');
182 | expect(i18nInstance.table('en')).toEqual({ hello: 'Hello, world!' });
183 | expect(i18nInstance.table('de')).toEqual({ hello: 'Hello, Welt!' });
184 | expect(getByText('Hello, Welt!')).toBeInTheDocument();
185 | });
186 | });
187 |
188 | test('Should be able to set new keys without changing locale', () => {
189 | const i18nInstance = I18n({
190 | en: { hello: 'Hello, world!' }
191 | });
192 | function Root() {
193 | return (
194 |
195 |
196 |
197 | );
198 | }
199 | function Child() {
200 | const i18n = useI18n();
201 |
202 | useEffect(() => {
203 | i18n.set('de', { hello: 'Hello, Welt!' });
204 | // eslint-disable-next-line react-hooks/exhaustive-deps
205 | }, []);
206 |
207 | return {i18n.t('hello')}
;
208 | }
209 |
210 | const { getByText } = render( );
211 | expect(i18nInstance.table('de')).toEqual({ hello: 'Hello, Welt!' });
212 | expect(getByText('Hello, world!')).toBeInTheDocument();
213 | });
214 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | test: {
4 | presets: [
5 | [
6 | '@babel/preset-env',
7 | {
8 | targets: {
9 | node: 'current'
10 | }
11 | }
12 | ],
13 | '@babel/preset-react'
14 | ]
15 | }
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/example/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": ["macros"]
4 | }
5 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # next-localization example
2 |
3 | This example uses [next-localization](https://github.com/StarpTech/next-localization) to provide a SSR, SSG, CSR compatible i18n solution.
4 |
5 | In `next.config.js` you can configure the locales and routing strategy.
6 |
7 | ## Development
8 |
9 | 1. Run `yarn link` in the root directory.
10 | 2. Run `yarn build` in the root directory.
11 | 3. Run `yarn` here to update the `next-localization` package with the latest updates.
12 | 4. Run `yarn dev` here to run the application.
13 | 5. Repeat step 2) if you want to update `next-localization`.
14 |
--------------------------------------------------------------------------------
/example/components/namespace.js:
--------------------------------------------------------------------------------
1 | import { loadJson } from 'json.macro';
2 | import { I18nProvider, useI18n } from 'next-localization';
3 | import { useRouter } from 'next/router';
4 | import { useMemo } from 'react';
5 |
6 | const translations = loadJson('../locales/namespace.json');
7 |
8 | export default function Namespace() {
9 | const router = useRouter();
10 | const map = useMemo(() => {
11 | return translations[router.locale];
12 | }, [router.locale]);
13 |
14 | return (
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | export function Title() {
22 | const i18n = useI18n();
23 | return {i18n.t('title')} ;
24 | }
25 |
--------------------------------------------------------------------------------
/example/components/title.js:
--------------------------------------------------------------------------------
1 | import { useI18n } from 'next-localization';
2 |
3 | export default function Title({ username }) {
4 | const i18n = useI18n();
5 | return {i18n.t('intro.welcome', { username })} ;
6 | }
7 |
--------------------------------------------------------------------------------
/example/locales/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "intro": {
3 | "welcome": "Willkommen, {{username}}!",
4 | "text": "Ich hoffe, du findest das nützlich.",
5 | "description": "Das Beispiel zeigt, wie man die Sprache für SSG und SSG optimierte Seiten wechselt."
6 | },
7 | "dashboard": {
8 | "description": "Das Beispiel zeigt, wie man die Sprache nur Frontendseitig verändert. Nützlich für Dashboards wo SEO nicht relevant ist."
9 | },
10 | "warning": "WARNING: {{birds}}",
11 | "birds": {
12 | "other": "Vögel",
13 | "one": "Vogel",
14 | "two": "zwei Vögel",
15 | "few": "einige Vögel"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/example/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "intro": {
3 | "welcome": "Welcome, {{username}}!",
4 | "text": "I hope you find this useful.",
5 | "description": "This example demonstrate how to change the language for SSG and SSR optimized pages."
6 | },
7 | "dashboard": {
8 | "description": "This example demonstrate how to lazy-load the language on the client side."
9 | },
10 | "warning": "WARNING: {{birds}}",
11 | "birds": {
12 | "other": "birds",
13 | "one": "bird",
14 | "two": "two birds",
15 | "few": "some birds"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/example/locales/namespace.json:
--------------------------------------------------------------------------------
1 | {
2 | "de": {
3 | "title": "Dynamic Hallo"
4 | },
5 | "en": {
6 | "title": "Dynamic Hello"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/example/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | i18n: {
3 | // These are all the locales you want to support in
4 | // your application
5 | locales: ['en', 'de'],
6 | // This is the default locale you want to be used when visiting
7 | // a non-locale prefixed path e.g. `/hello`
8 | defaultLocale: 'en'
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "with-rosetta",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "dev": "next",
6 | "build": "next build",
7 | "start": "next start",
8 | "postinstall": "yarn link 'next-localization' && rm -rf node_modules/react"
9 | },
10 | "dependencies": {
11 | "next": "latest",
12 | "next-localization": "latest",
13 | "react": "^17.0.1",
14 | "react-dom": "^17.0.1"
15 | },
16 | "license": "ISC",
17 | "devDependencies": {
18 | "babel-plugin-macros": "^2.8.0",
19 | "json.macro": "^1.3.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/example/pages/404.js:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 |
3 | const NotFoundPage = () => {
4 | const router = useRouter();
5 | return (
6 |
7 |
Not found
8 |
Current locale: {router.locale}
9 |
10 | );
11 | };
12 |
13 | export default NotFoundPage;
14 |
--------------------------------------------------------------------------------
/example/pages/[slug]/index.js:
--------------------------------------------------------------------------------
1 | import { useI18n } from 'next-localization';
2 | import Link from 'next/link';
3 | import { useRouter } from 'next/router';
4 |
5 | import Title from '../../components/title';
6 |
7 | const PostPage = () => {
8 | const router = useRouter();
9 | const i18n = useI18n();
10 | const tp = i18n.withPlural();
11 |
12 | return (
13 |
14 |
15 |
{i18n.t('intro.text')}
16 |
{i18n.t('intro.description')}
17 |
Current locale: {router.locale}
18 |
Pluralization: {tp('warning', { birds: 2 })}
19 |
24 |
29 |
30 | );
31 | };
32 |
33 | export async function getStaticProps({ locale }) {
34 | const { default: lngDict = {} } = await import(`../../locales/${locale}.json`);
35 |
36 | return {
37 | props: { lngDict }
38 | };
39 | }
40 |
41 | export async function getStaticPaths({ locales }) {
42 | return {
43 | paths: locales.map((l) => ({ params: { slug: `foo` }, locale: l })),
44 | fallback: false
45 | };
46 | }
47 |
48 | export default PostPage;
49 |
--------------------------------------------------------------------------------
/example/pages/_app.js:
--------------------------------------------------------------------------------
1 | import { I18nProvider } from 'next-localization';
2 | import { useRouter } from 'next/router';
3 |
4 | export default function MyApp({ Component, pageProps }) {
5 | const router = useRouter();
6 | const { lngDict, ...rest } = pageProps;
7 |
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/example/pages/dashboard.js:
--------------------------------------------------------------------------------
1 | import { useI18n } from 'next-localization';
2 | import Link from 'next/link';
3 | import { useRouter } from 'next/router';
4 | import { useEffect } from 'react';
5 |
6 | import Title from '../components/title';
7 |
8 | const Dashboard = () => {
9 | const router = useRouter();
10 | const i18n = useI18n();
11 |
12 | useEffect(() => {
13 | async function changeLocale() {
14 | if (router.locale === 'en') {
15 | i18n.set('en', await import('../locales/en.json'));
16 | i18n.locale('en');
17 | } else if (router.locale === 'de') {
18 | i18n.set('de', await import('../locales/de.json'));
19 | i18n.locale('de');
20 | }
21 | }
22 | changeLocale();
23 | // eslint-disable-next-line react-hooks/exhaustive-deps
24 | }, [router.locale]);
25 |
26 | return (
27 |
28 |
29 |
{i18n.t('intro.text')}
30 |
{i18n.t('dashboard.description')}
31 |
Current locale: {router.locale}
32 |
37 |
42 |
43 | );
44 | };
45 |
46 | export default Dashboard;
47 |
--------------------------------------------------------------------------------
/example/pages/index.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | const HomePage = () => {
4 | return (
5 |
6 |
next-localization
7 |
12 |
17 |
22 |
23 | );
24 | };
25 |
26 | export default HomePage;
27 |
--------------------------------------------------------------------------------
/example/pages/namespace.js:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic';
2 | import Link from 'next/link';
3 | import { useRouter } from 'next/router';
4 |
5 | const DynamicComponent = dynamic(() => import('../components/namespace'));
6 |
7 | const Dashboard = () => {
8 | const router = useRouter();
9 |
10 | return (
11 |
12 |
13 |
Current locale: {router.locale}
14 |
19 |
24 |
25 | );
26 | };
27 |
28 | export default Dashboard;
29 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | roots: [path.resolve(__dirname)],
5 | testEnvironment: 'jest-environment-jsdom-sixteen',
6 | displayName: 'local',
7 | testURL: 'http://localhost',
8 | setupFilesAfterEnv: [path.resolve(__dirname, './setupTests.js')]
9 | };
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-localization",
3 | "version": "0.12.0",
4 | "main": "dist/index.js",
5 | "source": "src/index.js",
6 | "module": "dist/index.modern.js",
7 | "unpkg": "dist/index.umd.js",
8 | "repository": "git@github.com:StarpTech/next-localization.git",
9 | "types": "types/index.d.ts",
10 | "author": {
11 | "name": "Dustin Deus",
12 | "email": "deusdustin@gmail.com",
13 | "url": "https://starptech.de"
14 | },
15 | "license": "MIT",
16 | "files": [
17 | "types/*.d.ts",
18 | "dist"
19 | ],
20 | "scripts": {
21 | "build": "microbundle --no-sourcemap --no-compress --jsx React.createElement",
22 | "tsd": "tsd",
23 | "dev": "microbundle watch",
24 | "test": "yarn lint && jest --collectCoverage",
25 | "lint": "eslint --fix ./src/**/* && yarn tsd",
26 | "format": "prettier --write \"./**/*.{js,jsx,ts,tsx,css,md,json}\" --config ./.prettierrc",
27 | "release": "yarn build && yarn lint && release-it"
28 | },
29 | "husky": {
30 | "hooks": {
31 | "pre-commit": "pretty-quick --staged && yarn test"
32 | }
33 | },
34 | "browserslist": {
35 | "development": [
36 | "last 2 chrome versions",
37 | "last 2 firefox versions",
38 | "last 2 edge versions"
39 | ],
40 | "production": [
41 | ">1%",
42 | "last 4 versions",
43 | "Firefox ESR",
44 | "not ie < 11"
45 | ]
46 | },
47 | "dependencies": {
48 | "rosetta": "1.1.0"
49 | },
50 | "peerDependencies": {
51 | "react": ">=17.0.1"
52 | },
53 | "devDependencies": {
54 | "eslint": "^7.16.0",
55 | "@babel/preset-env": "^7.12.11",
56 | "@babel/preset-react": "^7.12.10",
57 | "@testing-library/jest-dom": "^5.11.8",
58 | "@testing-library/react": "^13.0.1",
59 | "@types/react": "^18.0.4",
60 | "babel-jest": "^26.6.3",
61 | "eslint-config-prettier": "^7.1.0",
62 | "eslint-plugin-jsx-a11y": "^6.4.1",
63 | "eslint-plugin-prettier": "^3.3.0",
64 | "eslint-plugin-react": "^7.22.0",
65 | "eslint-plugin-react-hooks": "^4.2.0",
66 | "eslint-plugin-simple-import-sort": "^7.0.0",
67 | "husky": "^4.3.6",
68 | "jest": "^26.6.3",
69 | "jest-environment-jsdom-sixteen": "^1.0.3",
70 | "microbundle": "^0.12.4",
71 | "prettier": "^2.2.1",
72 | "pretty-quick": "^3.1.0",
73 | "react": "^18.0.0",
74 | "react-dom": "^18.0.0",
75 | "react-test-renderer": "^18.0.0",
76 | "release-it": "^14.2.2",
77 | "tsd": "^0.14.0"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/setupTests.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
3 | const editableFn = (_value) => ({
4 | get: () => _value,
5 | set: (v) => (_value = v)
6 | });
7 |
8 | Object.defineProperty(navigator, 'languages', editableFn(['en-US', 'en']));
9 |
--------------------------------------------------------------------------------
/src/hooks/use-i18n.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import { I18nContext } from '../i18n';
4 |
5 | export default function useI18n() {
6 | const i18n = useContext(I18nContext);
7 | return i18n;
8 | }
9 |
--------------------------------------------------------------------------------
/src/i18n.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo } from 'react';
2 | import { createContext, useState } from 'react';
3 | import rosetta from 'rosetta';
4 | // import rosetta from 'rosetta/debug';
5 |
6 | export const I18nContext = createContext();
7 |
8 | export const I18n = function (rosettaOpts) {
9 | const r = rosetta(rosettaOpts);
10 | return {
11 | /**
12 | * Triggers a render cycle
13 | */
14 | _onUpdate() {},
15 | /**
16 | * Retrieve a translation segment for the active language
17 | */
18 | t: r.t,
19 | /**
20 | * Get the table of translations for a language
21 | */
22 | table: r.table,
23 | /**
24 | * Define or extend the language table
25 | */
26 | set: r.set,
27 | /**
28 | * Set a locale or returns the active locale
29 | */
30 | locale(locale) {
31 | if (locale === undefined) {
32 | return r.locale();
33 | }
34 | const activelocale = r.locale(locale);
35 | this._onUpdate();
36 | return activelocale;
37 | },
38 | /**
39 | * Returns an i18n instance that treats number values as pluralization
40 | */
41 | withPlural(pluralRulesOptions = { type: 'ordinal' }) {
42 | const PR = new Intl.PluralRules(r.locale(), pluralRulesOptions);
43 | return (key, params, ...args) => {
44 | Object.keys(params).map((k) => {
45 | if (typeof params[k] === 'number') {
46 | let pkey = PR.select(params[k]);
47 | params[k] = this.t(`${k}.${pkey}`);
48 | }
49 | });
50 | return this.t(key, params, ...args);
51 | };
52 | }
53 | };
54 | };
55 |
56 | export default function I18nProvider({ children, locale = 'en', lngDict, i18nInstance }) {
57 | const [, setTick] = useState(0);
58 | const i18n = useMemo(() => {
59 | const instance = i18nInstance ?? I18n();
60 | instance._onUpdate = () => setTick((tick) => tick + 1);
61 | instance.set(locale, lngDict);
62 | instance.locale(locale);
63 | return instance;
64 | // eslint-disable-next-line react-hooks/exhaustive-deps
65 | }, [i18nInstance]);
66 |
67 | useEffect(() => {
68 | i18n.set(locale, lngDict);
69 | i18n.locale(locale);
70 | // eslint-disable-next-line react-hooks/exhaustive-deps
71 | }, [locale, lngDict]);
72 |
73 | return {children} ;
74 | }
75 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { I18n, default as I18nProvider } from './i18n';
2 | export { default as useI18n } from './hooks/use-i18n';
3 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | interface I18n {
2 | /** Get/Set the language key */
3 | locale(lang?: string): string;
4 | /** Define or extend the language table */
5 | set(lang: string, table: T): void;
6 | /** Get the table of translations for a language */
7 | table(lang: string): T | void;
8 | /** Retrieve a translation segment for the current language */
9 | t | any[]>(
10 | key: string | (string | number)[],
11 | params?: X,
12 | lang?: string
13 | ): string;
14 | withPlural(
15 | pluralOptions?: object
16 | ): | any[]>(
17 | key: string | (string | number)[],
18 | params?: X,
19 | lang?: string
20 | ) => string;
21 | }
22 |
23 | export function useI18n(): I18n;
24 | export function I18n(table?: T): I18n;
25 | export function I18nProvider(props: ProviderProps): React.ReactElement;
26 |
27 | interface ProviderProps {
28 | lngDict?: T;
29 | locale: string;
30 | i18nInstance?: I18n;
31 | children: JSX.Element;
32 | }
33 | interface II18nProvider extends React.FC> {}
34 |
--------------------------------------------------------------------------------
/types/index.test-d.ts:
--------------------------------------------------------------------------------
1 | import { useI18n, I18n } from '.';
2 |
3 | let i18n = I18n();
4 | i18n.locale('de');
5 |
6 | i18n = I18n({ en: { foo: 'bar' } });
7 | i18n.locale('de');
8 |
9 | i18n = useI18n>();
10 |
11 | i18n.locale('en');
12 | i18n.locale();
13 |
14 | i18n.set('en', { foo: 'bar' });
15 |
16 | i18n.t('a.b', { foo: 3 });
17 |
18 | i18n.table('de');
19 |
20 | const tp = i18n.withPlural();
21 |
22 | tp('a.b', { foo: 3 });
23 |
--------------------------------------------------------------------------------
/types/index.test-d.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { I18nProvider } from '.';
3 |
4 | function Component() {
5 | return null;
6 | }
7 |
8 | export default function App() {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------