├── .all-contributorsrc
├── .babelrc
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .husky
├── .gitignore
└── pre-commit
├── .npmrc
├── .prettierignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── __tests__
├── Trans.test.js
├── TransText.test.js
├── formatElements.test.js
├── getPageNamespaces.test.js
├── getT.test.js
├── loadNamespaces.test.js
├── transCore.test.js
├── useTranslation.test.js
└── withTranslation.test.js
├── build-packages.js
├── docs
├── hoist-non-react-statics.md
├── migration-guide-1.0.0.md
├── migration-guide-2.0.0.md
└── type-safety.md
├── examples
├── basic
│ ├── README.md
│ ├── components
│ │ ├── header.js
│ │ ├── header.module.css
│ │ ├── no-functional-component.js
│ │ └── plural-example.js
│ ├── i18n.json
│ ├── locales
│ │ ├── ca
│ │ │ ├── common.json
│ │ │ ├── dynamic.json
│ │ │ ├── error.json
│ │ │ ├── home.json
│ │ │ └── more-examples.json
│ │ ├── en
│ │ │ ├── common.json
│ │ │ ├── dynamic.json
│ │ │ ├── error.json
│ │ │ ├── home.json
│ │ │ └── more-examples.json
│ │ └── es
│ │ │ ├── common.json
│ │ │ ├── dynamic.json
│ │ │ ├── error.json
│ │ │ ├── home.json
│ │ │ └── more-examples.json
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── pages
│ │ ├── 404.js
│ │ ├── index.js
│ │ └── more-examples
│ │ │ ├── catchall
│ │ │ └── [...all].js
│ │ │ ├── dynamic-namespace.js
│ │ │ ├── dynamicroute
│ │ │ └── [slug].js
│ │ │ └── index.js
│ ├── public
│ │ └── favicon.ico
│ └── tsconfig.json
├── complex
│ ├── README.md
│ ├── i18n.js
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── public
│ │ └── favicon.ico
│ ├── src
│ │ ├── _middleware.ts
│ │ ├── components
│ │ │ ├── header.module.css
│ │ │ ├── header.tsx
│ │ │ ├── mdx-example.mdx
│ │ │ ├── no-functional-component.tsx
│ │ │ └── plural-example.tsx
│ │ ├── mdx.d.ts
│ │ ├── pages
│ │ │ ├── 404.tsx
│ │ │ ├── _app.tsx
│ │ │ ├── amp.tsx
│ │ │ ├── api
│ │ │ │ └── example.tsx
│ │ │ ├── index.tsx
│ │ │ └── more-examples
│ │ │ │ ├── catchall
│ │ │ │ └── [...all].tsx
│ │ │ │ ├── dynamic-namespace.tsx
│ │ │ │ ├── dynamicroute
│ │ │ │ └── [slug].tsx
│ │ │ │ └── index.tsx
│ │ ├── styles.css
│ │ └── translations
│ │ │ ├── common_ca.json
│ │ │ ├── common_en.json
│ │ │ ├── common_es.json
│ │ │ ├── dynamic_ca.json
│ │ │ ├── dynamic_en.json
│ │ │ ├── dynamic_es.json
│ │ │ ├── error_ca.json
│ │ │ ├── error_en.json
│ │ │ ├── error_es.json
│ │ │ ├── home_ca.json
│ │ │ ├── home_en.json
│ │ │ ├── home_es.json
│ │ │ ├── more-examples_ca.json
│ │ │ ├── more-examples_en.json
│ │ │ └── more-examples_es.json
│ └── tsconfig.json
├── with-app-directory
│ ├── README.md
│ ├── i18n.js
│ ├── locales
│ │ ├── ca
│ │ │ ├── common.json
│ │ │ └── home.json
│ │ ├── en
│ │ │ ├── common.json
│ │ │ └── home.json
│ │ └── es
│ │ │ ├── common.json
│ │ │ └── home.json
│ ├── next-env.d.ts
│ ├── next-translate.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── public
│ │ └── favicon.ico
│ ├── src
│ │ ├── app
│ │ │ ├── [lang]
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── loading.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── second-page
│ │ │ │ │ └── page.tsx
│ │ │ └── layout.tsx
│ │ └── components
│ │ │ └── client-code.tsx
│ └── tsconfig.json
└── without-loader
│ ├── README.md
│ ├── components
│ ├── header.js
│ ├── header.module.css
│ ├── no-functional-component.js
│ └── plural-example.js
│ ├── i18n.js
│ ├── locales
│ ├── ca
│ │ ├── common.json
│ │ ├── dynamic.json
│ │ ├── error.json
│ │ ├── home.json
│ │ └── more-examples.json
│ ├── en
│ │ ├── common.json
│ │ ├── dynamic.json
│ │ ├── error.json
│ │ ├── home.json
│ │ └── more-examples.json
│ └── es
│ │ ├── common.json
│ │ ├── dynamic.json
│ │ ├── error.json
│ │ ├── home.json
│ │ └── more-examples.json
│ ├── next.config.js
│ ├── package.json
│ ├── pages
│ ├── 404.js
│ ├── _app.js
│ ├── index.js
│ └── more-examples
│ │ ├── catchall
│ │ └── [...all].js
│ │ ├── dynamic-namespace.js
│ │ ├── dynamicroute
│ │ └── [slug].js
│ │ └── index.js
│ └── public
│ └── favicon.ico
├── images
├── bundle-size.png
├── logo.svg
└── translation-prerendered.gif
├── jest.setup.js
├── package.json
├── src
├── AppDirI18nProvider.tsx
├── DynamicNamespaces.tsx
├── I18nProvider.tsx
├── Trans.tsx
├── TransText.ts
├── appWithI18n.tsx
├── context.tsx
├── createTranslation.tsx
├── formatElements.tsx
├── getConfig.tsx
├── getPageNamespaces.tsx
├── getT.tsx
├── index.tsx
├── loadNamespaces.tsx
├── setLanguage.tsx
├── transCore.tsx
├── useTranslation.tsx
├── withTranslation.tsx
└── wrapTWithDefaultNs.tsx
├── tsconfig-cjs.json
├── tsconfig.json
└── yarn.lock
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "contributors": [
8 | {
9 | "login": "aralroca",
10 | "name": "Aral Roca Gomez",
11 | "avatar_url": "https://avatars3.githubusercontent.com/u/13313058?v=4",
12 | "profile": "https://aralroca.com",
13 | "contributions": [
14 | "maintenance",
15 | "code"
16 | ]
17 | },
18 | {
19 | "login": "vincentducorps",
20 | "name": "Vincent Ducorps",
21 | "avatar_url": "https://avatars0.githubusercontent.com/u/6338609?v=4",
22 | "profile": "https://twitter.com/vincentducorps",
23 | "contributions": [
24 | "code"
25 | ]
26 | },
27 | {
28 | "login": "BjoernRave",
29 | "name": "Björn Rave",
30 | "avatar_url": "https://avatars3.githubusercontent.com/u/36173920?v=4",
31 | "profile": "https://www.rahwn.com",
32 | "contributions": [
33 | "code"
34 | ]
35 | },
36 | {
37 | "login": "justincy",
38 | "name": "Justin",
39 | "avatar_url": "https://avatars2.githubusercontent.com/u/1037458?v=4",
40 | "profile": "https://github.com/justincy",
41 | "contributions": [
42 | "code"
43 | ]
44 | },
45 | {
46 | "login": "psanlorenzo",
47 | "name": "Pol",
48 | "avatar_url": "https://avatars2.githubusercontent.com/u/42739235?v=4",
49 | "profile": "https://github.com/psanlorenzo",
50 | "contributions": [
51 | "infra"
52 | ]
53 | },
54 | {
55 | "login": "ftonato",
56 | "name": "Ademílson F. Tonato",
57 | "avatar_url": "https://avatars2.githubusercontent.com/u/5417662?v=4",
58 | "profile": "https://twitter.com/ftonato",
59 | "contributions": [
60 | "code"
61 | ]
62 | },
63 | {
64 | "login": "Faulik",
65 | "name": "Faul",
66 | "avatar_url": "https://avatars3.githubusercontent.com/u/749225?v=4",
67 | "profile": "https://github.com/Faulik",
68 | "contributions": [
69 | "code"
70 | ]
71 | },
72 | {
73 | "login": "bickmaev5",
74 | "name": "bickmaev5",
75 | "avatar_url": "https://avatars2.githubusercontent.com/u/13235737?v=4",
76 | "profile": "https://github.com/bickmaev5",
77 | "contributions": [
78 | "code"
79 | ]
80 | },
81 | {
82 | "login": "pgrimaud",
83 | "name": "Pierre Grimaud",
84 | "avatar_url": "https://avatars1.githubusercontent.com/u/1866496?v=4",
85 | "profile": "https://p.ier.re",
86 | "contributions": [
87 | "doc"
88 | ]
89 | },
90 | {
91 | "login": "dnepro",
92 | "name": "Roman Minchyn",
93 | "avatar_url": "https://avatars0.githubusercontent.com/u/6419697?v=4",
94 | "profile": "https://roman-minchyn.de",
95 | "contributions": [
96 | "doc",
97 | "code"
98 | ]
99 | },
100 | {
101 | "login": "lone-cloud",
102 | "name": "Egor",
103 | "avatar_url": "https://avatars2.githubusercontent.com/u/595980?v=4",
104 | "profile": "https://www.egorphilippov.me/",
105 | "contributions": [
106 | "code"
107 | ]
108 | },
109 | {
110 | "login": "dhobbs",
111 | "name": "Darren",
112 | "avatar_url": "https://avatars2.githubusercontent.com/u/367375?v=4",
113 | "profile": "https://github.com/dhobbs",
114 | "contributions": [
115 | "code"
116 | ]
117 | },
118 | {
119 | "login": "giovannigiordano",
120 | "name": "Giovanni Giordano",
121 | "avatar_url": "https://avatars3.githubusercontent.com/u/15145952?v=4",
122 | "profile": "https://github.com/giovannigiordano",
123 | "contributions": [
124 | "code"
125 | ]
126 | },
127 | {
128 | "login": "kidnapkin",
129 | "name": "Eugene",
130 | "avatar_url": "https://avatars0.githubusercontent.com/u/9214135?v=4",
131 | "profile": "https://github.com/kidnapkin",
132 | "contributions": [
133 | "code"
134 | ]
135 | },
136 | {
137 | "login": "hibearpanda",
138 | "name": "Andrew Chung",
139 | "avatar_url": "https://avatars2.githubusercontent.com/u/11482515?v=4",
140 | "profile": "https://andrew-c.com",
141 | "contributions": [
142 | "code"
143 | ]
144 | },
145 | {
146 | "login": "thanhlmm",
147 | "name": "Thanh Minh",
148 | "avatar_url": "https://avatars0.githubusercontent.com/u/9281080?v=4",
149 | "profile": "http://cuthanh.com",
150 | "contributions": [
151 | "code"
152 | ]
153 | },
154 | {
155 | "login": "croutonn",
156 | "name": "crouton",
157 | "avatar_url": "https://avatars1.githubusercontent.com/u/68943932?v=4",
158 | "profile": "https://github.com/croutonn",
159 | "contributions": [
160 | "code"
161 | ]
162 | },
163 | {
164 | "login": "dislick",
165 | "name": "Patrick",
166 | "avatar_url": "https://avatars3.githubusercontent.com/u/3121902?v=4",
167 | "profile": "http://patrickmuff.ch",
168 | "contributions": [
169 | "doc"
170 | ]
171 | },
172 | {
173 | "login": "vimutti77",
174 | "name": "Vantroy",
175 | "avatar_url": "https://avatars3.githubusercontent.com/u/27840664?v=4",
176 | "profile": "https://github.com/vimutti77",
177 | "contributions": [
178 | "code"
179 | ]
180 | },
181 | {
182 | "login": "josephfarina",
183 | "name": "Joey",
184 | "avatar_url": "https://avatars1.githubusercontent.com/u/17398284?v=4",
185 | "profile": "https://www.npmjs.com/~farinajoey",
186 | "contributions": [
187 | "code"
188 | ]
189 | },
190 | {
191 | "login": "gurkerl83",
192 | "name": "gurkerl83",
193 | "avatar_url": "https://avatars0.githubusercontent.com/u/301689?v=4",
194 | "profile": "https://github.com/gurkerl83",
195 | "contributions": [
196 | "code"
197 | ]
198 | },
199 | {
200 | "login": "tperamaki",
201 | "name": "Teemu Perämäki",
202 | "avatar_url": "https://avatars0.githubusercontent.com/u/26067988?v=4",
203 | "profile": "https://github.com/tperamaki",
204 | "contributions": [
205 | "doc"
206 | ]
207 | },
208 | {
209 | "login": "luisgserrano",
210 | "name": "Luis Serrano",
211 | "avatar_url": "https://avatars3.githubusercontent.com/u/2024164?v=4",
212 | "profile": "https://github.com/luisgserrano",
213 | "contributions": [
214 | "doc"
215 | ]
216 | },
217 | {
218 | "login": "j-schumann",
219 | "name": "j-schumann",
220 | "avatar_url": "https://avatars.githubusercontent.com/u/114239?v=4",
221 | "profile": "https://github.com/j-schumann",
222 | "contributions": [
223 | "code"
224 | ]
225 | },
226 | {
227 | "login": "andrehsu",
228 | "name": "Andre Hsu",
229 | "avatar_url": "https://avatars.githubusercontent.com/u/4470828?v=4",
230 | "profile": "https://github.com/andrehsu",
231 | "contributions": [
232 | "code"
233 | ]
234 | },
235 | {
236 | "login": "slevy85",
237 | "name": "slevy85",
238 | "avatar_url": "https://avatars.githubusercontent.com/u/18260229?v=4",
239 | "profile": "https://github.com/slevy85",
240 | "contributions": [
241 | "code"
242 | ]
243 | },
244 | {
245 | "login": "berndartmueller",
246 | "name": "Bernd Artmüller",
247 | "avatar_url": "https://avatars.githubusercontent.com/u/761018?v=4",
248 | "profile": "https://www.berndartmueller.com",
249 | "contributions": [
250 | "code"
251 | ]
252 | },
253 | {
254 | "login": "rihardssceredins",
255 | "name": "Rihards Ščeredins",
256 | "avatar_url": "https://avatars.githubusercontent.com/u/23099574?v=4",
257 | "profile": "https://github.com/rihardssceredins",
258 | "contributions": [
259 | "code"
260 | ]
261 | },
262 | {
263 | "login": "Its-Just-Nans",
264 | "name": "n4n5",
265 | "avatar_url": "https://avatars.githubusercontent.com/u/56606507?v=4",
266 | "profile": "https://its-just-nans.github.io",
267 | "contributions": [
268 | "doc"
269 | ]
270 | },
271 | {
272 | "login": "rubenmoya",
273 | "name": "Rubén Moya",
274 | "avatar_url": "https://avatars.githubusercontent.com/u/905225?v=4",
275 | "profile": "https://rubenmoya.dev",
276 | "contributions": [
277 | "code"
278 | ]
279 | },
280 | {
281 | "login": "testerez",
282 | "name": "Tom Esterez",
283 | "avatar_url": "https://avatars.githubusercontent.com/u/815236?v=4",
284 | "profile": "https://github.com/testerez",
285 | "contributions": [
286 | "code"
287 | ]
288 | },
289 | {
290 | "login": "dndhm",
291 | "name": "Dan Needham",
292 | "avatar_url": "https://avatars.githubusercontent.com/u/1122983?v=4",
293 | "profile": "http://www.dan-needham.com",
294 | "contributions": [
295 | "code",
296 | "test",
297 | "doc"
298 | ]
299 | },
300 | {
301 | "login": "bmvantunes",
302 | "name": "Bruno Antunes",
303 | "avatar_url": "https://avatars.githubusercontent.com/u/9042965?v=4",
304 | "profile": "https://www.youtube.com/BrunoAntunesPT",
305 | "contributions": [
306 | "code"
307 | ]
308 | },
309 | {
310 | "login": "kaan-atakan",
311 | "name": "Kaan Atakan",
312 | "avatar_url": "https://avatars.githubusercontent.com/u/56063979?v=4",
313 | "profile": "https://github.com/kaan-atakan",
314 | "contributions": [
315 | "code"
316 | ]
317 | },
318 | {
319 | "login": "groomain",
320 | "name": "Romain",
321 | "avatar_url": "https://avatars.githubusercontent.com/u/3601848?v=4",
322 | "profile": "https://github.com/groomain",
323 | "contributions": [
324 | "code"
325 | ]
326 | },
327 | {
328 | "login": "ajmnz",
329 | "name": "Arnau Jiménez",
330 | "avatar_url": "https://avatars.githubusercontent.com/u/57961822?v=4",
331 | "profile": "http://ajb.cat",
332 | "contributions": [
333 | "code"
334 | ]
335 | },
336 | {
337 | "login": "edwinveldhuizen",
338 | "name": "Edwin Veldhuizen",
339 | "avatar_url": "https://avatars.githubusercontent.com/u/1787915?v=4",
340 | "profile": "https://github.com/edwinveldhuizen",
341 | "contributions": [
342 | "code"
343 | ]
344 | },
345 | {
346 | "login": "duc-gp",
347 | "name": "Duc Ngo Viet",
348 | "avatar_url": "https://avatars.githubusercontent.com/u/40763918?v=4",
349 | "profile": "http://dviet.de",
350 | "contributions": [
351 | "code"
352 | ]
353 | },
354 | {
355 | "login": "bilLkarkariy",
356 | "name": "Billel Helali",
357 | "avatar_url": "https://avatars.githubusercontent.com/u/43569083?v=4",
358 | "profile": "https://github.com/bilLkarkariy",
359 | "contributions": [
360 | "code"
361 | ]
362 | },
363 | {
364 | "login": "wuifdesign",
365 | "name": "Wuif",
366 | "avatar_url": "https://avatars.githubusercontent.com/u/5678318?v=4",
367 | "profile": "https://github.com/wuifdesign",
368 | "contributions": [
369 | "code"
370 | ]
371 | },
372 | {
373 | "login": "MrPumpking",
374 | "name": "Michał Bar",
375 | "avatar_url": "https://avatars.githubusercontent.com/u/9134970?v=4",
376 | "profile": "https://michal.bar",
377 | "contributions": [
378 | "code"
379 | ]
380 | },
381 | {
382 | "login": "wuifdesign",
383 | "name": "Wuif",
384 | "avatar_url": "https://avatars.githubusercontent.com/u/5678318?v=4",
385 | "profile": "https://github.com/wuifdesign",
386 | "contributions": [
387 | "code"
388 | ]
389 | },
390 | {
391 | "login": "marcesengel",
392 | "name": "Marces Engel",
393 | "avatar_url": "https://avatars.githubusercontent.com/u/6208890?v=4",
394 | "profile": "https://github.com/marcesengel",
395 | "contributions": [
396 | "code"
397 | ]
398 | },
399 | {
400 | "login": "MrPumpking",
401 | "name": "Michał Bar",
402 | "avatar_url": "https://avatars.githubusercontent.com/u/9134970?v=4",
403 | "profile": "https://michal.bar",
404 | "contributions": [
405 | "code"
406 | ]
407 | },
408 | {
409 | "login": "Dragate",
410 | "name": "Dragate",
411 | "avatar_url": "https://avatars.githubusercontent.com/u/28112929?v=4",
412 | "profile": "https://github.com/Dragate",
413 | "contributions": [
414 | "code"
415 | ]
416 | },
417 | {
418 | "login": "marcesengel",
419 | "name": "Marces Engel",
420 | "avatar_url": "https://avatars.githubusercontent.com/u/6208890?v=4",
421 | "profile": "https://github.com/marcesengel",
422 | "contributions": [
423 | "code"
424 | ]
425 | },
426 | {
427 | "login": "vascosilvaa",
428 | "name": "Vasco Silva",
429 | "avatar_url": "https://avatars.githubusercontent.com/u/16561642?v=4",
430 | "profile": "https://github.com/vascosilvaa",
431 | "contributions": [
432 | "code"
433 | ]
434 | },
435 | {
436 | "login": "StLyn4",
437 | "name": "Vsevolod Volkov",
438 | "avatar_url": "https://avatars.githubusercontent.com/u/73965070?v=4",
439 | "profile": "https://github.com/StLyn4",
440 | "contributions": [
441 | "code"
442 | ]
443 | },
444 | {
445 | "login": "felixonmars",
446 | "name": "Felix Yan",
447 | "avatar_url": "https://avatars.githubusercontent.com/u/1006477?v=4",
448 | "profile": "https://github.com/felixonmars",
449 | "contributions": [
450 | "doc"
451 | ]
452 | },
453 | {
454 | "login": "alziqziq",
455 | "name": "Muhammad Al Ziqri",
456 | "avatar_url": "https://avatars.githubusercontent.com/u/29282122?v=4",
457 | "profile": "https://github.com/alziqziq",
458 | "contributions": [
459 | "code"
460 | ]
461 | },
462 | {
463 | "login": "marcelotk15",
464 | "name": "Marcelo Oliveira",
465 | "avatar_url": "https://avatars.githubusercontent.com/u/4443094?v=4",
466 | "profile": "http://teka.dev",
467 | "contributions": [
468 | "code"
469 | ]
470 | },
471 | {
472 | "login": "SimplyComplexable",
473 | "name": "Zack Sunderland",
474 | "avatar_url": "https://avatars.githubusercontent.com/u/8563846?v=4",
475 | "profile": "https://github.com/SimplyComplexable",
476 | "contributions": [
477 | "code"
478 | ]
479 | },
480 | {
481 | "login": "aovens-quantifi",
482 | "name": "Andrew Ovens",
483 | "avatar_url": "https://avatars.githubusercontent.com/u/107420510?v=4",
484 | "profile": "http://andrewovens.com",
485 | "contributions": [
486 | "code"
487 | ]
488 | },
489 | {
490 | "login": "danielpid",
491 | "name": "dANi",
492 | "avatar_url": "https://avatars.githubusercontent.com/u/16427301?v=4",
493 | "profile": "https://github.com/danielpid",
494 | "contributions": [
495 | "code"
496 | ]
497 | },
498 | {
499 | "login": "TheMatrixan",
500 | "name": "Mateusz Lesiak",
501 | "avatar_url": "https://avatars.githubusercontent.com/u/28862367?v=4",
502 | "profile": "https://thematrixan.github.io/",
503 | "contributions": [
504 | "code"
505 | ]
506 | },
507 | {
508 | "login": "Curetix",
509 | "name": "Curetix",
510 | "avatar_url": "https://avatars.githubusercontent.com/u/15160542?v=4",
511 | "profile": "https://github.com/Curetix",
512 | "contributions": [
513 | "doc"
514 | ]
515 | },
516 | {
517 | "login": "crs1138",
518 | "name": "Honza",
519 | "avatar_url": "https://avatars.githubusercontent.com/u/1313681?v=4",
520 | "profile": "https://github.com/crs1138",
521 | "contributions": [
522 | "maintenance"
523 | ]
524 | },
525 | {
526 | "login": "BandhiyaHardik",
527 | "name": "HardikBandhiya",
528 | "avatar_url": "https://avatars.githubusercontent.com/u/110784317?v=4",
529 | "profile": "https://github.com/BandhiyaHardik",
530 | "contributions": [
531 | "doc"
532 | ]
533 | },
534 | {
535 | "login": "timotew",
536 | "name": "Tim O. Peters",
537 | "avatar_url": "https://avatars.githubusercontent.com/u/12928383?v=4",
538 | "profile": "https://www.linkedin.com/in/timotew/",
539 | "contributions": [
540 | "code"
541 | ]
542 | },
543 | {
544 | "login": "hydRAnger",
545 | "name": "Li Ming",
546 | "avatar_url": "https://avatars.githubusercontent.com/u/1228449?v=4",
547 | "profile": "https://github.com/hydRAnger",
548 | "contributions": [
549 | "doc"
550 | ]
551 | },
552 | {
553 | "login": "acidfernando",
554 | "name": "Fernando García Hernández",
555 | "avatar_url": "https://avatars.githubusercontent.com/u/86410308?v=4",
556 | "profile": "https://github.com/acidfernando",
557 | "contributions": [
558 | "code"
559 | ]
560 | },
561 | {
562 | "login": "hichemfantar",
563 | "name": "Hichem Fantar",
564 | "avatar_url": "https://avatars.githubusercontent.com/u/34947993?v=4",
565 | "profile": "https://www.hichemfantar.com/",
566 | "contributions": [
567 | "code"
568 | ]
569 | },
570 | {
571 | "login": "huseyinonalcom",
572 | "name": "Huseyin Onal",
573 | "avatar_url": "https://avatars.githubusercontent.com/u/65453275?v=4",
574 | "profile": "https://github.com/huseyinonalcom",
575 | "contributions": [
576 | "code"
577 | ]
578 | },
579 | {
580 | "login": "jessemartin",
581 | "name": "Jesse Martin",
582 | "avatar_url": "https://avatars.githubusercontent.com/u/1626240?v=4",
583 | "profile": "https://github.com/jessemartin",
584 | "contributions": [
585 | "code"
586 | ]
587 | }
588 | ],
589 | "contributorsPerLine": 7,
590 | "projectName": "next-translate",
591 | "projectOwner": "aralroca",
592 | "repoType": "github",
593 | "repoHost": "https://github.com",
594 | "skipCi": true,
595 | "commitConvention": "angular",
596 | "commitType": "docs"
597 | }
598 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "test": {
4 | "plugins": [["transform-es2015-modules-commonjs"]],
5 | "presets": [
6 | "@babel/preset-typescript",
7 | ["next/babel", { "preset-env": { "modules": "commonjs" } }]
8 | ]
9 | }
10 | },
11 | "ignore": ["node_modules"],
12 | "comments": false
13 | }
14 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [pull_request]
3 | jobs:
4 | build:
5 | name: Test
6 | runs-on: ${{ matrix.os }}
7 | strategy:
8 | matrix:
9 | os: [ubuntu-latest, windows-latest]
10 | node-version: [16.10.x, 18.x, 20.x]
11 | steps:
12 | - uses: actions/checkout@v1
13 | - name: Use Node.js ${{ matrix.node-version }}
14 | uses: actions/setup-node@v1
15 | with:
16 | node-version: ${{ matrix.node-version }}
17 | - name: Install dependencies
18 | run: |
19 | npm install -g yarn
20 | - name: yarn install, build, and test
21 | run: |
22 | yarn install
23 | yarn build
24 | yarn test
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.github
3 | !docs
4 | !docs/*
5 | !*.json
6 | !*.js
7 | !*.ts
8 | *.d.ts
9 | !*.tsx
10 | !src/app-dir
11 | !examples
12 | !examples/**/*
13 | !src
14 | !__tests__
15 | !.babelrc
16 | !.husky
17 | !.husky/*
18 | !.prettierignore
19 |
20 | .DS_Store
21 |
22 | # dependencies
23 | /node_modules
24 | /examples/*/node_modules
25 | /examples/*/yarn.lock
26 |
27 | # next.js internal files
28 | out
29 | /.next/
30 | /examples/*/.next/
31 |
32 | # debug
33 | npm-debug.log*
34 | yarn-debug.log*
35 | yarn-error.log*
36 |
37 | # vscode
38 | .vscode
39 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn husky
5 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org/
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | README.md
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, please first discuss the change you wish to make via issue,
4 | email, or any other method with the owners of this repository before making a change.
5 |
6 | Please note we have a code of conduct, please follow it in all your interactions with the project.
7 |
8 | ## Pull Request Process
9 |
10 | 1. Ensure you are doing the PR to the canary branch.
11 | 2. Write the failing tests about the issue / feature you are working on.
12 | 3. Do the fix taking care to add the necessary TypeScript types.
13 | 4. Update the README.md with details of changes to the interface.
14 | 5. You may merge the Pull Request in once you have the approval of at least one maintainer, or if you
15 | do not have permission to do that, you may request the maintainer to merge it for you.
16 |
17 | ## Code of Conduct
18 |
19 | ### Our Pledge
20 |
21 | In the interest of fostering an open and welcoming environment, we as
22 | contributors and maintainers pledge to making participation in our project and
23 | our community a harassment-free experience for everyone, regardless of age, body
24 | size, disability, ethnicity, gender identity and expression, level of experience,
25 | nationality, personal appearance, race, religion, or sexual identity and
26 | orientation.
27 |
28 | ### Our Standards
29 |
30 | Examples of behavior that contributes to creating a positive environment
31 | include:
32 |
33 | - Using welcoming and inclusive language
34 | - Being respectful of differing viewpoints and experiences
35 | - Gracefully accepting constructive criticism
36 | - Focusing on what is best for the community
37 | - Showing empathy towards other community members
38 |
39 | Examples of unacceptable behavior by participants include:
40 |
41 | - The use of sexualized language or imagery and unwelcome sexual attention or
42 | advances
43 | - Trolling, insulting/derogatory comments, and personal or political attacks
44 | - Public or private harassment
45 | - Publishing others' private information, such as a physical or electronic
46 | address, without explicit permission
47 | - Other conduct which could reasonably be considered inappropriate in a
48 | professional setting
49 |
50 | ### Our Responsibilities
51 |
52 | Project maintainers are responsible for clarifying the standards of acceptable
53 | behavior and are expected to take appropriate and fair corrective action in
54 | response to any instances of unacceptable behavior.
55 |
56 | Project maintainers have the right and responsibility to remove, edit, or
57 | reject comments, commits, code, wiki edits, issues, and other contributions
58 | that are not aligned to this Code of Conduct, or to ban temporarily or
59 | permanently any contributor for other behaviors that they deem inappropriate,
60 | threatening, offensive, or harmful.
61 |
62 | ### Scope
63 |
64 | This Code of Conduct applies both within project spaces and in public spaces
65 | when an individual is representing the project or its community. Examples of
66 | representing a project or community include using an official project e-mail
67 | address, posting via an official social media account, or acting as an appointed
68 | representative at an online or offline event. Representation of a project may be
69 | further defined and clarified by project maintainers.
70 |
71 | ### Enforcement
72 |
73 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
74 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
75 | complaints will be reviewed and investigated and will result in a response that
76 | is deemed necessary and appropriate to the circumstances. The project team is
77 | obligated to maintain confidentiality with regard to the reporter of an incident.
78 | Further details of specific enforcement policies may be posted separately.
79 |
80 | Project maintainers who do not follow or enforce the Code of Conduct in good
81 | faith may face temporary or permanent repercussions as determined by other
82 | members of the project's leadership.
83 |
84 | ### Attribution
85 |
86 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
87 | available at [http://contributor-covenant.org/version/1/4][version]
88 |
89 | [homepage]: http://contributor-covenant.org
90 | [version]: http://contributor-covenant.org/version/1/4/
91 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | MIT License
4 |
5 | Copyright (c) 2021 Aral Roca Gomez
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/__tests__/Trans.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, cleanup } from '@testing-library/react'
3 | import I18nProvider from '../src/I18nProvider'
4 | import Trans from '../src/Trans'
5 |
6 | const TestEnglish = ({ namespaces, logger, ...props }) => {
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | describe('Trans', () => {
15 | afterEach(cleanup)
16 |
17 | describe('without components', () => {
18 | test('should work the same way than useTranslate', () => {
19 | const i18nKey = 'ns:number'
20 | const expected = 'The number is 42'
21 | const withSingular = {
22 | number: 'The number is {{ num }}',
23 | }
24 | const { container } = render(
25 |
30 | )
31 | expect(container.textContent).toContain(expected)
32 | })
33 |
34 | test('should work with nested keys', () => {
35 | const i18nKey = 'ns:parent.child'
36 | const expected = 'The number is 42'
37 | const withSingular = {
38 | parent: {
39 | child: 'The number is {{ num }}',
40 | },
41 | }
42 | const { container } = render(
43 |
48 | )
49 | expect(container.textContent).toContain(expected)
50 | })
51 |
52 | test('should work with arrays', () => {
53 | const i18nKey = 'ns:parent.child'
54 | const expectedFirstElement = 'First element 42'
55 | const expectedSecondElement = 'Second element 42'
56 | const withSingular = {
57 | parent: {
58 | child: [
59 | '<0>First0> element {{num}}',
60 | '<0>Second0> element {{num}}',
61 | ],
62 | },
63 | }
64 | const { container } = render(
65 | ]}
71 | />
72 | )
73 | expect(container.innerHTML).toContain(expectedFirstElement)
74 | expect(container.innerHTML).toContain(expectedSecondElement)
75 | })
76 |
77 | test('should work with arrays and singulars', () => {
78 | const i18nKey = 'ns:withsingular'
79 | const expected = 'The number is one'
80 | const withSingular = {
81 | withsingular_0: ['<0>The number0> is ZERO!'],
82 | withsingular_one: ['<0>The number0> is one'],
83 | withsingular_other: ['<0>The number0> is plural'],
84 | }
85 |
86 | const { container } = render(
87 | ]}
93 | />
94 | )
95 |
96 | expect(container.innerHTML).toContain(expected)
97 | })
98 |
99 | test('should work with arrays and plurals', () => {
100 | const i18nKey = 'ns:withsingular'
101 | const expected = 'The number is plural'
102 | const withSingular = {
103 | withsingular: ['<0>First0> is not zero'],
104 | withsingular_0: ['<0>The number0> is ZERO!'],
105 | withsingular_other: ['<0>The number0> is plural'],
106 | }
107 |
108 | const { container } = render(
109 | ]}
115 | />
116 | )
117 |
118 | expect(container.innerHTML).toContain(expected)
119 | })
120 |
121 | test('should work with nested keys and custom keySeparator', () => {
122 | const i18nKey = 'ns:parent_child'
123 | const expected = 'The number is 42'
124 | const withSingular = {
125 | parent: {
126 | child: 'The number is {{ num }}',
127 | },
128 | }
129 |
130 | const config = { keySeparator: '_' }
131 |
132 | const { container } = render(
133 |
138 |
139 |
140 | )
141 | expect(container.textContent).toContain(expected)
142 | })
143 |
144 | test('should work with no keySeparator', () => {
145 | const i18nKey = 'ns:parent.child'
146 | const expected = 'The number is 42'
147 | const withSingular = {
148 | 'parent.child': 'The number is {{ num }}',
149 | }
150 |
151 | const config = { keySeparator: false }
152 |
153 | const { container } = render(
154 |
159 |
160 |
161 | )
162 | expect(container.textContent).toContain(expected)
163 | })
164 |
165 | test('should work with ns prop', () => {
166 | const i18nKey = 'number'
167 | const expected = 'The number is 42'
168 | const ns = {
169 | number: 'The number is 42',
170 | }
171 |
172 | const config = { keySeparator: false }
173 |
174 | const { container } = render(
175 |
176 |
177 |
178 | )
179 | expect(container.textContent).toContain(expected)
180 | })
181 |
182 | test('should work with flat keys', () => {
183 | const i18nKey = 'this.is.a.flat.key'
184 | const expected = 'The number is 42'
185 | const ns = {
186 | 'this.is.a.flat.key': 'The number is 42',
187 | }
188 |
189 | const config = { keySeparator: false }
190 |
191 | const { container } = render(
192 |
193 |
194 |
195 | )
196 | expect(container.textContent).toContain(expected)
197 | })
198 |
199 | test('should work the same way than useTranslate with default value', () => {
200 | console.warn = jest.fn()
201 | const i18nKey = 'ns:number'
202 | const expected = 'The number is 42'
203 | const expectedWarning =
204 | '[next-translate] "ns:number" is missing in current namespace configuration. Try adding "number" to the namespace "ns".'
205 |
206 | const { container } = render(
207 |
213 | )
214 | expect(container.textContent).toContain(expected)
215 | expect(console.warn).toBeCalledWith(expectedWarning)
216 | })
217 | })
218 |
219 | describe('with components', () => {
220 | test('should work with HTML5 Elements', () => {
221 | const i18nKey = 'ns:number'
222 | const expectedText = 'The number is 42'
223 | const expectedHTML = '
The number is 42
'
224 | const withSingular = {
225 | number: '<0>The number is <1>{{ num }}1>0>',
226 | }
227 | const { container } = render(
228 | , ]}
233 | />
234 | )
235 | expect(container.textContent).toContain(expectedText)
236 | expect(container.innerHTML).toContain(expectedHTML)
237 | })
238 |
239 | test('should work with React Components', () => {
240 | const i18nKey = 'ns:number'
241 | const expectedText = 'The number is 42'
242 | const expectedHTML = 'The number is 42
'
243 | const withSingular = {
244 | number: '<0>The number is <1>{{ num }}1>0>',
245 | }
246 | const H1 = (p) =>
247 | const B = (p) =>
248 |
249 | const { container } = render(
250 | , ]}
255 | />
256 | )
257 | expect(container.textContent).toContain(expectedText)
258 | expect(container.innerHTML).toContain(expectedHTML)
259 | })
260 |
261 | test('should work with very nested components', () => {
262 | const i18nKey = 'ns:number'
263 | const expectedText = 'Is the number 42?'
264 | const expectedHTML = ''
265 | const withSingular = {
266 | number: '<0><1>Is1> <2>the <3>number3>2> {{num}}?0>',
267 | }
268 |
269 | const { container } = render(
270 | , , , ]}
275 | />
276 | )
277 | expect(container.textContent).toContain(expectedText)
278 | expect(container.innerHTML).toContain(expectedHTML)
279 | })
280 |
281 | test('should work without replacing the HTMLElement if the index is incorrectly', () => {
282 | const i18nKey = 'common:test-html'
283 | const expectedHTML = 'test with bad index.'
284 | const common = {
285 | 'test-html': 'test <10>with bad index10>.',
286 | }
287 |
288 | const { container } = render(
289 | ]}
293 | />
294 | )
295 | expect(container.innerHTML).toContain(expectedHTML)
296 | })
297 |
298 | test('should work if translation is missing', () => {
299 | const i18nKey = 'common:test-html-missing'
300 | const expectedHTML = ''
301 | const common = {
302 | 'test-html': 'test <10>with missing translation10>.',
303 | }
304 |
305 | const { container } = render(
306 | ]}
310 | />
311 | )
312 | expect(container.innerHTML).toContain(expectedHTML)
313 | })
314 | })
315 |
316 | describe('components prop as a object', () => {
317 | test('should work with component as a object', () => {
318 | const i18nKey = 'common:test-html'
319 | const expectedHTML = 'test components as a object.'
320 | const common = {
321 | 'test-html': 'test components as a object.',
322 | }
323 |
324 | const { container } = render(
325 | }}
329 | />
330 | )
331 | expect(container.innerHTML).toContain(expectedHTML)
332 | })
333 |
334 | test('should work with component as a object of React Components', () => {
335 | const i18nKey = 'common:test-html'
336 | const expectedHTML = 'test components as a object.'
337 | const common = {
338 | 'test-html': 'test components as a object.',
339 | }
340 |
341 | const Component = ({ children }) => {children}
342 |
343 | const { container } = render(
344 | }}
348 | />
349 | )
350 | expect(container.innerHTML).toContain(expectedHTML)
351 | })
352 |
353 | test('should work with component as a object without replacing the HTMLElement if the key is incorrectly', () => {
354 | const i18nKey = 'common:test-html'
355 | const expectedHTML =
356 | 'test components as a object.'
357 | const common = {
358 | 'test-html':
359 | 'test components as a object.',
360 | }
361 |
362 | const Component = ({ children }) => {children}
363 |
364 | const { container } = render(
365 | , u: }}
369 | />
370 | )
371 | expect(container.innerHTML).toContain(expectedHTML)
372 | })
373 |
374 | test('should work if translation is missing', () => {
375 | const i18nKey = 'common:test-html-missing'
376 | const expectedHTML = ''
377 | const common = {
378 | 'test-html': 'test with missing translation.',
379 | }
380 |
381 | const { container } = render(
382 | }}
386 | />
387 | )
388 | expect(container.innerHTML).toContain(expectedHTML)
389 | })
390 | })
391 |
392 | describe('logger', () => {
393 | test('should log a warn key if a key does not exist in the namespace', () => {
394 | console.warn = jest.fn()
395 | const i18nKey = 'ns:number'
396 | const expected =
397 | '[next-translate] "ns:number" is missing in current namespace configuration. Try adding "number" to the namespace "ns".'
398 |
399 | const withSingular = {}
400 | render(
401 |
402 | )
403 | expect(console.warn).toBeCalledWith(expected)
404 | })
405 |
406 | test('should log a warn key if it has a fallback', () => {
407 | console.warn = jest.fn()
408 | const i18nKey = 'ns:number'
409 | const expected =
410 | '[next-translate] "ns:number" is missing in current namespace configuration. Try adding "number" to the namespace "ns".'
411 |
412 | const withSingular = { fllbck: 'Im a fallback' }
413 | const { container } = render(
414 |
419 | )
420 | expect(console.warn).toBeCalledWith(expected)
421 | expect(container.innerHTML).toContain('Im a fallback')
422 | })
423 |
424 | test('should log a warn key multiple times if all fallbacks are also missing', () => {
425 | console.warn = jest.fn()
426 | const i18nKey = 'ns:number'
427 | const expected =
428 | '[next-translate] "ns:number" is missing in current namespace configuration. Try adding "number" to the namespace "ns".'
429 |
430 | const withSingular = { fallback4: 'Im a fallback number 4' }
431 | const { container } = render(
432 |
442 | )
443 | expect(console.warn).toBeCalledWith(expected)
444 | expect(console.warn.mock.calls.length).toBe(4)
445 | expect(container.innerHTML).toContain('Im a fallback number 4')
446 | })
447 |
448 | test('should log correctly if the value includes a ":", for example an URL', () => {
449 | console.warn = jest.fn()
450 | const i18nKey = 'ns:https://linkinsomelanguage.com'
451 | const expected =
452 | '[next-translate] "ns:https://linkinsomelanguage.com" is missing in current namespace configuration. Try adding "https://linkinsomelanguage.com" to the namespace "ns".'
453 |
454 | const withSingular = {}
455 | render(
456 |
457 | )
458 | expect(console.warn).toBeCalledWith(expected)
459 | })
460 |
461 | test('should not log when the translation have ":" inside', () => {
462 | console.warn = jest.fn()
463 | const i18nKey = 'Some text without namespace'
464 | const expected =
465 | '[next-translate] The text "Some text without namespace" has no namespace in front of it.'
466 |
467 | const withSingular = {}
468 | render(
469 |
470 | )
471 | expect(console.warn).toBeCalledWith(expected)
472 | })
473 |
474 | test('should log a warn key if a nested key does not exist in the namespace', () => {
475 | console.warn = jest.fn()
476 | const i18nKey = 'ns:parent.child'
477 | const expected =
478 | '[next-translate] "ns:parent.child" is missing in current namespace configuration. Try adding "parent.child" to the namespace "ns".'
479 |
480 | const withSingular = {}
481 | render(
482 |
483 | )
484 | expect(console.warn).toBeCalledWith(expected)
485 | })
486 |
487 | test('should pass the key and the namespace to the logger function if the key does not exist in the namespace', () => {
488 | console.log = jest.fn()
489 | const i18nKey = 'ns:number'
490 | const logger = ({ i18nKey, namespace }) =>
491 | console.log(`Logger: ${i18nKey} ${namespace}`)
492 | const expected = 'Logger: number ns'
493 |
494 | const withSingular = {}
495 | render(
496 |
501 | )
502 | expect(console.log).toBeCalledWith(expected)
503 | })
504 | })
505 | })
506 |
--------------------------------------------------------------------------------
/__tests__/TransText.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, cleanup } from '@testing-library/react'
3 |
4 | import TransText from '../src/TransText'
5 |
6 | describe('TransText', () => {
7 | afterEach(cleanup)
8 |
9 | describe('without components', () => {
10 | test('should return the provided text', () => {
11 | const text = 'The number is 42'
12 |
13 | const { container } = render(
14 |
15 | )
16 | expect(container.textContent).toContain(text)
17 | })
18 | })
19 |
20 | describe('with components', () => {
21 | test('should work with HTML5 Elements', () => {
22 | const expectedText = 'The number is 42'
23 | const text = '<0>The number is <1>421>0>'
24 | const expectedHTML = 'The number is 42
'
25 |
26 | const { container } = render(
27 | , ]}
30 | />
31 | )
32 | expect(container.textContent).toContain(expectedText)
33 | expect(container.innerHTML).toContain(expectedHTML)
34 | })
35 |
36 | test('should work with React Components', () => {
37 | const text = '<0>The number is <1>421>0>'
38 | const expectedText = 'The number is 42'
39 | const expectedHTML = 'The number is 42
'
40 | const H1 = (p) =>
41 | const B = (p) =>
42 |
43 | const { container } = render(
44 | , ]}
47 | />
48 | )
49 | expect(container.textContent).toContain(expectedText)
50 | expect(container.innerHTML).toContain(expectedHTML)
51 | })
52 |
53 | test('should work with very nested components', () => {
54 | const text = '<0><1>Is1> <2>the <3>number3>2> 42?0>'
55 | const expectedText = 'Is the number 42?'
56 | const expectedHTML = ''
57 |
58 | const { container } = render(
59 | , , , ]}
62 | />
63 | )
64 | expect(container.textContent).toContain(expectedText)
65 | expect(container.innerHTML).toContain(expectedHTML)
66 | })
67 |
68 | test('should work without replacing the HTMLElement if the index is incorrectly', () => {
69 | const text = 'test <10>with bad index10>.'
70 | const expectedHTML = 'test with bad index.'
71 |
72 | const { container } = render(
73 | ]}
76 | />
77 | )
78 | expect(container.innerHTML).toContain(expectedHTML)
79 | })
80 | })
81 |
82 | describe('components prop as a object', () => {
83 | test('should work with component as a object', () => {
84 | const text = 'test components as a object.'
85 | const expectedHTML = 'test components as a object.'
86 |
87 | const { container } = render(
88 | }}
91 | />
92 | )
93 | expect(container.innerHTML).toContain(expectedHTML)
94 | })
95 |
96 | test('should work with component as a object of React Components', () => {
97 | const text = 'test components as a object.'
98 | const expectedHTML = 'test components as a object.'
99 |
100 | const Component = ({ children }) => {children}
101 |
102 | const { container } = render(
103 | }}
106 | />
107 | )
108 | expect(container.innerHTML).toContain(expectedHTML)
109 | })
110 |
111 | test('should work with component as a object without replacing the HTMLElement if the key is incorrect', () => {
112 | const text = 'test components as a object.'
113 | const expectedHTML =
114 | 'test components as a object.'
115 |
116 | const Component = ({ children }) => {children}
117 |
118 | const { container } = render(
119 | , u: }}
122 | />
123 | )
124 | expect(container.innerHTML).toContain(expectedHTML)
125 | })
126 | })
127 | })
128 |
--------------------------------------------------------------------------------
/__tests__/formatElements.test.js:
--------------------------------------------------------------------------------
1 | import { tagParsingRegex } from '../src/formatElements'
2 |
3 | describe('formatElements', () => {
4 | describe('tagParsingRegex', () => {
5 | it('should match tags in text', () => {
6 | const match = 'foobar
baz'.match(tagParsingRegex)
7 | expect(match[0]).toBe('bar
')
8 | expect(match[1]).toBe('p')
9 | expect(match[2]).toBe('bar')
10 | expect(match[3]).toBe(undefined)
11 | })
12 | it('should match empty tags', () => {
13 | const match = 'foobaz'.match(tagParsingRegex)
14 | expect(match[0]).toBe('')
15 | expect(match[1]).toBe('p')
16 | expect(match[2]).toBe('')
17 | expect(match[3]).toBe(undefined)
18 | })
19 | it('should match self closing tags without spaces', () => {
20 | const match = 'foobaz'.match(tagParsingRegex)
21 | expect(match[0]).toBe('')
22 | expect(match[1]).toBe(undefined)
23 | expect(match[2]).toBe(undefined)
24 | expect(match[3]).toBe('p')
25 | })
26 | it('should match self closing tags with spaces', () => {
27 | const match = 'foobaz'.match(tagParsingRegex)
28 | expect(match[0]).toBe('')
29 | expect(match[1]).toBe(undefined)
30 | expect(match[2]).toBe(undefined)
31 | expect(match[3]).toBe('p')
32 | })
33 | it('should match first occurrence of a tag when input has several', () => {
34 | const match = 'foobarbaz'.match(tagParsingRegex)
35 | expect(match[0]).toBe('bar')
36 | expect(match[1]).toBe('a')
37 | expect(match[2]).toBe('bar')
38 | expect(match[3]).toBe(undefined)
39 | })
40 | it('should match first occurrence of a tag when they are nested', () => {
41 | const match = 'foobarbazfoobarqux'.match(tagParsingRegex)
42 | expect(match[0]).toBe('barbazfoobar')
43 | expect(match[1]).toBe('a')
44 | expect(match[2]).toBe('barbazfoobar')
45 | expect(match[3]).toBe(undefined)
46 | })
47 | it('should tolerate spaces in regular tags too', () => {
48 | const match = 'foobarbaz'.match(tagParsingRegex)
49 | expect(match[0]).toBe('bar')
50 | expect(match[1]).toBe('a')
51 | expect(match[2]).toBe('bar')
52 | expect(match[3]).toBe(undefined)
53 | })
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/__tests__/getPageNamespaces.test.js:
--------------------------------------------------------------------------------
1 | import getPageNamespaces from '../src/getPageNamespaces'
2 |
3 | describe('getPageNamespaces', () => {
4 | let ctx
5 | beforeAll(() => {
6 | ctx = { query: {} }
7 | })
8 |
9 | describe('empty', () => {
10 | test('should not return any namespace with empty pages', async () => {
11 | const input = [{ pages: {} }, '/test-page', ctx]
12 | const output = await getPageNamespaces(...input)
13 |
14 | expect(output.length).toBe(0)
15 | })
16 | test('should not return any namespace with pages as undefined', async () => {
17 | const input = [{}, '/test-page', ctx]
18 | const output = await getPageNamespaces(...input)
19 |
20 | expect(output.length).toBe(0)
21 | })
22 | })
23 |
24 | describe('regular expressions', () => {
25 | test('should return namespaces that match the rgx', async () => {
26 | const config = {
27 | pages: {
28 | '*': ['common'],
29 | '/example/form': ['valid'],
30 | '/example/form/other': ['invalid'],
31 | 'rgx:/form$': ['form'],
32 | 'rgx:/invalid$': ['invalid'],
33 | 'rgx:^/example': ['example'],
34 | },
35 | }
36 | const input = [config, '/example/form']
37 | const output = await getPageNamespaces(...input)
38 |
39 | expect(output.length).toBe(4)
40 | expect(output[0]).toBe('common')
41 | expect(output[1]).toBe('valid')
42 | expect(output[2]).toBe('form')
43 | expect(output[3]).toBe('example')
44 | })
45 | })
46 |
47 | describe('as array', () => {
48 | test('should return the page namespace', async () => {
49 | const input = [
50 | { pages: { '/test-page': ['test-ns'] } },
51 | '/test-page',
52 | ctx,
53 | ]
54 | const output = await getPageNamespaces(...input)
55 | const expected = ['test-ns']
56 |
57 | expect(output.length).toBe(1)
58 | expect(output[0]).toBe(expected[0])
59 | })
60 |
61 | test('should return the page namespace + common', async () => {
62 | const input = [
63 | {
64 | pages: {
65 | '*': ['common'],
66 | '/test-page': ['test-ns'],
67 | },
68 | },
69 | '/test-page',
70 | ctx,
71 | ]
72 | const output = await getPageNamespaces(...input)
73 | const expected = ['common', 'test-ns']
74 |
75 | expect(output.length).toBe(2)
76 | expect(output[0]).toBe(expected[0])
77 | expect(output[1]).toBe(expected[1])
78 | })
79 | })
80 |
81 | describe('as function', () => {
82 | test('should work as a fn', async () => {
83 | ctx.query.example = '1'
84 | const input = [
85 | {
86 | pages: {
87 | '/test-page': ({ query }) => (query.example ? ['test-ns'] : []),
88 | },
89 | },
90 | '/test-page',
91 | ctx,
92 | ]
93 | const output = await getPageNamespaces(...input)
94 | const expected = ['test-ns']
95 |
96 | expect(output.length).toBe(1)
97 | expect(output[0]).toBe(expected[0])
98 | })
99 |
100 | test('should work as an async fn', async () => {
101 | ctx.query.example = '1'
102 | const input = [
103 | {
104 | pages: {
105 | '*': () => ['common'],
106 | '/test-page': async ({ query }) =>
107 | query.example ? ['test-ns'] : [],
108 | },
109 | },
110 | '/test-page',
111 | ctx,
112 | ]
113 | const output = await getPageNamespaces(...input)
114 | const expected = ['common', 'test-ns']
115 |
116 | expect(output.length).toBe(2)
117 | expect(output[0]).toBe(expected[0])
118 | expect(output[1]).toBe(expected[1])
119 | })
120 | })
121 | })
122 |
--------------------------------------------------------------------------------
/__tests__/getT.test.js:
--------------------------------------------------------------------------------
1 | import getT from '../src/getT'
2 |
3 | const mockLoadLocaleFrom = jest.fn()
4 |
5 | global.i18nConfig = {
6 | keySeparator: false,
7 | loadLocaleFrom: (...args) => mockLoadLocaleFrom(...args),
8 | }
9 |
10 | describe('getT', () => {
11 | beforeEach(() => {
12 | globalThis.__NEXT_TRANSLATE__ = {}
13 | mockLoadLocaleFrom.mockImplementation((__lang, ns) => {
14 | if (ns === 'ns1') {
15 | return Promise.resolve({
16 | key_ns1: 'message from ns1',
17 | })
18 | }
19 | if (ns === 'ns2') {
20 | return Promise.resolve({
21 | key_ns2: 'message from ns2',
22 | })
23 | }
24 | })
25 | })
26 | test('should load one namespace and translate + warning', async () => {
27 | console.warn = jest.fn()
28 | const t = await getT('en', 'ns1')
29 | const expectedWarning =
30 | '[next-translate] "ns2:key_ns2" is missing in current namespace configuration. Try adding "key_ns2" to the namespace "ns2".'
31 |
32 | expect(typeof t).toBe('function')
33 | expect(t('ns1:key_ns1')).toEqual('message from ns1')
34 | expect(t('ns2:key_ns2')).toEqual('ns2:key_ns2')
35 | expect(console.warn).toBeCalledWith(expectedWarning)
36 | })
37 |
38 | test('should work with flat keys', async () => {
39 | mockLoadLocaleFrom.mockImplementationOnce(async (__lang, ns) => ({
40 | 'this.is.a.flat.key': 'works',
41 | }))
42 | const t = await getT('en', 'common')
43 | expect(t('this.is.a.flat.key')).toEqual('works')
44 | })
45 |
46 | test('should work inside appDir', async () => {
47 | const mockAppDirLoadLocaleFrom = jest.fn()
48 | globalThis.__NEXT_TRANSLATE__ = {
49 | config: {
50 | keySeparator: false,
51 | loadLocaleFrom: (...args) => mockAppDirLoadLocaleFrom(...args),
52 | },
53 | }
54 | mockAppDirLoadLocaleFrom.mockImplementationOnce(async (__lang, ns) => ({
55 | 'example-app-dir': 'works',
56 | }))
57 | const t = await getT('en', 'common')
58 | expect(t('example-app-dir')).toEqual('works')
59 | })
60 |
61 | test('should load multiple namespaces and translate', async () => {
62 | const t = await getT('en', ['ns1', 'ns2'])
63 | expect(typeof t).toBe('function')
64 |
65 | expect(t('ns1:key_ns1')).toEqual('message from ns1')
66 | expect(t('ns2:key_ns2')).toEqual('message from ns2')
67 | })
68 |
69 | test('should use the only namespace as default', async () => {
70 | const t = await getT('en', 'ns1')
71 | expect(typeof t).toBe('function')
72 |
73 | expect(t('key_ns1')).toEqual('message from ns1')
74 | })
75 |
76 | test('should use the first namespace as default', async () => {
77 | console.warn = jest.fn()
78 | const t = await getT('en', ['ns2', 'ns1'])
79 | const expectedWarning =
80 | '[next-translate] "ns2:key_ns1" is missing in current namespace configuration. Try adding "key_ns1" to the namespace "ns2".'
81 |
82 | expect(typeof t).toBe('function')
83 | expect(t('key_ns2')).toEqual('message from ns2')
84 | expect(t('key_ns1')).toEqual('key_ns1')
85 | expect(t('ns1:key_ns1')).toEqual('message from ns1')
86 | expect(console.warn).toBeCalledWith(expectedWarning)
87 | })
88 | })
89 |
--------------------------------------------------------------------------------
/__tests__/loadNamespaces.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import loadNamespaces from '../src/loadNamespaces'
3 |
4 | describe('loadNamespaces', () => {
5 | test('should load a namespace', async () => {
6 | const result = await loadNamespaces({
7 | logBuild: false,
8 | loader: false,
9 | locale: 'en',
10 | pages: { '*': ['common'] },
11 | pathname: './index.js',
12 | loadLocaleFrom: (__lang, ns) =>
13 | Promise.resolve({ test: 'This is a Test' }),
14 | })
15 |
16 | expect(result).toEqual({
17 | __lang: 'en',
18 | __namespaces: { common: { test: 'This is a Test' } },
19 | })
20 | })
21 |
22 | test('should load existing namespaces if one failed', async () => {
23 | const result = await loadNamespaces({
24 | logBuild: false,
25 | loader: false,
26 | locale: 'en',
27 | pages: { '*': ['common', 'demo'] },
28 | pathname: './index.js',
29 | loadLocaleFrom: (__lang, ns) => {
30 | if (ns === 'demo') {
31 | return Promise.reject()
32 | } else {
33 | return Promise.resolve({ test: 'This is a Test' })
34 | }
35 | },
36 | })
37 |
38 | expect(result).toEqual({
39 | __lang: 'en',
40 | __namespaces: { common: { test: 'This is a Test' }, demo: {} },
41 | })
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/__tests__/transCore.test.js:
--------------------------------------------------------------------------------
1 | import transCore from '../src/transCore'
2 |
3 | const nsNestedKeys = {
4 | key_1: {
5 | key_1_nested: 'message 1 nested',
6 | key_2_nested: 'message 2 nested',
7 | },
8 | key_2: 'message 2',
9 | }
10 |
11 | const nsRootKeys = {
12 | root_key_1: 'root message 1',
13 | root_key_2: 'root message 2',
14 | }
15 |
16 | const nsInterpolate = {
17 | key_1: {
18 | key_1_nested: 'message 1 {{count}}',
19 | key_2_nested: 'message 2 {{count}}',
20 | },
21 | key_2: 'message 2',
22 | }
23 |
24 | const nsWithEmpty = {
25 | emptyKey: '',
26 | }
27 |
28 | describe('transCore', () => {
29 | test('should return an object of root keys', async () => {
30 | const t = transCore({
31 | config: {},
32 | allNamespaces: { nsRootKeys },
33 | lang: 'en',
34 | })
35 |
36 | expect(typeof t).toBe('function')
37 | expect(t('nsRootKeys:.', null, { returnObjects: true })).toEqual(nsRootKeys)
38 | })
39 |
40 | test('should return an object of root keys for the defaultNs', async () => {
41 | const t = transCore({
42 | config: {
43 | defaultNS: 'nsRootKeys',
44 | },
45 | allNamespaces: { nsRootKeys },
46 | lang: 'en',
47 | })
48 |
49 | expect(typeof t).toBe('function')
50 | expect(t('.', null, { returnObjects: true })).toEqual(nsRootKeys)
51 | })
52 |
53 | test('should return an object of root keys when using the keySeparator', async () => {
54 | const keySeparator = '___'
55 | const t = transCore({
56 | config: {
57 | keySeparator,
58 | },
59 | allNamespaces: { nsRootKeys },
60 | lang: 'en',
61 | })
62 |
63 | expect(typeof t).toBe('function')
64 | expect(
65 | t(`nsRootKeys:${keySeparator}`, null, { returnObjects: true })
66 | ).toEqual(nsRootKeys)
67 | })
68 |
69 | test('should allow flat keys when keySeparator=false', async () => {
70 | const t = transCore({
71 | config: {
72 | keySeparator: false,
73 | },
74 | allNamespaces: { common: { 'example.flat.key': 'works' } },
75 | lang: 'en',
76 | })
77 |
78 | expect(t('common:example.flat.key')).toEqual('works')
79 | })
80 |
81 | test('should return an object of nested keys', async () => {
82 | const t = transCore({
83 | config: {},
84 | allNamespaces: { nsObject: nsNestedKeys },
85 | lang: 'en',
86 | })
87 |
88 | expect(typeof t).toBe('function')
89 | expect(t('nsObject:key_1', null, { returnObjects: true })).toEqual(
90 | nsNestedKeys.key_1
91 | )
92 | expect(t('nsObject:key_2', null, { returnObjects: true })).toEqual(
93 | nsNestedKeys.key_2
94 | )
95 | })
96 |
97 | test('should return an object of nested keys and interpolate correctly', async () => {
98 | const t = transCore({
99 | config: {},
100 | allNamespaces: { nsInterpolate },
101 | pluralRules: { select() {}, resolvedOptions() {} },
102 | lang: 'en',
103 | })
104 |
105 | const count = 999
106 | const expected = {
107 | key_1: {
108 | key_1_nested: `message 1 ${count}`,
109 | key_2_nested: `message 2 ${count}`,
110 | },
111 | key_2: 'message 2',
112 | }
113 |
114 | expect(typeof t).toBe('function')
115 | expect(t('nsInterpolate:.', { count }, { returnObjects: true })).toEqual(
116 | expected
117 | )
118 | })
119 |
120 | test('should return empty string when allowEmptyStrings is passed as true.', () => {
121 | const t = transCore({
122 | config: {
123 | allowEmptyStrings: true,
124 | },
125 | allNamespaces: { nsWithEmpty },
126 | lang: 'en',
127 | })
128 |
129 | expect(typeof t).toBe('function')
130 | expect(t('nsWithEmpty:emptyKey')).toEqual('')
131 | })
132 |
133 | test('should return empty string when allowEmptyStrings is omitted.', () => {
134 | const t = transCore({
135 | allNamespaces: { nsWithEmpty },
136 | config: {},
137 | lang: 'en',
138 | })
139 |
140 | expect(typeof t).toBe('function')
141 | expect(t('nsWithEmpty:emptyKey')).toEqual('')
142 | })
143 |
144 | test('should return the key name when allowEmptyStrings is omit passed as false.', () => {
145 | const t = transCore({
146 | config: {
147 | allowEmptyStrings: false,
148 | },
149 | allNamespaces: { nsWithEmpty },
150 | lang: 'en',
151 | })
152 |
153 | expect(typeof t).toBe('function')
154 | expect(t('nsWithEmpty:emptyKey')).toEqual('nsWithEmpty:emptyKey')
155 | })
156 | })
157 |
--------------------------------------------------------------------------------
/__tests__/withTranslation.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, cleanup } from '@testing-library/react'
3 | import I18nProvider from '../src/I18nProvider'
4 | import withTranslation from '../src/withTranslation'
5 |
6 | class Translate extends React.Component {
7 | render() {
8 | const { i18nKey, query } = this.props
9 | const { t } = this.props.i18n
10 |
11 | return t(i18nKey, query)
12 | }
13 | }
14 |
15 | const Inner = withTranslation(Translate)
16 |
17 | const TestEnglish = ({ i18nKey, query, namespaces }) => {
18 | return (
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | const TestRussian = ({ i18nKey, query, namespaces }) => {
26 | return (
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | describe('withTranslation', () => {
34 | afterEach(cleanup)
35 |
36 | describe('getInitialProps', () => {
37 | test('should invoke getInitialProps of inner component', async () => {
38 | Translate.getInitialProps = jest.fn()
39 | const wrapperWithTranslation = withTranslation(Translate)
40 | await wrapperWithTranslation.getInitialProps()
41 | expect(Translate.getInitialProps).toBeCalled()
42 | })
43 | })
44 |
45 | describe('plurals', () => {
46 | test('should work with singular | count=1', () => {
47 | const i18nKey = 'ns:withsingular'
48 | const expected = 'The number is NOT ZERO'
49 | const withSingular = {
50 | withsingular: 'The number is NOT ZERO',
51 | withsingular_0: 'The number is ZERO!',
52 | withsingular_other: 'Oops!',
53 | }
54 | const { container } = render(
55 |
60 | )
61 | expect(container.textContent).toContain(expected)
62 | })
63 |
64 | test('should work with russian', () => {
65 | const i18nKey = 'ns:withrussian'
66 | const withRussian = {
67 | withrussian: 'The cart has only {{count}} product',
68 | withrussian_0: 'The cart is empty',
69 | withrussian_one: 'The cart number ends with one',
70 | withrussian_few: 'The card number is a number like 42',
71 | withrussian_many: 'The card number is a number like 100',
72 | withrussian_999: "The cart is full, you can't buy more products",
73 | withrussian_other: 'The cart has {{count}} products',
74 | }
75 | const r1 = render(
76 |
81 | )
82 | expect(r1.container.textContent).toContain(
83 | 'The cart number ends with one'
84 | )
85 |
86 | const r2 = render(
87 |
92 | )
93 | expect(r2.container.textContent).toContain(
94 | 'The card number is a number like 42'
95 | )
96 |
97 | const r3 = render(
98 |
103 | )
104 | expect(r3.container.textContent).toContain(
105 | 'The card number is a number like 100'
106 | )
107 |
108 | const r4 = render(
109 |
114 | )
115 | expect(r4.container.textContent).toContain(
116 | "The cart is full, you can't buy more products"
117 | )
118 |
119 | const r5 = render(
120 |
125 | )
126 | expect(r5.container.textContent).toContain(
127 | 'The cart number ends with one'
128 | )
129 |
130 | const r6 = render(
131 |
136 | )
137 | expect(r6.container.textContent).toContain('The cart is empty')
138 |
139 | const r7 = render(
140 |
145 | )
146 | expect(r7.container.textContent).toContain(
147 | 'The cart has Infinity products'
148 | )
149 | })
150 |
151 | test('should work with singular | count=0', () => {
152 | const i18nKey = 'ns:withsingular'
153 | const expected = 'The number is NOT ONE'
154 | const withSingular = {
155 | withsingular: 'The number is NOT ONE',
156 | withsingular_1: 'The number is ONE!',
157 | }
158 | const { container } = render(
159 |
164 | )
165 | expect(container.textContent).toContain(expected)
166 | })
167 |
168 | test('should work with _1 | count=1', () => {
169 | const i18nKey = 'ns:withsingular'
170 | const expected = 'The number is ONE!'
171 | const with_1 = {
172 | withsingular: 'The number is NOT ONE',
173 | withsingular_1: 'The number is ONE!',
174 | withsingular_other: 'Oops!',
175 | }
176 | const { container } = render(
177 |
182 | )
183 | expect(container.textContent).toContain(expected)
184 | })
185 |
186 | test('should work with _0 | count=0', () => {
187 | const i18nKey = 'ns:withsingular'
188 | const expected = 'The number is ZERO!'
189 | const with_0 = {
190 | withsingular: 'The number is NOT ZERO',
191 | withsingular_0: 'The number is ZERO!',
192 | withsingular_other: 'Oops!',
193 | }
194 | const { container } = render(
195 |
200 | )
201 | expect(container.textContent).toContain(expected)
202 | })
203 |
204 | test('should work with plural | count=2', () => {
205 | const i18nKey = 'ns:withplural'
206 | const expected = 'Number is bigger than one!'
207 | const withPlural = {
208 | withplural: 'Singular',
209 | withplural_1: 'The number is ONE!',
210 | withplural_other: 'Number is bigger than one!',
211 | }
212 | const { container } = render(
213 |
218 | )
219 | expect(container.textContent).toContain(expected)
220 | })
221 |
222 | test('should work with _2 | count=2', () => {
223 | const i18nKey = 'ns:withplural'
224 | const expected = 'The number is TWO!'
225 | const withPlural = {
226 | withplural: 'Singular',
227 | withplural_2: 'The number is TWO!',
228 | withplural_other: 'Number is bigger than one!',
229 | }
230 | const { container } = render(
231 |
236 | )
237 | expect(container.textContent).toContain(expected)
238 | })
239 | })
240 |
241 | test('should work with default namespace', () => {
242 | const i18nKey = 'simple'
243 | const namespace = {
244 | simple: 'This is working',
245 | }
246 |
247 | const Inner = withTranslation(Translate, 'ns')
248 |
249 | const { container } = render(
250 |
251 |
252 |
253 | )
254 | expect(container.textContent).toContain(namespace.simple)
255 | })
256 | })
257 |
--------------------------------------------------------------------------------
/build-packages.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 |
4 | const supportedExt = new Set(['js', 'ts', 'tsx'])
5 |
6 | function createPackageFromFile(file, prefix, subfolder) {
7 | const [name, ext] = file.split('.')
8 |
9 | if (!supportedExt.has(ext)) return
10 |
11 | fs.mkdirSync(`${subfolder}${name}`)
12 |
13 | const packageJSON = JSON.stringify(
14 | {
15 | name: name.toLowerCase(),
16 | private: true,
17 | main: `${prefix}lib/cjs/${subfolder}${name}.js`,
18 | module: `${prefix}lib/esm/${subfolder}${name}.js`,
19 | types: `${prefix}${subfolder}${name}.d.ts`,
20 | },
21 | undefined,
22 | 2
23 | )
24 |
25 | fs.writeFileSync(`${subfolder}${name}/package.json`, packageJSON)
26 | }
27 |
28 | function createPackagesFromFolder(folder, prefix, subfolder = '') {
29 | const files = fs.readdirSync(folder)
30 |
31 | files.forEach((file) => {
32 | createPackageFromFile(file, prefix, subfolder)
33 | })
34 | }
35 |
36 | createPackagesFromFolder(path.join(__dirname, 'src'), '../')
37 |
--------------------------------------------------------------------------------
/docs/hoist-non-react-statics.md:
--------------------------------------------------------------------------------
1 | # hoist-non-react-statics
2 |
3 | The HOCs we have in our API ([appWithI18n](../README.md#appwithi18n)), do not use [hoist-non-react-statics](https://github.com/mridgway/hoist-non-react-statics) in order not to include more kb than necessary _(static values different than getInitialProps in the pages are rarely used)_. If you have any conflict with statics, you can add hoist-non-react-statics (or any other alternative) in the configuration.
4 |
5 | **i18n.js**
6 |
7 | ```js
8 | const hoistNonReactStatics = require('hoist-non-react-statics')
9 |
10 | module.exports = {
11 | locales: ['en', 'ca', 'es'],
12 | defaultLocale: 'en',
13 | // you need to add:
14 | staticsHoc: hoistNonReactStatics,
15 | // ... rest of conf
16 | }
17 | ```
18 |
--------------------------------------------------------------------------------
/docs/migration-guide-1.0.0.md:
--------------------------------------------------------------------------------
1 | # Migration Guide `0.x` to `1.0.0`
2 |
3 | This migration guide describes how to upgrade existing projects using `next-translate@0.x` to `next-translate@1.0.0`.
4 |
5 | - [About 1.0](https://dev-blog.aralroca.com/next-translate-1.0)
6 | - [Release notes about 1.0](https://github.com/aralroca/next-translate/releases/tag/1.0.0)
7 | - [Demo video with 1.0](https://www.youtube.com/watch?v=QnCIjjYLCfc)
8 | - [Examples with 1.0](https://github.com/aralroca/next-translate/tree/1.0.0/examples)
9 |
10 | **If you are using a version prior to `0.19`, you should first [follow these steps](https://github.com/aralroca/next-translate/releases/tag/0.19.0) to migrate to `0.19` because it has some breaking changes.**
11 |
12 | This guide is useful both if you used the **"build step"** and if you used a **`getInitialProps` on top of `_app.js`** with the `appWithI18n`, since both have been unified under the webpack loader.
13 |
14 | ## Steps to follow
15 |
16 | 1. Update `next-translate` to `^1.0.0` using your preferred package manager.
17 |
18 | ```bash
19 | yarn add next-translate@^1.0.0
20 | ```
21 |
22 | ```bash
23 | npm install next-translate@^1.0.0
24 | ```
25 |
26 | 2. Update `next.config.js`:
27 |
28 | ```diff
29 | -const { locales, defaultLocale } = require("./i18n.json");
30 | +const nextTranslate = require("next-translate");
31 |
32 | -module.exports = {
33 | +module.exports = nextTranslate()
34 | - i18n: { locales, defaultLocale },
35 | -};
36 | ```
37 |
38 | 3. Remove obsolete options from `i18n.json`:
39 |
40 | ```diff
41 | {
42 | "locales": ["de", "fr", "it"],
43 | "defaultLocale": "de",
44 | - "currentPagesDir": "pages_",
45 | - "finalPagesDir": "pages",
46 | - "localesPath": "locales",
47 | - "package": false,
48 | "pages": {
49 | "*": ["common"],
50 | "rgx:^/products": ["products"]
51 | }
52 | }
53 | ```
54 |
55 | If you were using `localesPath` to load the namespaces from another directory, you should change `i18n.json` to `i18n.js` and use:
56 |
57 | ```js
58 | module.exports = {
59 | // ...rest of config
60 | loadLocaleFrom: (lang, ns) =>
61 | import(`./locales/${lang}/${ns}.json`).then((m) => m.default),
62 | }
63 | ```
64 |
65 | 4. Remove the extra build step in `package.json` _(in the case you were using the build step)_:
66 |
67 | ```diff
68 | "scripts": {
69 | - "dev": "next-translate && next dev",
70 | - "build": "next-translate && next build",
71 | + "dev": "next dev",
72 | + "build": "next build",
73 | "start": "next start"
74 | }
75 | ```
76 |
77 | 5. Remove `/pages` from `.gitignore` _(in the case you were using the build step)_:
78 |
79 | ```diff
80 | # local env files
81 | .env.local
82 | .env.development.local
83 | .env.test.local
84 | .env.production.local
85 |
86 | -# i18n
87 | -/pages
88 | ```
89 |
90 | 6. Rename directory `pages_` to `pages` _(in the case you were using the build step)_:
91 |
92 | ```bash
93 | rm -rf pages
94 | mv pages_ pages
95 | ```
96 |
97 | 7. Remove the `appWithI18n` wrapper on `_app.js` _(if instead of the "build step" you were using the appWithI18n)_:
98 |
99 | ```diff
100 | import React from 'react'
101 | import type { AppProps } from 'next/app'
102 | -import appWithI18n from 'next-translate/appWithI18n'
103 | -import i18nConfig from '../i18n'
104 |
105 | import '../styles.css'
106 |
107 | function MyApp({ Component, pageProps }: AppProps) {
108 | return
109 | }
110 |
111 | -export default appWithI18n(MyApp, i18nConfig)
112 | +export default MyApp
113 | ```
114 |
115 | 8. Replace `_plural` suffixes to `_other`. We are now supporting 6 plural forms, more info [in the README](https://github.com/aralroca/next-translate/blob/1.0.0/README.md#5-plurals).
116 |
117 | ```diff
118 | {
119 | - "lorem-ipsum_plural": "The value is {{count}}"
120 | + "lorem-ipsum_other": "The value is {{count}}"
121 | }
122 | ```
123 |
124 | ## Optional changes
125 |
126 | The `dynamic` prop of `DynamicNamespaces` component now is optional. By default is using the `locales` root folder or whatever you specified in `loadLocaleFrom` in the configuration. However, you can still use it if by the dynamic namespaces you want to use another place to store the JSONs.
127 |
128 | ```diff
129 | import React from 'react'
130 | import Trans from 'next-translate/Trans'
131 | import DynamicNamespaces from 'next-translate/DynamicNamespaces'
132 |
133 | export default function ExampleWithDynamicNamespace() {
134 | return (
135 |
137 | - import(`../../locales/${lang}/${ns}.json`).then((m) => m.default)
138 | - }
139 | namespaces={['dynamic']}
140 | fallback="Loading..."
141 | >
142 |
143 |
144 | )
145 | }
146 | ```
147 |
--------------------------------------------------------------------------------
/docs/migration-guide-2.0.0.md:
--------------------------------------------------------------------------------
1 | # Migrating from next-translate v1 to v2
2 |
3 | In version 2 of the **next-translate** library, we have made a significant change to the way the Webpack plugin is implemented. This change improves the library by solving many of the issues reported by users and makes it more powerful. However, it also means that two of the library's rules have been broken - it is now slightly larger and has a dependency on a specific version of the TypeScript parser.
4 |
5 | To address this, we have split the library into two separate packages. The main package, **[next-translate](https://github.com/aralroca/next-translate)**, is used as before and handles all of the translation functionality. The new package, **[next-translate-plugin](https://github.com/aralroca/next-translate-plugin)**, is used specifically for the Webpack plugin and should only be used in the `next.config.js` file.
6 |
7 | It's worth noting that by splitting the library into two packages and only using the `next-translate-plugin` in the `next.config.js` file, we are able to ensure that the extra kilobytes added by the TypeScript parser are only loaded during the build process and not included in the final bundle that is served to the client. This helps to keep the size of the final bundle as small as possible and improves the performance of your application. Additionally, by listing the `next-translate-plugin` will be installed as devDependency to reduce the extra kilobytes in the pipeline.
8 |
9 | To migrate your project to use version 2, you will need to make the following changes:
10 |
11 | - Install the **next-translate-plugin** package as a devDependency using `yarn add -D next-translate-plugin` or `npm install --save-dev next-translate-plugin`
12 | - In your `next.config.js` file, replace `require('next-translate')` with `require('next-translate-plugin')`
13 |
14 | Example of `next.config.js`:
15 |
16 | ```js
17 | const nextTranslate = require('next-translate-plugin')
18 |
19 | module.exports = nextTranslate()
20 | ```
21 |
22 | After making these changes, your project should be fully migrated to the latest version of next-translate
23 |
--------------------------------------------------------------------------------
/docs/type-safety.md:
--------------------------------------------------------------------------------
1 | # Type safety consuming translations (TypeScript only)
2 |
3 | To enable type safety consuming translations, you have to add this to the `next-translate.d.ts` file specifying the name and location of the namespaces:
4 |
5 | ```ts
6 | import type { Paths, I18n, Translate } from 'next-translate'
7 |
8 | type Tail = T extends [unknown, ...infer Rest] ? Rest : never;
9 |
10 | export interface TranslationsKeys {
11 | // Example with "common" and "home" namespaces in "en" (the default language):
12 | common: Paths
13 | home: Paths
14 | // Specify here all the namespaces you have...
15 | }
16 |
17 | type TranslationNamespace = keyof TranslationsKeys;
18 |
19 | export interface TranslateFunction {
20 | (
21 | key: TranslationsKeys[Namespace],
22 | ...rest: Tail>
23 | ): string
24 | (template: TemplateStringsArray): string
25 | };
26 |
27 | export interface TypeSafeTranslate
28 | extends Omit {
29 | t: TranslateFunction
30 | }
31 |
32 | declare module 'next-translate/useTranslation' {
33 | export default function useTranslation<
34 | Namespace extends TranslationNamespace
35 | >(namespace: Namespace): TypeSafeTranslate
36 | }
37 |
38 | declare module 'next-translate/getT' {
39 | export default function getT<
40 | Namespace extends TranslationNamespace
41 | >(locale?: string, namespace: Namespace): Promise>
42 | }
43 | ```
44 |
45 | Then type safety should work:
46 |
47 |
48 |
49 |
50 |
51 | Reference:
52 |
--------------------------------------------------------------------------------
/examples/basic/README.md:
--------------------------------------------------------------------------------
1 | # Basic example
2 |
3 | 
4 |
--------------------------------------------------------------------------------
/examples/basic/components/header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Head from 'next/head'
3 | import Link from 'next/link'
4 | import Router from 'next/router'
5 | import useTranslation from 'next-translate/useTranslation'
6 |
7 | import styles from './header.module.css'
8 |
9 | export default function Header() {
10 | const { t, lang } = useTranslation('common')
11 | const title = t('title')
12 | const headTitle = `${title} (${lang.toUpperCase()})`
13 |
14 | function changeToEn() {
15 | Router.push('/', undefined, { locale: 'en' })
16 | }
17 |
18 | return (
19 | <>
20 |
21 | {headTitle}
22 |
23 |
24 |
25 | {title}
26 | {lang !== 'es' && (
27 |
28 | Español
29 |
30 | )}
31 | {lang !== 'ca' && (
32 |
33 | Català
34 |
35 | )}
36 | {lang !== 'en' && (
37 | <>
38 |
39 | English
40 |
41 |
42 | >
43 | )}
44 |
45 | >
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/examples/basic/components/header.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
--------------------------------------------------------------------------------
/examples/basic/components/no-functional-component.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import withTranslation from 'next-translate/withTranslation'
3 |
4 | class NoFunctionalComponent extends React.Component {
5 | render() {
6 | const { t, lang } = this.props.i18n
7 |
8 | return {t('more-examples:no-functional-example')}
9 | }
10 | }
11 |
12 | export default withTranslation(NoFunctionalComponent)
13 |
--------------------------------------------------------------------------------
/examples/basic/components/plural-example.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import useTranslation from 'next-translate/useTranslation'
3 |
4 | export default function PluralExample() {
5 | const [count, setCount] = useState(0)
6 | const { t } = useTranslation()
7 |
8 | useEffect(() => {
9 | const interval = setInterval(() => {
10 | setCount((v) => (v === 5 ? 0 : v + 1))
11 | }, 1000)
12 |
13 | return () => clearInterval(interval)
14 | }, [])
15 |
16 | return {t('more-examples:plural-example', { count })}
17 | }
18 |
--------------------------------------------------------------------------------
/examples/basic/i18n.json:
--------------------------------------------------------------------------------
1 | {
2 | "locales": ["en", "ca", "es"],
3 | "defaultLocale": "en",
4 | "pages": {
5 | "*": ["common"],
6 | "/404": ["error"],
7 | "/": ["home"],
8 | "/dashboard": ["home"],
9 | "rgx:^/more-examples": ["more-examples"]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/examples/basic/locales/ca/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Llibreria next-translate"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/basic/locales/ca/dynamic.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-of-dynamic-translation": "Sóc contingut carregat de forma dinàmica"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/basic/locales/ca/error.json:
--------------------------------------------------------------------------------
1 | {
2 | "404": "Pàgina no trobada"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/basic/locales/ca/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Aquest text està escrit en Català desde el diccionari de la pàgina d'inici",
3 | "more-examples": "Més exemples"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/basic/locales/ca/more-examples.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-with-variable": "Aquest és un exemple amb variable {{count}}",
3 | "plural-example": "És en singular perquè el valor és {{count}}",
4 | "plural-example_0": "És cero perquè el valor és {{count}}",
5 | "plural-example_2": "És dos perquè el valor és {{count}}",
6 | "plural-example_other": "És en plural perquè el valor és {{count}}",
7 | "example-with-html": "<0>Aquest és un exempre <1>utilitzant HTML1> dintre de la traducció0>",
8 | "no-functional-example": "Traducció feta des d'un component no funcional",
9 | "dynamic-namespaces-link": "Veure un exemple de càrrega dinàmica",
10 | "dynamic-route": "Ruta dinàmica",
11 | "go-to-home": "Anar a la pàgina principal",
12 | "nested-example": {
13 | "very-nested": {
14 | "nested": "Exemple anidat!"
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/examples/basic/locales/en/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "next-translate library"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/basic/locales/en/dynamic.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-of-dynamic-translation": "I'm a dynamic loaded content"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/basic/locales/en/error.json:
--------------------------------------------------------------------------------
1 | {
2 | "404": "Page not found"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/basic/locales/en/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "This text is wrote in English from the homepage dictionary",
3 | "more-examples": "More examples"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/basic/locales/en/more-examples.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-with-variable": "This is an example with variable {{count}}",
3 | "plural-example": "This is singular because the value is {{count}}",
4 | "plural-example_0": "Is zero because the value is {{count}}",
5 | "plural-example_2": "Is two because the value is {{count}}",
6 | "plural-example_other": "Is in plural because the value is {{count}}",
7 | "example-with-html": "<0>This is an example <1>using HTML1> inside the translation0>",
8 | "no-functional-example": "Translation done from a no-functional component",
9 | "dynamic-namespaces-link": "See an example of dynamic namespace",
10 | "dynamic-route": "Dynamic route",
11 | "go-to-home": "Go to the homepage",
12 | "nested-example": {
13 | "very-nested": {
14 | "nested": "Nested example!"
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/examples/basic/locales/es/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Librería next-translate"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/basic/locales/es/dynamic.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-of-dynamic-translation": "Soy contenido cargado de forma dinámica"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/basic/locales/es/error.json:
--------------------------------------------------------------------------------
1 | {
2 | "404": "Página no encontrada"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/basic/locales/es/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Este texto está escrito en Español desde el diccionario de la página de inicio",
3 | "more-examples": "Más ejemplos"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/basic/locales/es/more-examples.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-with-variable": "Este es un ejemplo con variable {{count}}",
3 | "plural-example": "Es en singular porque el valor es {{count}}",
4 | "plural-example_0": "Es cero porque el valor es {{count}}",
5 | "plural-example_2": "Es dos porque el valor es {{count}}",
6 | "plural-example_other": "Es en plural porque el valor es {{count}}",
7 | "example-with-html": "<0>Este es un ejemplo <1>usando HTML1> dentro de la traducción0>",
8 | "no-functional-example": "Traducción hecha desde un componente no funcional",
9 | "dynamic-namespaces-link": "Ver un ejemplo de carga dinámica",
10 | "dynamic-route": "Ruta dinamica",
11 | "go-to-home": "Ir a la página principal",
12 | "nested-example": {
13 | "very-nested": {
14 | "nested": "¡Ejemplo anidado!"
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/examples/basic/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/examples/basic/next.config.js:
--------------------------------------------------------------------------------
1 | const nextTranslate = require('next-translate-plugin')
2 |
3 | module.exports = nextTranslate()
4 |
--------------------------------------------------------------------------------
/examples/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "-",
3 | "version": "1.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "next dev",
8 | "build": "next build"
9 | },
10 | "dependencies": {
11 | "next": "14.1.0",
12 | "next-translate": "link:../../",
13 | "react": "link:../../node_modules/react",
14 | "react-dom": "link:../../node_modules/react-dom"
15 | },
16 | "devDependencies": {
17 | "next-translate-plugin": "2.6.2"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/basic/pages/404.js:
--------------------------------------------------------------------------------
1 | import useTranslation from 'next-translate/useTranslation'
2 |
3 | export default function Error404() {
4 | const { t, lang } = useTranslation()
5 | const errorMessage = t`error:404`
6 |
7 | console.log({ lang })
8 |
9 | return errorMessage
10 | }
11 |
--------------------------------------------------------------------------------
/examples/basic/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Link from 'next/link'
3 | import useTranslation from 'next-translate/useTranslation'
4 |
5 | import Header from '../components/header'
6 |
7 | export default function Home() {
8 | const { t } = useTranslation('home')
9 | const description = t('description')
10 | const linkName = t('more-examples')
11 |
12 | return (
13 | <>
14 |
15 | {description}
16 | {linkName}
17 | >
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/examples/basic/pages/more-examples/catchall/[...all].js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import useTranslation from 'next-translate/useTranslation'
3 | import { useRouter } from 'next/router'
4 |
5 | export default function All() {
6 | const { query } = useRouter()
7 | const { t, lang } = useTranslation()
8 |
9 | return (
10 | <>
11 | {JSON.stringify({ query, lang })}
12 |
13 | {t`more-examples:go-to-home`}
14 | >
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/examples/basic/pages/more-examples/dynamic-namespace.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Trans from 'next-translate/Trans'
3 | import DynamicNamespaces from 'next-translate/DynamicNamespaces'
4 |
5 | export default function ExampleWithDynamicNamespace() {
6 | return (
7 |
8 | {/* ALSO IS POSSIBLE TO USE NAMESPACES FROM THE PAGE */}
9 |
10 |
11 |
12 |
13 | {/* USING DYNAMIC NAMESPACE */}
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/examples/basic/pages/more-examples/dynamicroute/[slug].js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import useTranslation from 'next-translate/useTranslation'
3 | import getT from 'next-translate/getT'
4 | import { useRouter } from 'next/router'
5 |
6 | export default function DynamicRoute({ title }) {
7 | const { query } = useRouter()
8 | const { t, lang } = useTranslation()
9 |
10 | console.log({ query })
11 |
12 | return (
13 | <>
14 | {title}
15 | {t`more-examples:dynamic-route`}
16 |
17 | {query.slug} - {lang}
18 |
19 | {t`more-examples:go-to-home`}
20 | >
21 | )
22 | }
23 |
24 | export async function getServerSideProps({ locale }) {
25 | const t = await getT(locale, 'common')
26 | return { props: { title: t('title') } }
27 | }
28 |
--------------------------------------------------------------------------------
/examples/basic/pages/more-examples/index.js:
--------------------------------------------------------------------------------
1 | import useTranslation from 'next-translate/useTranslation'
2 | import Trans from 'next-translate/Trans'
3 | import Link from 'next/link'
4 |
5 | import PluralExample from '../../components/plural-example'
6 | import Header from '../../components/header'
7 | import NoFunctionalComponent from '../../components/no-functional-component'
8 |
9 | const Component = (props) =>
10 |
11 | export default function MoreExamples() {
12 | const { t } = useTranslation()
13 | const exampleWithVariable = t('more-examples:example-with-variable', {
14 | count: 42,
15 | })
16 |
17 | return (
18 | <>
19 |
20 | {exampleWithVariable}
21 |
22 | , ]}
25 | />
26 |
27 |
28 | {t`more-examples:nested-example.very-nested.nested`}
29 |
30 |
31 | {t('more-examples:dynamic-namespaces-link')}
32 |
33 |
34 |
40 | {t('more-examples:dynamic-route')}
41 |
42 |
43 | Catchall
44 | >
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/examples/basic/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aralroca/next-translate/c47f226ffd0c0f2600595ae7620f8708d6f70a05/examples/basic/public/favicon.ico
--------------------------------------------------------------------------------
/examples/basic/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": false,
7 | "forceConsistentCasingInFileNames": true,
8 | "noEmit": true,
9 | "incremental": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve"
16 | },
17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "pages/**/*.js"],
18 | "exclude": ["node_modules"]
19 | }
20 |
--------------------------------------------------------------------------------
/examples/complex/README.md:
--------------------------------------------------------------------------------
1 | # Complex example
2 |
3 | Similar than the basic example but with some extras:
4 |
5 | - TypeScript
6 | - Webpack 5
7 | - MDX
8 | - With _app.js on top
9 | - Custom interpolation with ${thisFormat} instead of the {{defaultFormat}}
10 | - pages located on src/pages folder
11 | - Loading locales from src/translations with a different structure
12 |
13 | 
14 |
--------------------------------------------------------------------------------
/examples/complex/i18n.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | locales: ['__default', 'en', 'ca', 'es'],
3 | defaultLocale: '__default',
4 | localesToIgnore: ['__default'],
5 | pages: {
6 | '*': ['common'],
7 | '/404': ['error'],
8 | '/': ['home'],
9 | '/dashboard': ['home'],
10 | 'rgx:^/more-examples': ['more-examples'],
11 | },
12 | interpolation: {
13 | prefix: '${',
14 | suffix: '}',
15 | },
16 | loadLocaleFrom: async (locale, namespace) =>
17 | import(`./src/translations/${namespace}_${locale}`).then((r) => r.default),
18 | }
19 |
--------------------------------------------------------------------------------
/examples/complex/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/examples/complex/next.config.js:
--------------------------------------------------------------------------------
1 | const nextTranslate = require('next-translate-plugin')
2 | const withMDX = require('@next/mdx')()
3 | const withBundleAnalyzer = require('@next/bundle-analyzer')({
4 | enabled: process.env.ANALYZE === 'true',
5 | })
6 |
7 | console.log('Webpack version', require('webpack').version)
8 |
9 | module.exports = nextTranslate(withBundleAnalyzer(withMDX()))
10 |
--------------------------------------------------------------------------------
/examples/complex/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "-",
3 | "version": "1.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "next dev",
8 | "build": "next build",
9 | "analyze": "ANALYZE=true yarn build"
10 | },
11 | "dependencies": {
12 | "@mdx-js/loader": "3.0.0",
13 | "@mdx-js/react": "3.0.0",
14 | "@next/mdx": "14.1.0",
15 | "next": "14.1.0",
16 | "next-translate": "link:../../",
17 | "react": "link:../../node_modules/react",
18 | "react-dom": "link:../../node_modules/react-dom"
19 | },
20 | "devDependencies": {
21 | "@next/bundle-analyzer": "14.1.0",
22 | "@types/node": "20.11.5",
23 | "@types/react": "18.2.48",
24 | "next-translate-plugin": "2.6.2",
25 | "typescript": "5.3.3"
26 | },
27 | "resolutions": {
28 | "webpack": "5.11.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/examples/complex/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aralroca/next-translate/c47f226ffd0c0f2600595ae7620f8708d6f70a05/examples/complex/public/favicon.ico
--------------------------------------------------------------------------------
/examples/complex/src/_middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server'
2 |
3 | const PUBLIC_FILE = /\.(.*)$/
4 |
5 | export function middleware(request: NextRequest) {
6 | const shouldHandleLocale =
7 | !PUBLIC_FILE.test(request.nextUrl.pathname) &&
8 | !request.nextUrl.pathname.includes('/api/') &&
9 | request.nextUrl.locale === '__default'
10 |
11 | return shouldHandleLocale
12 | ? NextResponse.redirect(`/es${request.nextUrl.href}`)
13 | : undefined
14 | }
15 |
--------------------------------------------------------------------------------
/examples/complex/src/components/header.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
--------------------------------------------------------------------------------
/examples/complex/src/components/header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Head from 'next/head'
3 | import Link from 'next/link'
4 | import Router from 'next/router'
5 | import useTranslation from 'next-translate/useTranslation'
6 |
7 | import styles from './header.module.css'
8 |
9 | export default function Header() {
10 | const { t, lang } = useTranslation()
11 | const title = t('common:title')
12 | const headTitle = `${title} (${lang.toUpperCase()})`
13 |
14 | function changeToEn() {
15 | Router.push('/', undefined, { locale: 'en' })
16 | }
17 |
18 | return (
19 | <>
20 |
21 | {headTitle}
22 |
23 |
24 |
25 | {title}
26 | {lang !== 'es' && (
27 |
28 | Español
29 |
30 | )}
31 | {lang !== 'ca' && (
32 |
33 | Català
34 |
35 | )}
36 | {lang !== 'en' && (
37 | <>
38 |
39 | English
40 |
41 |
42 | >
43 | )}
44 |
45 | >
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/examples/complex/src/components/mdx-example.mdx:
--------------------------------------------------------------------------------
1 | import Trans from 'next-translate/Trans'
2 |
3 | ### ✋
4 |
--------------------------------------------------------------------------------
/examples/complex/src/components/no-functional-component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import withTranslation from 'next-translate/withTranslation'
3 | import type { I18n } from 'next-translate'
4 |
5 | interface Props {
6 | i18n: I18n
7 | }
8 |
9 | class NoFunctionalComponent extends React.Component {
10 | render() {
11 | const { t, lang } = this.props.i18n
12 |
13 | return {t('more-examples:no-functional-example')}
14 | }
15 | }
16 |
17 | export default withTranslation(NoFunctionalComponent)
18 |
--------------------------------------------------------------------------------
/examples/complex/src/components/plural-example.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import useTranslation from 'next-translate/useTranslation'
3 |
4 | export default function PluralExample() {
5 | const [count, setCount] = useState(0)
6 | const { t } = useTranslation()
7 |
8 | useEffect(() => {
9 | const interval = setInterval(() => {
10 | setCount((v) => (v === 5 ? 0 : v + 1))
11 | }, 1000)
12 |
13 | return () => clearInterval(interval)
14 | }, [])
15 |
16 | return {t('more-examples:plural-example', { count })}
17 | }
18 |
--------------------------------------------------------------------------------
/examples/complex/src/mdx.d.ts:
--------------------------------------------------------------------------------
1 | // types/mdx.d.ts
2 | declare module '*.mdx' {
3 | let MDXComponent: (props) => JSX.Element;
4 | export default MDXComponent;
5 | }
6 |
--------------------------------------------------------------------------------
/examples/complex/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import useTranslation from 'next-translate/useTranslation'
2 |
3 | export default function Error404() {
4 | const { t, lang } = useTranslation()
5 |
6 | console.log({ lang })
7 |
8 | return t`error:404`
9 | }
10 |
--------------------------------------------------------------------------------
/examples/complex/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import NextApp from 'next/app'
3 |
4 | import '../styles.css'
5 |
6 | class MyApp extends NextApp {
7 | render() {
8 | const { Component, pageProps } = this.props
9 | return
10 | }
11 | }
12 |
13 | export default MyApp
14 |
--------------------------------------------------------------------------------
/examples/complex/src/pages/amp.tsx:
--------------------------------------------------------------------------------
1 | export const config = { amp: true }
2 |
3 | // @ts-ignore
4 | function Amp() {
5 | return My AMP Page!
6 | }
7 |
8 | export default Amp
9 |
--------------------------------------------------------------------------------
/examples/complex/src/pages/api/example.tsx:
--------------------------------------------------------------------------------
1 | import getT from 'next-translate/getT'
2 |
3 | // @ts-ignore
4 | export default async function handler(req, res) {
5 | const t = await getT(req.query.__nextLocale, 'common')
6 | const title = t('title')
7 |
8 | res.statusCode = 200
9 | res.setHeader('Content-Type', 'application/json')
10 | res.end(JSON.stringify({ title }))
11 | }
12 |
--------------------------------------------------------------------------------
/examples/complex/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Link from 'next/link'
3 | import useTranslation from 'next-translate/useTranslation'
4 | import Header from '../components/header'
5 |
6 | export default function Home() {
7 | const { t } = useTranslation('home')
8 | const description = t('description')
9 | const linkName = t('more-examples')
10 |
11 | return (
12 | <>
13 |
14 | {description}
15 | {linkName}
16 | >
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/examples/complex/src/pages/more-examples/catchall/[...all].tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import useTranslation from 'next-translate/useTranslation'
3 | import { useRouter } from 'next/router'
4 |
5 | export default function All() {
6 | const { query } = useRouter()
7 | const { t, lang } = useTranslation()
8 |
9 | return (
10 | <>
11 | {JSON.stringify({ query, lang })}
12 |
13 | {t`more-examples:go-to-home`}
14 | >
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/examples/complex/src/pages/more-examples/dynamic-namespace.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Trans from 'next-translate/Trans'
3 | import DynamicNamespaces from 'next-translate/DynamicNamespaces'
4 |
5 | export default function ExampleWithDynamicNamespace() {
6 | return (
7 |
8 | {/* ALSO IS POSSIBLE TO USE NAMESPACES FROM THE PAGE */}
9 |
10 |
11 |
12 |
13 | {/* USING DYNAMIC NAMESPACE */}
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/examples/complex/src/pages/more-examples/dynamicroute/[slug].tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import getT from 'next-translate/getT'
3 | import useTranslation from 'next-translate/useTranslation'
4 | import { useRouter } from 'next/router'
5 | import { GetStaticProps } from 'next'
6 |
7 | export default function DynamicRoute({ title = '' }) {
8 | const { query } = useRouter()
9 | const { t, lang } = useTranslation()
10 |
11 | console.log({ query })
12 |
13 | return (
14 | <>
15 | {title}
16 | {t`more-examples:dynamic-route`}
17 |
18 | {query.slug} - {lang}
19 |
20 | {t`more-examples:go-to-home`}
21 | >
22 | )
23 | }
24 |
25 | export function getStaticPaths({ locales }: any) {
26 | return {
27 | paths: locales
28 | .filter((locale: string) => locale !== 'default')
29 | .map((locale: string) => ({
30 | locale,
31 | params: { slug: 'example' },
32 | })),
33 | fallback: true,
34 | }
35 | }
36 |
37 | export const getStaticProps: GetStaticProps = async ({ locale }) => {
38 | const t = await getT(locale, 'common')
39 | return { props: { title: t('title') } }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/complex/src/pages/more-examples/index.tsx:
--------------------------------------------------------------------------------
1 | import useTranslation from 'next-translate/useTranslation'
2 | import Trans from 'next-translate/Trans'
3 | import Link from 'next/link'
4 |
5 | import Header from '../../components/header'
6 | import MdxExample from '../../components/mdx-example.mdx'
7 | import NoFunctionalComponent from '../../components/no-functional-component'
8 | import PluralExample from '../../components/plural-example'
9 |
10 | const Component = (props: any) =>
11 |
12 | export default function MoreExamples() {
13 | const { t } = useTranslation('more-examples')
14 | const exampleWithVariable = t('example-with-variable', {
15 | exampleOfVariable: 42,
16 | })
17 |
18 | return (
19 | <>
20 |
21 | {exampleWithVariable}
22 |
23 | , ]}
26 | />
27 |
28 |
29 | {t`nested-example.very-nested.nested`}
30 |
31 |
32 |
33 |
34 | {t('dynamic-namespaces-link')}
35 |
36 |
37 |
43 | {t('dynamic-route')}
44 |
45 |
46 | Catchall
47 | >
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/examples/complex/src/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | font-size: 18px;
6 | font-weight: 400;
7 | line-height: 1.8;
8 | background-color: #fafafa;
9 | color: #212121;
10 | font-family: sans-serif;
11 | }
12 | h1 {
13 | font-weight: 700;
14 | }
15 | p {
16 | margin-bottom: 10px;
17 | }
18 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/common_ca.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Llibreria next-translate"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/common_en.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "next-translate library"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/common_es.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Librería next-translate"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/dynamic_ca.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-of-dynamic-translation": "Sóc contingut carregat de forma dinàmica"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/dynamic_en.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-of-dynamic-translation": "I'm a dynamic loaded content"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/dynamic_es.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-of-dynamic-translation": "Soy contenido cargado de forma dinámica"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/error_ca.json:
--------------------------------------------------------------------------------
1 | {
2 | "404": "Pàgina no trobada"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/error_en.json:
--------------------------------------------------------------------------------
1 | {
2 | "404": "Page not found"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/error_es.json:
--------------------------------------------------------------------------------
1 | {
2 | "404": "Página no encontrada"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/home_ca.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Aquest text està escrit en Català desde el diccionari de la pàgina d'inici",
3 | "more-examples": "Més exemples"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/home_en.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "This text is wrote in English from the homepage dictionary",
3 | "more-examples": "More examples"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/home_es.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Este texto está escrito en Español desde el diccionario de la página de inicio",
3 | "more-examples": "Más ejemplos"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/more-examples_ca.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-with-variable": "Aquest és un exemple amb variable ${exampleOfVariable}",
3 | "plural-example": {
4 | "one": "És en singular perquè el valor és ${count}",
5 | "0": "És cero perquè el valor és ${count}",
6 | "2": "És dos perquè el valor és ${count}",
7 | "other": "És en plural perquè el valor és ${count}"
8 | },
9 | "plural-example_other": "",
10 | "example-with-html": "<0>Aquest és un exempre <1>utilitzant HTML1> dintre de la traducció0>",
11 | "no-functional-example": "Traducció feta des d'un component no funcional",
12 | "dynamic-namespaces-link": "Veure un exemple de càrrega dinàmica",
13 | "dynamic-route": "Ruta dinàmica",
14 | "mdx-example": "Això és un exemple amb MDX",
15 | "go-to-home": "Anar a la pàgina principal",
16 | "nested-example": {
17 | "very-nested": {
18 | "nested": "Exemple anidat!"
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/more-examples_en.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-with-variable": "This is an example with variable ${exampleOfVariable}",
3 | "plural-example": {
4 | "one": "This is singular because the value is ${count}",
5 | "0": "Is zero because the value is ${count}",
6 | "2": "Is two because the value is ${count}",
7 | "other": "Is in plural because the value is ${count}"
8 | },
9 | "example-with-html": "<0>This is an example <1>using HTML1> inside the translation0>",
10 | "no-functional-example": "Translation done from a no-functional component",
11 | "dynamic-namespaces-link": "See an example of dynamic namespace",
12 | "dynamic-route": "Dynamic route",
13 | "mdx-example": "This is an example with MDX",
14 | "go-to-home": "Go to the homepage",
15 | "nested-example": {
16 | "very-nested": {
17 | "nested": "Nested example!"
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/examples/complex/src/translations/more-examples_es.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-with-variable": "Este es un ejemplo con variable ${exampleOfVariable}",
3 | "plural-example": {
4 | "one": "Es en singular porque el valor es ${count}",
5 | "0": "Es cero porque el valor es ${count}",
6 | "2": "Es dos porque el valor es ${count}",
7 | "other": "Es en plural porque el valor es ${count}"
8 | },
9 | "example-with-html": "<0>Este es un ejemplo <1>usando HTML1> dentro de la traducción0>",
10 | "no-functional-example": "Traducción hecha desde un componente no funcional",
11 | "dynamic-namespaces-link": "Ver un ejemplo de carga dinámica",
12 | "dynamic-route": "Ruta dinamica",
13 | "mdx-example": "Este es un ejemplo con MDX",
14 | "go-to-home": "Ir a la página principal",
15 | "nested-example": {
16 | "very-nested": {
17 | "nested": "¡Ejemplo anidado!"
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/examples/complex/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noImplicitAny": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true
18 | },
19 | "exclude": ["node_modules"],
20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
21 | "typeRoots": ["./src/mdx.d.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/examples/with-app-directory/README.md:
--------------------------------------------------------------------------------
1 | # App dir example
2 |
3 | Example loading and consuming translations with Next.js 13 app dir.
4 |
5 |
--------------------------------------------------------------------------------
/examples/with-app-directory/i18n.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | locales: ['en', 'ca', 'es'],
3 | defaultLocale: 'en',
4 | pages: {
5 | '*': ['common'],
6 | '/[lang]': ['home'],
7 | '/[lang]/second-page': ['home'],
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/examples/with-app-directory/locales/ca/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Llibreria next-translate",
3 | "second-page": "Segona <0>pàgina0>",
4 | "second-page-title": "Títol Segona Pàgina",
5 | "loading": "Carregant...",
6 | "layout-title": "Títol de la pàgina (del layout)"
7 | }
8 |
--------------------------------------------------------------------------------
/examples/with-app-directory/locales/ca/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "client-only": "Només al client"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/with-app-directory/locales/en/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "next-translate library",
3 | "second-page": "Second <0>page0>",
4 | "second-page-title": "Second Page Title",
5 | "loading": "Loading...",
6 | "layout-title": "Layout title"
7 | }
8 |
--------------------------------------------------------------------------------
/examples/with-app-directory/locales/en/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "client-only": "Only client-side"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/with-app-directory/locales/es/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Librería next-translate",
3 | "second-page": "Segunda <0>página0>",
4 | "second-page-title": "Título Segunda Página",
5 | "loading": "Cargando...",
6 | "layout-title": "Título de la página (del layout)"
7 | }
8 |
--------------------------------------------------------------------------------
/examples/with-app-directory/locales/es/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "client-only": "Sólo al lado del cliente"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/with-app-directory/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/examples/with-app-directory/next-translate.d.ts:
--------------------------------------------------------------------------------
1 | import type { Paths, I18n, Translate } from 'next-translate'
2 |
3 | type Tail = T extends [unknown, ...infer Rest] ? Rest : never;
4 |
5 | export interface TranslationsKeys {
6 | // Example with "common" and "home" namespaces in "en" (the default language):
7 | common: Paths
8 | home: Paths
9 | // Specify here all the namespaces you have...
10 | }
11 |
12 | type TranslationNamespace = keyof TranslationsKeys;
13 |
14 | export interface TranslateFunction {
15 | (
16 | key: TranslationsKeys[Namespace],
17 | ...rest: Tail>
18 | ): string
19 | (template: TemplateStringsArray): string
20 | };
21 |
22 | export interface TypeSafeTranslate
23 | extends Omit {
24 | t: TranslateFunction
25 | }
26 |
27 | declare module 'next-translate/useTranslation' {
28 | export default function useTranslation<
29 | Namespace extends TranslationNamespace
30 | >(namespace: Namespace): TypeSafeTranslate
31 | }
32 |
33 | declare module 'next-translate/getT' {
34 | export default function getT<
35 | Namespace extends TranslationNamespace
36 | >(locale?: string, namespace: Namespace): Promise>
37 | }
38 |
--------------------------------------------------------------------------------
/examples/with-app-directory/next.config.js:
--------------------------------------------------------------------------------
1 | const nextTranslate = require('next-translate-plugin')
2 |
3 | module.exports = nextTranslate()
4 |
--------------------------------------------------------------------------------
/examples/with-app-directory/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "-",
3 | "version": "1.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "next dev",
8 | "build": "next build",
9 | "start": "next start",
10 | "analyze": "ANALYZE=true yarn build"
11 | },
12 | "dependencies": {
13 | "@mdx-js/loader": "3.0.0",
14 | "@mdx-js/react": "3.0.0",
15 | "@next/mdx": "14.1.0",
16 | "next": "14.1.0",
17 | "next-translate": "link:../../",
18 | "react": "link:../../node_modules/react",
19 | "react-dom": "link:../../node_modules/react-dom"
20 | },
21 | "devDependencies": {
22 | "@next/bundle-analyzer": "14.1.0",
23 | "@types/node": "20.11.5",
24 | "@types/react": "18.2.48",
25 | "next-translate-plugin": "2.6.2",
26 | "typescript": "5.3.3"
27 | },
28 | "resolutions": {
29 | "webpack": "5.11.1"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/examples/with-app-directory/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aralroca/next-translate/c47f226ffd0c0f2600595ae7620f8708d6f70a05/examples/with-app-directory/public/favicon.ico
--------------------------------------------------------------------------------
/examples/with-app-directory/src/app/[lang]/layout.tsx:
--------------------------------------------------------------------------------
1 | import useTranslation from 'next-translate/useTranslation'
2 | import i18n from '../../../i18n'
3 | import { redirect } from 'next/navigation'
4 |
5 | export default function RootLayout({
6 | children,
7 | }: {
8 | children: React.ReactNode
9 | }) {
10 | const { t, lang } = useTranslation('common')
11 |
12 | // Redirect to default locale if lang is not supported. /second-page -> /en/second-page
13 | if (!i18n.locales.includes(lang)) redirect(`/${i18n.defaultLocale}/${lang}`)
14 |
15 | return (
16 |
17 |
18 |
19 | {t`layout-title`}
20 | {children}
21 |
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/examples/with-app-directory/src/app/[lang]/loading.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import useTranslation from 'next-translate/useTranslation'
4 |
5 | export default function Loading() {
6 | const { t } = useTranslation('common')
7 | return {t`loading`}
8 | }
9 |
--------------------------------------------------------------------------------
/examples/with-app-directory/src/app/[lang]/page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Link from 'next/link'
3 | import useTranslation from 'next-translate/useTranslation'
4 | import ClientCode from '../../components/client-code'
5 |
6 | export default async function Page() {
7 | await sleep(500) // simulate slow page load to show loading page
8 |
9 | const { t, lang } = useTranslation('common')
10 |
11 | return (
12 | <>
13 | {t('title')}
14 |
15 |
16 |
17 |
18 | English
19 |
20 |
21 |
22 | Español
23 |
24 |
25 |
26 | Català
27 |
28 |
29 |
30 | ➡️
31 |
32 | >
33 | )
34 | }
35 |
36 | function sleep(ms: number) {
37 | return new Promise((resolve) => setTimeout(resolve, ms))
38 | }
39 |
40 | export const metadata = {
41 | title: 'App directory',
42 | }
43 |
--------------------------------------------------------------------------------
/examples/with-app-directory/src/app/[lang]/second-page/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import useTranslation from 'next-translate/useTranslation'
3 | import Trans from 'next-translate/Trans'
4 | import { Metadata } from "next";
5 |
6 | export function generateMetadata(): Metadata {
7 | const { t } = useTranslation("common");
8 |
9 | return {
10 | title: t`second-page-title`
11 | };
12 | }
13 |
14 | export default function Page() {
15 | const { t, lang } = useTranslation('common')
16 | return (
17 | <>
18 | {t`title`}
19 | ]} />
20 |
21 | ⬅️
22 |
23 | >
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/examples/with-app-directory/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import useTranslation from 'next-translate/useTranslation'
2 | import i18n from '../../i18n'
3 | import { redirect } from 'next/navigation'
4 |
5 | export default function RootLayout({
6 | children,
7 | }: {
8 | children: React.ReactNode
9 | }) {
10 | const { lang } = useTranslation('common')
11 |
12 | // Redirect to default locale if lang is not supported. /second-page -> /en/second-page
13 | if (!i18n.locales.includes(lang)) redirect(`/${i18n.defaultLocale}/${lang}`)
14 |
15 | return (
16 |
17 |
18 | {children}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/examples/with-app-directory/src/components/client-code.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import useTranslation from 'next-translate/useTranslation'
3 | import { useState } from 'react'
4 |
5 | export default function ClientCode() {
6 | const [count, setCount] = useState(0)
7 | const { t } = useTranslation('home')
8 |
9 | return (
10 |
11 | {t('client-only')}:
12 |
13 | {count}
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/examples/with-app-directory/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noImplicitAny": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "plugins": [
19 | {
20 | "name": "next"
21 | }
22 | ]
23 | },
24 | "exclude": ["node_modules"],
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "typeRoots": ["./src/mdx.d.ts"]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/without-loader/README.md:
--------------------------------------------------------------------------------
1 | # Example without the webpack loader
2 |
3 | Similar than the basic example but loading the page namespaces manually deactivating the webpack loader in the `i18n.json` config file.
4 |
5 | > We do not recommend that it be used in this way. However we give the opportunity for anyone to do so if they are not comfortable with our webpack loader.
6 |
7 | 
8 |
--------------------------------------------------------------------------------
/examples/without-loader/components/header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Head from 'next/head'
3 | import Link from 'next/link'
4 | import Router from 'next/router'
5 | import useTranslation from 'next-translate/useTranslation'
6 |
7 | import styles from './header.module.css'
8 |
9 | export default function Header() {
10 | const { t, lang } = useTranslation()
11 | const title = t('common:title')
12 |
13 | function changeToEn() {
14 | Router.push('/', undefined, { locale: 'en' })
15 | }
16 |
17 | return (
18 | <>
19 |
20 |
21 | {title} | ({lang.toUpperCase()})
22 |
23 |
24 |
25 |
26 | {title}
27 | {lang !== 'es' && (
28 |
29 | Español
30 |
31 | )}
32 | {lang !== 'ca' && (
33 |
34 | Català
35 |
36 | )}
37 | {lang !== 'en' && (
38 | <>
39 |
40 | English
41 |
42 |
43 | >
44 | )}
45 |
46 | >
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/examples/without-loader/components/header.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
--------------------------------------------------------------------------------
/examples/without-loader/components/no-functional-component.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import withTranslation from 'next-translate/withTranslation'
3 |
4 | class NoFunctionalComponent extends React.Component {
5 | render() {
6 | const { t, lang } = this.props.i18n
7 |
8 | return {t('more-examples:no-functional-example')}
9 | }
10 | }
11 |
12 | export default withTranslation(NoFunctionalComponent)
13 |
--------------------------------------------------------------------------------
/examples/without-loader/components/plural-example.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import useTranslation from 'next-translate/useTranslation'
3 |
4 | export default function PluralExample() {
5 | const [count, setCount] = useState(0)
6 | const { t } = useTranslation()
7 |
8 | useEffect(() => {
9 | const interval = setInterval(() => {
10 | setCount((v) => (v === 5 ? 0 : v + 1))
11 | }, 1000)
12 |
13 | return () => clearInterval(interval)
14 | }, [])
15 |
16 | return {t('more-examples:plural-example', { count })}
17 | }
18 |
--------------------------------------------------------------------------------
/examples/without-loader/i18n.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | locales: ['en', 'ca', 'es'],
3 | defaultLocale: 'en',
4 | loader: false, // This deactivate the webpack loader that loads the namespaces
5 | pages: {
6 | '*': ['common'],
7 | '/404': ['error'],
8 | '/': ['home'],
9 | '/dashboard': ['home'],
10 | 'rgx:^/more-examples': ['more-examples'],
11 | },
12 | // When loader === false, then loadLocaleFrom is required
13 | loadLocaleFrom: async (locale, namespace) =>
14 | import(`./locales/${locale}/${namespace}`).then((r) => r.default),
15 | }
16 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/ca/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Llibreria next-translate"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/ca/dynamic.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-of-dynamic-translation": "Sóc contingut carregat de forma dinàmica"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/ca/error.json:
--------------------------------------------------------------------------------
1 | {
2 | "404": "Pàgina no trobada"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/ca/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Aquest text està escrit en Català desde el diccionari de la pàgina d'inici",
3 | "more-examples": "Més exemples"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/ca/more-examples.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-with-variable": "Aquest és un exemple amb variable {{count}}",
3 | "plural-example": "És en singular perquè el valor és {{count}}",
4 | "plural-example_0": "És cero perquè el valor és {{count}}",
5 | "plural-example_2": "És dos perquè el valor és {{count}}",
6 | "plural-example_other": "És en plural perquè el valor és {{count}}",
7 | "example-with-html": "<0>Aquest és un exempre <1>utilitzant HTML1> dintre de la traducció0>",
8 | "no-functional-example": "Traducció feta des d'un component no funcional",
9 | "dynamic-namespaces-link": "Veure un exemple de càrrega dinàmica",
10 | "dynamic-route": "Ruta dinàmica",
11 | "go-to-home": "Anar a la pàgina principal",
12 | "nested-example": {
13 | "very-nested": {
14 | "nested": "Exemple anidat!"
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/en/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "next-translate library"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/en/dynamic.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-of-dynamic-translation": "I'm a dynamic loaded content"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/en/error.json:
--------------------------------------------------------------------------------
1 | {
2 | "404": "Page not found"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/en/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "This text is wrote in English from the homepage dictionary",
3 | "more-examples": "More examples"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/en/more-examples.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-with-variable": "This is an example with variable {{count}}",
3 | "plural-example": "This is singular because the value is {{count}}",
4 | "plural-example_0": "Is zero because the value is {{count}}",
5 | "plural-example_2": "Is two because the value is {{count}}",
6 | "plural-example_other": "Is in plural because the value is {{count}}",
7 | "example-with-html": "<0>This is an example <1>using HTML1> inside the translation0>",
8 | "no-functional-example": "Translation done from a no-functional component",
9 | "dynamic-namespaces-link": "See an example of dynamic namespace",
10 | "dynamic-route": "Dynamic route",
11 | "go-to-home": "Go to the homepage",
12 | "nested-example": {
13 | "very-nested": {
14 | "nested": "Nested example!"
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/es/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Librería next-translate"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/es/dynamic.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-of-dynamic-translation": "Soy contenido cargado de forma dinámica"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/es/error.json:
--------------------------------------------------------------------------------
1 | {
2 | "404": "Página no encontrada"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/es/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Este texto está escrito en Español desde el diccionario de la página de inicio",
3 | "more-examples": "Más ejemplos"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/without-loader/locales/es/more-examples.json:
--------------------------------------------------------------------------------
1 | {
2 | "example-with-variable": "Este es un ejemplo con variable {{count}}",
3 | "plural-example": "Es en singular porque el valor es {{count}}",
4 | "plural-example_0": "Es cero porque el valor es {{count}}",
5 | "plural-example_2": "Es dos porque el valor es {{count}}",
6 | "plural-example_other": "Es en plural porque el valor es {{count}}",
7 | "example-with-html": "<0>Este es un ejemplo <1>usando HTML1> dentro de la traducción0>",
8 | "no-functional-example": "Traducción hecha desde un componente no funcional",
9 | "dynamic-namespaces-link": "Ver un ejemplo de carga dinámica",
10 | "dynamic-route": "Ruta dinamica",
11 | "go-to-home": "Ir a la página principal",
12 | "nested-example": {
13 | "very-nested": {
14 | "nested": "¡Ejemplo anidado!"
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/examples/without-loader/next.config.js:
--------------------------------------------------------------------------------
1 | const nextTranslate = require('next-translate-plugin')
2 |
3 | module.exports = nextTranslate()
4 |
--------------------------------------------------------------------------------
/examples/without-loader/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "-",
3 | "version": "1.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "next dev",
8 | "build": "next build"
9 | },
10 | "dependencies": {
11 | "next": "14.1.0",
12 | "next-translate": "link:../../",
13 | "react": "link:../../node_modules/react",
14 | "react-dom": "link:../../node_modules/react-dom"
15 | },
16 | "devDependencies": {
17 | "next-translate-plugin": "2.6.2"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/without-loader/pages/404.js:
--------------------------------------------------------------------------------
1 | import useTranslation from 'next-translate/useTranslation'
2 | import loadNamespaces from 'next-translate/loadNamespaces'
3 |
4 | export default function Error404() {
5 | const { t, lang } = useTranslation()
6 | const errorMessage = t`error:404`
7 |
8 | console.log({ lang })
9 |
10 | return errorMessage
11 | }
12 |
13 | export async function getStaticProps(ctx) {
14 | return {
15 | props: await loadNamespaces({
16 | ...ctx,
17 | pathname: '/404',
18 | }),
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/examples/without-loader/pages/_app.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import appWithI18n from 'next-translate/appWithI18n'
3 | import i18nConfig from '../i18n'
4 |
5 | function MyApp({ Component, pageProps }) {
6 | return
7 | }
8 |
9 | export default appWithI18n(MyApp, {
10 | ...i18nConfig,
11 | //
12 | // If you remove the "skipInitialProps", then all the namespaces
13 | // will be downloaded in the getInitialProps of the app.js and you
14 | // won't need to have any helper loadNamespaces on each page.
15 | //
16 | // skipInitialProps=false (default):
17 | // 🟢 Easy to configure
18 | // 🔴 All your pages are behind a server. No automatic page optimization.
19 | //
20 | // skipInitialProps=true:
21 | // 🔴 Hard to configure
22 | // 🟢 Better performance with automatic page optimization.
23 | //
24 | skipInitialProps: true,
25 | })
26 |
--------------------------------------------------------------------------------
/examples/without-loader/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Link from 'next/link'
3 | import useTranslation from 'next-translate/useTranslation'
4 | import loadNamespaces from 'next-translate/loadNamespaces'
5 | import Header from '../components/header'
6 |
7 | export default function Home() {
8 | const { t } = useTranslation('home')
9 | const description = t('description')
10 | const linkName = t('more-examples')
11 |
12 | return (
13 | <>
14 |
15 | {description}
16 | {linkName}
17 | >
18 | )
19 | }
20 |
21 | export async function getStaticProps(ctx) {
22 | return {
23 | props: await loadNamespaces({
24 | ...ctx,
25 | pathname: '/',
26 | }),
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/examples/without-loader/pages/more-examples/catchall/[...all].js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import useTranslation from 'next-translate/useTranslation'
3 | import loadNamespaces from 'next-translate/loadNamespaces'
4 | import { useRouter } from 'next/router'
5 |
6 | export default function All() {
7 | const { query } = useRouter()
8 | const { t, lang } = useTranslation()
9 |
10 | return (
11 | <>
12 | {JSON.stringify({ query, lang })}
13 |
14 | {t`more-examples:go-to-home`}
15 | >
16 | )
17 | }
18 |
19 | export function getStaticPaths({ locales }) {
20 | return {
21 | paths: [
22 | ...locales.map((locale) => ({
23 | locale,
24 | params: { all: ['this'] },
25 | })),
26 | ...locales.map((locale) => ({
27 | locale,
28 | params: { all: ['this', 'is'] },
29 | })),
30 | ...locales.map((locale) => ({
31 | locale,
32 | params: { all: ['this', 'is', 'an'] },
33 | })),
34 | ...locales.map((locale) => ({
35 | locale,
36 | params: { all: ['this', 'is', 'an', 'example'] },
37 | })),
38 | ],
39 | fallback: true,
40 | }
41 | }
42 |
43 | export async function getStaticProps(ctx) {
44 | return {
45 | props: await loadNamespaces({
46 | ...ctx,
47 | pathname: '/more-examples/catchall/[..all]',
48 | }),
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/examples/without-loader/pages/more-examples/dynamic-namespace.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Trans from 'next-translate/Trans'
3 | import DynamicNamespaces from 'next-translate/DynamicNamespaces'
4 | import loadNamespaces from 'next-translate/loadNamespaces'
5 |
6 | export default function ExampleWithDynamicNamespace() {
7 | return (
8 |
9 | {/* ALSO IS POSSIBLE TO USE NAMESPACES FROM THE PAGE */}
10 |
11 |
12 |
13 |
14 | {/* USING DYNAMIC NAMESPACE */}
15 |
16 |
17 | )
18 | }
19 |
20 | export async function getStaticProps(ctx) {
21 | return {
22 | props: await loadNamespaces({
23 | ...ctx,
24 | pathname: '/more-examples/dynamic-namespace',
25 | }),
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/examples/without-loader/pages/more-examples/dynamicroute/[slug].js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import useTranslation from 'next-translate/useTranslation'
3 | import { useRouter } from 'next/router'
4 | import loadNamespaces from 'next-translate/loadNamespaces'
5 |
6 | export default function DynamicRoute() {
7 | const { query } = useRouter()
8 | const { t, lang } = useTranslation()
9 |
10 | console.log({ query })
11 |
12 | return (
13 | <>
14 | {t`more-examples:dynamic-route`}
15 |
16 | {query.slug} - {lang}
17 |
18 | {t`more-examples:go-to-home`}
19 | >
20 | )
21 | }
22 |
23 | export function getStaticPaths({ locales }) {
24 | return {
25 | paths: locales.map((locale) => ({
26 | locale,
27 | params: { slug: 'example' },
28 | })),
29 | fallback: true,
30 | }
31 | }
32 |
33 | export async function getStaticProps(ctx) {
34 | return {
35 | props: await loadNamespaces({
36 | ...ctx,
37 | pathname: '/more-examples/dynamicroute/[slug]',
38 | }),
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/without-loader/pages/more-examples/index.js:
--------------------------------------------------------------------------------
1 | import useTranslation from 'next-translate/useTranslation'
2 | import Trans from 'next-translate/Trans'
3 | import Link from 'next/link'
4 | import loadNamespaces from 'next-translate/loadNamespaces'
5 |
6 | import PluralExample from '../../components/plural-example'
7 | import Header from '../../components/header'
8 | import NoFunctionalComponent from '../../components/no-functional-component'
9 |
10 | const Component = (props) =>
11 |
12 | export default function MoreExamples() {
13 | const { t } = useTranslation()
14 | const exampleWithVariable = t('more-examples:example-with-variable', {
15 | count: 42,
16 | })
17 |
18 | return (
19 | <>
20 |
21 | {exampleWithVariable}
22 |
23 | , ]}
26 | />
27 |
28 |
29 | {t`more-examples:nested-example.very-nested.nested`}
30 |
31 |
32 | {t('more-examples:dynamic-namespaces-link')}
33 |
34 |
35 |
41 | {t('more-examples:dynamic-route')}
42 |
43 |
44 | Catchall
45 | >
46 | )
47 | }
48 |
49 | export async function getStaticProps(ctx) {
50 | return {
51 | props: await loadNamespaces({
52 | ...ctx,
53 | pathname: '/more-examples',
54 | }),
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/examples/without-loader/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aralroca/next-translate/c47f226ffd0c0f2600595ae7620f8708d6f70a05/examples/without-loader/public/favicon.ico
--------------------------------------------------------------------------------
/images/bundle-size.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aralroca/next-translate/c47f226ffd0c0f2600595ae7620f8708d6f70a05/images/bundle-size.png
--------------------------------------------------------------------------------
/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
128 |
--------------------------------------------------------------------------------
/images/translation-prerendered.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aralroca/next-translate/c47f226ffd0c0f2600595ae7620f8708d6f70a05/images/translation-prerendered.gif
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | jest.mock('next/router', () => require('next-router-mock'))
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-translate",
3 | "version": "3.0.0-canary.2",
4 | "description": "Tiny and powerful i18n tools (Next plugin + API) to translate your Next.js pages.",
5 | "license": "MIT",
6 | "keywords": [
7 | "react",
8 | "preact",
9 | "i18n",
10 | "nextjs",
11 | "next.js",
12 | "next",
13 | "plugin",
14 | "translate",
15 | "translation",
16 | "internationalization",
17 | "locale",
18 | "locales",
19 | "globalization"
20 | ],
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/aralroca/next-translate.git"
24 | },
25 | "author": {
26 | "name": "Aral Roca Gòmez",
27 | "email": "contact@aralroca.com"
28 | },
29 | "main": "./lib/cjs/index.js",
30 | "module": "./lib/esm/index.js",
31 | "types": "./index.d.ts",
32 | "files": [
33 | "lib",
34 | "*.d.ts",
35 | "appWithI18n",
36 | "DynamicNamespaces",
37 | "I18nProvider",
38 | "context",
39 | "getT",
40 | "loadNamespaces",
41 | "Trans",
42 | "TransText",
43 | "withTranslation",
44 | "useTranslation",
45 | "setLanguage",
46 | "index",
47 | "AppDirI18nProvider",
48 | "createTranslation",
49 | "formatElements"
50 | ],
51 | "scripts": {
52 | "build": "yarn clean && cross-env NODE_ENV=production && yarn tsc",
53 | "clean": "yarn clean:build && yarn clean:examples",
54 | "clean:build": "del lib appWith* Dynamic* I18n* index context loadNa* setLang* Trans* useT* withT* getP* getC* *.d.ts getT transC* wrapT* types formatElements AppDirI18nProvider* createTrans*",
55 | "clean:examples": "del examples/**/.next examples/**/node_modules examples/**/yarn.lock",
56 | "example": "yarn example:complex",
57 | "example:basic": "yarn build && yarn --cwd examples/basic && yarn --cwd examples/basic dev",
58 | "example:complex": "yarn build && yarn --cwd examples/complex && yarn --cwd examples/complex dev",
59 | "example:with-app-directory": "yarn build && yarn --cwd examples/with-app-directory && yarn --cwd examples/with-app-directory dev",
60 | "example:without-loader": "yarn build && yarn --cwd examples/without-loader && yarn --cwd examples/without-loader dev",
61 | "format": "pretty-quick",
62 | "husky": "pretty-quick --staged && yarn test",
63 | "prepare": "husky install",
64 | "prepublish": "yarn test && yarn build",
65 | "test": "cross-env NODE_ENV=test jest --env=jsdom",
66 | "test:coverage": "cross-env NODE_ENV=test jest --env=jsdom --coverage",
67 | "test:watch": "cross-env NODE_ENV=test jest --env=jsdom --watch",
68 | "tsc": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && node build-packages.js"
69 | },
70 | "devDependencies": {
71 | "@babel/cli": "7.22.5",
72 | "@babel/core": "7.22.5",
73 | "@babel/preset-env": "7.22.5",
74 | "@babel/preset-typescript": "7.22.5",
75 | "@testing-library/react": "13.4.0",
76 | "@types/jest": "29.5.2",
77 | "@types/node": "20.3.1",
78 | "@types/react": "18.2.13",
79 | "@types/react-dom": "18.2.6",
80 | "@types/webpack": "5.28.1",
81 | "babel-jest": "29.5.0",
82 | "babel-plugin-transform-es2015-modules-commonjs": "6.26.2",
83 | "babel-preset-minify": "0.5.2",
84 | "cross-env": "7.0.3",
85 | "del-cli": "^5.0.0",
86 | "express": "4.18.2",
87 | "husky": "7.0.4",
88 | "jest": "27.3.1",
89 | "next": "13.4.7",
90 | "next-router-mock": "0.9.6",
91 | "prettier": "2.8.8",
92 | "pretty-quick": "3.1.3",
93 | "react": "18.2.0",
94 | "react-dom": "18.2.0",
95 | "supertest": "6.3.3",
96 | "typescript": "5.1.3"
97 | },
98 | "peerDependencies": {
99 | "next": ">= 13.2.5",
100 | "react": ">= 18.0.0"
101 | },
102 | "prettier": {
103 | "trailingComma": "es5",
104 | "tabWidth": 2,
105 | "semi": false,
106 | "singleQuote": true
107 | },
108 | "engines": {
109 | "node": ">=16.10.0"
110 | },
111 | "jest": {
112 | "roots": [
113 | "/__tests__",
114 | "/src"
115 | ],
116 | "setupFilesAfterEnv": [
117 | "/jest.setup.js"
118 | ],
119 | "testPathIgnorePatterns": [
120 | "/node_modules/",
121 | ".utils.js"
122 | ],
123 | "transform": {
124 | "^.+\\.(j|t)sx?$": "babel-jest"
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/AppDirI18nProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { I18nDictionary, LoaderConfig } from '.'
4 |
5 | type AppDirI18nProviderProps = {
6 | lang: string
7 | namespaces: Record
8 | config: LoaderConfig
9 | children: React.ReactNode
10 | }
11 |
12 | /**
13 | * @description AppDirI18nProvider for internal use (next-translate-plugin) only.
14 | * Is required because this is a RCC (React Client Component) and this way we
15 | * provide a global i18n context for client components.
16 | */
17 | export default function AppDirI18nProvider({
18 | lang,
19 | namespaces = {},
20 | config,
21 | children,
22 | }: AppDirI18nProviderProps) {
23 | globalThis.__NEXT_TRANSLATE__ = { lang, namespaces, config }
24 |
25 | // It return children and avoid re-renders and also allow children to be RSC (React Server Components)
26 | return children
27 | }
28 |
--------------------------------------------------------------------------------
/src/DynamicNamespaces.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from 'react'
2 | import { DynamicNamespacesProps, I18nConfig, I18nDictionary } from '.'
3 | import I18nProvider, { InternalContext } from './I18nProvider'
4 | import useTranslation from './useTranslation'
5 |
6 | export default function DynamicNamespaces({
7 | dynamic,
8 | namespaces = [],
9 | fallback,
10 | children,
11 | }: DynamicNamespacesProps): any {
12 | const config = useContext(InternalContext).config as I18nConfig
13 | const { lang } = useTranslation()
14 | const [loaded, setLoaded] = useState(false)
15 | const [pageNs, setPageNs] = useState([])
16 | const loadLocale =
17 | dynamic || config.loadLocaleFrom || (() => Promise.resolve({}))
18 |
19 | async function loadNamespaces() {
20 | if (typeof loadLocale !== 'function') return
21 |
22 | const pageNamespaces = await Promise.all(
23 | namespaces.map((ns) => loadLocale(lang, ns))
24 | )
25 | setPageNs(pageNamespaces)
26 | setLoaded(true)
27 | }
28 |
29 | useEffect(() => {
30 | loadNamespaces()
31 | }, [namespaces.join()])
32 |
33 | if (!loaded) return fallback || null
34 |
35 | return (
36 | , ns, i) => {
40 | obj[ns] = pageNs[i]
41 | return obj
42 | },
43 | {}
44 | )}
45 | >
46 | {children}
47 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/src/I18nProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext } from 'react'
2 | import { useRouter } from 'next/router'
3 | import I18nContext from './context'
4 | import transCore from './transCore'
5 | import useTranslation from './useTranslation'
6 | import { I18nDictionary, I18nProviderProps } from '.'
7 |
8 | export const InternalContext = createContext({ ns: {}, config: {} })
9 |
10 | export default function I18nProvider({
11 | lang: lng,
12 | namespaces = {},
13 | children,
14 | config: newConfig = {},
15 | }: I18nProviderProps) {
16 | const { lang: parentLang } = useTranslation()
17 | const { locale, defaultLocale } = useRouter() || {}
18 | const internal = useContext(InternalContext)
19 | const allNamespaces: Record = {
20 | ...initialBrowserNamespaces(),
21 | ...internal.ns,
22 | ...namespaces,
23 | }
24 | const lang = lng || parentLang || locale || defaultLocale || ''
25 | const config = { ...internal.config, ...newConfig }
26 | const localesToIgnore = config.localesToIgnore || ['default']
27 | const ignoreLang = !lang || localesToIgnore.includes(lang)
28 | const pluralRules = new Intl.PluralRules(ignoreLang ? undefined : lang)
29 | const t = transCore({ config, allNamespaces, pluralRules, lang })
30 |
31 | return (
32 |
33 |
34 | {children}
35 |
36 |
37 | )
38 | }
39 |
40 | function initialBrowserNamespaces() {
41 | if (typeof window === 'undefined') return {}
42 | return window.__NEXT_DATA__?.props?.__namespaces || {}
43 | }
44 |
--------------------------------------------------------------------------------
/src/Trans.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 |
3 | import { TransProps } from '.'
4 | import useTranslation from './useTranslation'
5 | import formatElements from './formatElements'
6 |
7 | /**
8 | * Translate transforming:
9 | * <0>This is an <1>example1><0>
10 | * to -> This is an example
11 | */
12 | export default function Trans({
13 | i18nKey,
14 | values,
15 | components,
16 | fallback,
17 | defaultTrans,
18 | ns,
19 | returnObjects,
20 | }: TransProps): any {
21 | const { t, lang } = useTranslation(ns)
22 |
23 | /**
24 | * Memoize the transformation
25 | */
26 | const result = useMemo(() => {
27 | const text = t(i18nKey, values, {
28 | fallback,
29 | default: defaultTrans,
30 | returnObjects,
31 | })
32 |
33 | if (!text) return text
34 |
35 | if (!components || components.length === 0)
36 | return Array.isArray(text) ? text.map((item) => item) : text
37 |
38 | if (Array.isArray(text))
39 | return text.map((item) => formatElements(item, components))
40 |
41 | return formatElements(text, components)
42 | }, [i18nKey, values, components, lang]) as string
43 |
44 | return result
45 | }
46 |
--------------------------------------------------------------------------------
/src/TransText.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 |
3 | import formatElements from './formatElements'
4 | import { TransProps } from '.'
5 |
6 | type ValueTransProps = Pick & {
7 | text: string
8 | }
9 |
10 | export default function TransText({ text, components }: ValueTransProps): any {
11 | return useMemo(
12 | () =>
13 | !components || components.length === 0
14 | ? text
15 | : formatElements(text, components),
16 | [text, components]
17 | ) as string
18 | }
19 |
--------------------------------------------------------------------------------
/src/appWithI18n.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import I18nProvider from './I18nProvider'
3 | import loadNamespaces from './loadNamespaces'
4 | import { LoaderConfig } from '.'
5 |
6 | type Props = {
7 | [key: string]: any
8 | }
9 |
10 | interface PartialNextContext {
11 | res?: any
12 | AppTree?: NextComponentType
13 | Component?: NextComponentType
14 | ctx?: PartialNextContext
15 | [key: string]: any
16 | }
17 |
18 | type NextComponentType<
19 | C extends PartialNextContext = PartialNextContext,
20 | IP = {},
21 | P = {}
22 | > = React.ComponentType & {
23 | getInitialProps?(context: C): IP | Promise
24 | }
25 |
26 | export default function appWithI18n(
27 | AppToTranslate: NextComponentType,
28 | config: LoaderConfig = {}
29 | ) {
30 | if (!config.isLoader && config.loader !== false) {
31 | console.warn(
32 | '🚨 [next-translate] You can remove the "appWithI18n" HoC on the _app.js, unless you set "loader: false" in your i18n config file.'
33 | )
34 | }
35 |
36 | function AppWithTranslations(props: Props) {
37 | const { defaultLocale } = config
38 |
39 | return (
40 |
45 |
46 |
47 | )
48 | }
49 |
50 | if (typeof config.staticsHoc === 'function') {
51 | config.staticsHoc(AppWithTranslations, AppToTranslate)
52 | }
53 |
54 | // @ts-ignore
55 | if (typeof window === 'undefined') global.i18nConfig = config
56 | // @ts-ignore
57 | else window.i18nConfig = config
58 |
59 | if (config.skipInitialProps) return AppWithTranslations
60 |
61 | AppWithTranslations.getInitialProps = async (appCtx: any) => {
62 | const ctx = { ...(appCtx.ctx || {}), ...(appCtx || {}) }
63 | let appProps: object = { pageProps: {} }
64 |
65 | if (AppToTranslate.getInitialProps) {
66 | appProps = (await AppToTranslate.getInitialProps(appCtx)) || {}
67 | }
68 |
69 | return {
70 | ...appProps,
71 | ...(await loadNamespaces({
72 | ...ctx,
73 | ...config,
74 | loaderName: 'getInitialProps',
75 | })),
76 | }
77 | }
78 |
79 | return AppWithTranslations
80 | }
81 |
--------------------------------------------------------------------------------
/src/context.tsx:
--------------------------------------------------------------------------------
1 | import { I18n } from '.'
2 | import React from 'react'
3 |
4 | let context
5 |
6 | // For serverComponents (app-dir), the context cannot be used and
7 | // this makes that all the imports to here don't break the build.
8 | // The use of this context is inside each util, depending pages-dir or app-dir.
9 | if (typeof React.createContext === 'function') {
10 | context = React.createContext({
11 | t: (k) => (Array.isArray(k) ? k[0] : k),
12 | lang: '',
13 | })
14 | }
15 |
16 | export default context as React.Context
17 |
--------------------------------------------------------------------------------
/src/createTranslation.tsx:
--------------------------------------------------------------------------------
1 | import transCore from './transCore'
2 | import wrapTWithDefaultNs from './wrapTWithDefaultNs'
3 |
4 | // Only for App directory
5 | export default function createTranslation(defaultNS?: string) {
6 | const { lang, namespaces, config } = globalThis.__NEXT_TRANSLATE__ ?? {}
7 | const localesToIgnore = config.localesToIgnore || ['default']
8 | const ignoreLang = !lang || localesToIgnore.includes(lang)
9 | const t = transCore({
10 | config,
11 | allNamespaces: namespaces,
12 | pluralRules: new Intl.PluralRules(ignoreLang ? undefined : lang),
13 | lang,
14 | })
15 |
16 | return { t: wrapTWithDefaultNs(t, defaultNS), lang }
17 | }
18 |
--------------------------------------------------------------------------------
/src/formatElements.tsx:
--------------------------------------------------------------------------------
1 | import React, { cloneElement, Fragment, ReactElement, ReactNode } from 'react'
2 |
3 | export const tagParsingRegex = /<(\w+) *>(.*?)<\/\1 *>|<(\w+) *\/>/
4 |
5 | const nlRe = /(?:\r\n|\r|\n)/g
6 |
7 | function getElements(
8 | parts: Array
9 | ): Array[] {
10 | if (!parts.length) return []
11 |
12 | const [paired, children, unpaired, after] = parts.slice(0, 4)
13 |
14 | return [
15 | [(paired || unpaired) as string, children || ('' as string), after],
16 | ].concat(getElements(parts.slice(4, parts.length)))
17 | }
18 |
19 | export default function formatElements(
20 | value: string,
21 | elements: ReactElement[] | Record = []
22 | ): string | ReactNode[] {
23 | const parts = value.replace(nlRe, '').split(tagParsingRegex)
24 |
25 | if (parts.length === 1) return value
26 |
27 | const tree = []
28 |
29 | const before = parts.shift()
30 | if (before) tree.push(before)
31 |
32 | getElements(parts).forEach(([key, children, after], realIndex: number) => {
33 | const element =
34 | // @ts-ignore
35 | elements[key as string] ||
36 |
37 | tree.push(
38 | cloneElement(
39 | element,
40 | { key: realIndex },
41 |
42 | // format children for pair tags
43 | // unpaired tags might have children if it's a component passed as a variable
44 | children ? formatElements(children, elements) : element.props.children
45 | )
46 | )
47 |
48 | if (after) tree.push(after)
49 | })
50 |
51 | return tree
52 | }
53 |
--------------------------------------------------------------------------------
/src/getConfig.tsx:
--------------------------------------------------------------------------------
1 | import { LoaderConfig } from './index'
2 |
3 | export default function getConfig(): LoaderConfig {
4 | // We are not using globalThis to support Node 10
5 | const g: any = typeof window === 'undefined' ? global : window
6 | return g.i18nConfig
7 | }
8 |
--------------------------------------------------------------------------------
/src/getPageNamespaces.tsx:
--------------------------------------------------------------------------------
1 | import { I18nConfig, PageValue } from '.'
2 |
3 | // @todo Replace to [].flat() in the future
4 | function flat(a: string[][]): string[] {
5 | return a.reduce((b, c) => b.concat(c), [])
6 | }
7 |
8 | /**
9 | * Get page namespaces
10 | *
11 | * @param {object} config
12 | * @param {string} page
13 | */
14 | export default async function getPageNamespaces(
15 | { pages = {} }: I18nConfig,
16 | page: string,
17 | ctx: object
18 | ): Promise {
19 | const rgx = 'rgx:'
20 | const getNs = async (ns: PageValue): Promise =>
21 | typeof ns === 'function' ? ns(ctx) : ns || []
22 |
23 | // Namespaces promises using regex
24 | const rgxs = Object.keys(pages).reduce((arr: Promise[], p) => {
25 | if (
26 | p.substring(0, rgx.length) === rgx &&
27 | new RegExp(p.replace(rgx, '')).test(page)
28 | ) {
29 | arr.push(getNs(pages[p]))
30 | }
31 | return arr
32 | }, [])
33 |
34 | return [
35 | ...(await getNs(pages['*'])),
36 | ...(await getNs(pages[page])),
37 | ...flat(await Promise.all(rgxs)),
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/src/getT.tsx:
--------------------------------------------------------------------------------
1 | import getConfig from './getConfig'
2 | import transCore from './transCore'
3 | import wrapTWithDefaultNs from './wrapTWithDefaultNs'
4 | import { I18nDictionary, LocaleLoader } from './index'
5 |
6 | export default async function getT(
7 | locale = '',
8 | namespace: string | string[] = ''
9 | ) {
10 | const appDir = globalThis.__NEXT_TRANSLATE__
11 | const config = appDir?.config ?? getConfig()
12 | const defaultLoader = async () => Promise.resolve({})
13 | const lang = locale || config.defaultLocale || ''
14 | const loader: LocaleLoader = config.loadLocaleFrom || defaultLoader
15 |
16 | const namespaces = Array.isArray(namespace) ? namespace : [namespace]
17 |
18 | const allNamespaces: Record = {}
19 | await Promise.all(
20 | namespaces.map(async (namespace) => {
21 | allNamespaces[namespace] = await loader(lang, namespace)
22 | })
23 | )
24 |
25 | const localesToIgnore = config.localesToIgnore || ['default']
26 | const ignoreLang = localesToIgnore.includes(lang)
27 | const pluralRules = new Intl.PluralRules(ignoreLang ? undefined : lang)
28 | const t = transCore({ config, allNamespaces, pluralRules, lang })
29 |
30 | const defaultNamespace = namespaces[0]
31 | return wrapTWithDefaultNs(t, defaultNamespace)
32 | }
33 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement, ReactNode } from 'react'
2 | import type { NextConfig } from 'next'
3 |
4 | export interface TranslationQuery {
5 | [name: string]: any
6 | }
7 |
8 | export type DataForStoreType = {
9 | lang: string
10 | namespaces: Record
11 | config: LoaderConfig
12 | }
13 |
14 | export type Translate = (
15 | i18nKey: string | TemplateStringsArray,
16 | query?: TranslationQuery | null,
17 | options?: {
18 | returnObjects?: boolean
19 | fallback?: string | string[]
20 | default?: T | string
21 | ns?: string
22 | }
23 | ) => T
24 |
25 | export interface I18n {
26 | t: Translate
27 | lang: string
28 | }
29 |
30 | export interface I18nProviderProps {
31 | lang?: string
32 | namespaces?: Record
33 | children?: ReactNode
34 | config?: I18nConfig
35 | }
36 |
37 | export interface TransProps {
38 | i18nKey: string
39 | components?: ReactElement[] | Record
40 | values?: TranslationQuery
41 | fallback?: string | string[]
42 | defaultTrans?: string
43 | ns?: string
44 | returnObjects?: boolean
45 | }
46 |
47 | export type PageValue = string[] | ((context: object) => string[])
48 |
49 | export type LocaleLoader = (
50 | language: string | undefined,
51 | namespace: string
52 | ) => Promise
53 |
54 | // Makes the specified properties within a Typescript interface optional
55 | export type Optional = Pick, K> & Omit
56 |
57 | // Built-in i18n Next.js options
58 | export type RawNextI18nConfig = Exclude
59 | export type NextI18nConfig = Optional<
60 | RawNextI18nConfig,
61 | 'locales' | 'defaultLocale'
62 | >
63 |
64 | export interface I18nConfig extends NextI18nConfig {
65 | loadLocaleFrom?: LocaleLoader
66 | localesToIgnore?: string[]
67 | pages?: Record
68 | logger?: I18nLogger
69 | loggerEnvironment?: 'node' | 'browser' | 'both'
70 | staticsHoc?: Function
71 | extensionsRgx?: string
72 | loader?: boolean
73 | logBuild?: boolean
74 | revalidate?: number
75 | pagesInDir?: string
76 | interpolation?: {
77 | format?: (value: TranslationQuery[string], format: any, lang: string | undefined) => string
78 | prefix?: string
79 | suffix?: string
80 | }
81 | keySeparator?: string | false
82 | nsSeparator?: string | false
83 | defaultNS?: string
84 | allowEmptyStrings?: boolean
85 | }
86 |
87 | export interface LoaderConfig extends I18nConfig {
88 | locale?: string
89 | router?: { locale: string }
90 | pathname?: string
91 | skipInitialProps?: boolean
92 | loaderName?: string
93 | isLoader?: boolean
94 | [key: string]: any
95 | }
96 |
97 | export interface LoggerProps {
98 | namespace: string | undefined
99 | i18nKey: string
100 | }
101 |
102 | export interface I18nLogger {
103 | (context: LoggerProps): void
104 | }
105 |
106 | export interface I18nDictionary {
107 | [key: string]: string | I18nDictionary
108 | }
109 |
110 | export interface DynamicNamespacesProps {
111 | dynamic?: LocaleLoader
112 | namespaces?: string[]
113 | fallback?: ReactNode
114 | children?: ReactNode
115 | }
116 |
117 | declare global {
118 | // For NodeJS 16+
119 | // eslint-disable-next-line no-var
120 | var i18nConfig: LoaderConfig
121 | // eslint-disable-next-line no-var
122 | var __NEXT_TRANSLATE__: {
123 | namespaces: Record
124 | lang: string
125 | config: LoaderConfig
126 | }
127 |
128 | namespace NodeJS {
129 | interface Global {
130 | i18nConfig: LoaderConfig
131 | __NEXT_TRANSLATE__: {
132 | namespaces: Record
133 | lang: string
134 | }
135 | }
136 | }
137 |
138 | interface Window {
139 | i18nConfig: LoaderConfig
140 | __NEXT_TRANSLATE__: {
141 | namespaces: Record
142 | lang: string
143 | }
144 | }
145 | }
146 |
147 | //////// For type safety (next-translate.d.ts): ///////////
148 | /*
149 | *
150 | * import type { Paths, I18n, Translate } from "next-translate";
151 | *
152 | * export interface TranslationsKeys {
153 | * common: Paths;
154 | * home: Paths;
155 | * }
156 | *
157 | * export interface TypeSafeTranslate
158 | * extends Omit {
159 | * t: {
160 | * (key: TranslationsKeys[Namespace], ...rest: Tail>): string;
161 | * (template: TemplateStringsArray): string;
162 | * };
163 | * }
164 | *
165 | * declare module "next-translate/useTranslation" {
166 | * export default function useTranslation<
167 | * Namespace extends keyof TranslationsKeys,
168 | * >(namespace: Namespace): TypeSafeTranslate;
169 | * }
170 | */
171 |
172 | type RemovePlural = Key extends `${infer Prefix}${
173 | | '_zero'
174 | | '_one'
175 | | '_two'
176 | | '_few'
177 | | '_many'
178 | | '_other'
179 | | `_${number}`}`
180 | ? Prefix
181 | : Key
182 |
183 | type Join = S1 extends string
184 | ? S2 extends string
185 | ? `${S1}.${S2}`
186 | : never
187 | : never
188 |
189 | // @ts-ignore
190 | export type Paths = RemovePlural<
191 | // @ts-ignore
192 | {
193 | // @ts-ignore
194 | [K in Extract]: T[K] extends Record
195 | ? Join>
196 | : K
197 | }[Extract]
198 | >
199 |
200 | // TODO: Remove this in future versions > 2.0.0
201 | function nextTranslate(nextConfig: NextConfig = {}): NextConfig {
202 | console.log(`
203 | #########################################################################
204 | # #
205 | # next-translate plugin in 2.0.0 is replaced by #
206 | # next-translate-plugin package: #
207 | # #
208 | # > yarn add next-translate-plugin -D #
209 | # or: #
210 | # > npm install next-translate-plugin --save-dev #
211 | # #
212 | # replace in next.config.js file: #
213 | # const nextTranslate = require('next-translate') #
214 | # to: #
215 | # const nextTranslate = require('next-translate-plugin') #
216 | # #
217 | #########################################################################
218 | `)
219 | return nextConfig
220 | }
221 |
222 | module.exports = nextTranslate
223 | export default nextTranslate
224 |
--------------------------------------------------------------------------------
/src/loadNamespaces.tsx:
--------------------------------------------------------------------------------
1 | import { I18nDictionary, LoaderConfig, LocaleLoader } from '.'
2 | import getConfig from './getConfig'
3 | import getPageNamespaces from './getPageNamespaces'
4 |
5 | export default async function loadNamespaces(
6 | config: LoaderConfig = {} as LoaderConfig
7 | ): Promise<{
8 | __lang: string
9 | __namespaces?: Record
10 | }> {
11 | const conf = { ...getConfig(), ...config }
12 | const localesToIgnore = conf.localesToIgnore || ['default']
13 | const __lang: string =
14 | conf.req?.locale ||
15 | conf.locale ||
16 | conf.router?.locale ||
17 | conf.defaultLocale ||
18 | ''
19 |
20 | if (!conf.pathname) {
21 | console.warn(
22 | '🚨 [next-translate] You forgot to pass the "pathname" inside "loadNamespaces" configuration'
23 | )
24 | return { __lang }
25 | }
26 |
27 | if (localesToIgnore.includes(__lang)) return { __lang }
28 |
29 | if (!conf.loaderName && conf.loader !== false) {
30 | console.warn(
31 | '🚨 [next-translate] You can remove the "loadNamespaces" helper, unless you set "loader: false" in your i18n config file.'
32 | )
33 | }
34 |
35 | const page = removeTrailingSlash(conf.pathname.replace(/\/index$/, '')) || '/'
36 | const namespaces = await getPageNamespaces(conf, page, conf)
37 | const defaultLoader: LocaleLoader = () => Promise.resolve({})
38 | const pageNamespaces =
39 | (await Promise.all(
40 | namespaces.map((ns) =>
41 | typeof conf.loadLocaleFrom === 'function'
42 | ? conf.loadLocaleFrom(__lang, ns).catch(() => ({}))
43 | : defaultLoader(__lang, ns)
44 | )
45 | )) || []
46 |
47 | log(conf, { page, lang: __lang, namespaces })
48 |
49 | return {
50 | __lang,
51 | __namespaces: namespaces.reduce(
52 | (obj: Record, ns, i) => {
53 | obj[ns] = pageNamespaces[i] || (null as unknown as I18nDictionary)
54 | return obj
55 | },
56 | {}
57 | ),
58 | }
59 | }
60 |
61 | function removeTrailingSlash(path = '') {
62 | return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path
63 | }
64 |
65 | type LogProps = {
66 | page: string
67 | lang: string
68 | namespaces: string[]
69 | }
70 |
71 | export function log(conf: LoaderConfig, { page, lang, namespaces }: LogProps) {
72 | if (conf.logBuild !== false && typeof window === 'undefined') {
73 | const colorEnabled =
74 | process.env.NODE_DISABLE_COLORS == null &&
75 | process.env.NO_COLOR == null &&
76 | process.env.TERM !== 'dumb' &&
77 | process.env.FORCE_COLOR !== '0'
78 | const color = (c: string) => (colorEnabled ? `\x1b[36m${c}\x1b[0m` : c)
79 | console.log(
80 | color('next-translate'),
81 | `- compiled page:`,
82 | color(page),
83 | '- locale:',
84 | color(lang),
85 | '- namespaces:',
86 | color(namespaces.join(', ')),
87 | '- used loader:',
88 | color(conf.loaderName || '-')
89 | )
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/setLanguage.tsx:
--------------------------------------------------------------------------------
1 | import Router from 'next/router'
2 |
3 | export default async function setLanguage(
4 | locale: string,
5 | scroll = true
6 | ): Promise {
7 | return await Router.push(
8 | {
9 | pathname: Router.pathname,
10 | query: Router.query,
11 | },
12 | Router.asPath,
13 | { locale, scroll }
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/transCore.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | I18nConfig,
3 | I18nDictionary,
4 | LoaderConfig,
5 | LoggerProps,
6 | TranslationQuery,
7 | } from '.'
8 | import { Translate } from './index'
9 |
10 | function splitNsKey(key: string, nsSeparator: string | false) {
11 | if (!nsSeparator) return { i18nKey: key }
12 | const i = key.indexOf(nsSeparator)
13 | if (i < 0) return { i18nKey: key }
14 | return {
15 | namespace: key.slice(0, i),
16 | i18nKey: key.slice(i + nsSeparator.length),
17 | }
18 | }
19 |
20 | export default function transCore({
21 | config,
22 | allNamespaces,
23 | pluralRules,
24 | lang,
25 | }: {
26 | config: LoaderConfig
27 | allNamespaces: Record
28 | pluralRules: Intl.PluralRules
29 | lang: string | undefined
30 | }): Translate {
31 | const {
32 | logger = missingKeyLogger,
33 | // An optional parameter allowEmptyStrings - true as default.
34 | // If allowEmptyStrings parameter is marked as false,
35 | // it should log an error when an empty string is attempted to be translated
36 | // and return the namespace and key as result of the translation.
37 | allowEmptyStrings = true,
38 | } = config
39 |
40 | const interpolateUnknown = (
41 | value: unknown,
42 | query?: TranslationQuery | null
43 | ): typeof value => {
44 | if (Array.isArray(value)) {
45 | return value.map((val) => interpolateUnknown(val, query))
46 | }
47 | if (value instanceof Object) {
48 | return objectInterpolation({
49 | obj: value as Record,
50 | query,
51 | config,
52 | lang,
53 | })
54 | }
55 | return interpolation({ text: value as string, query, config, lang })
56 | }
57 |
58 | const t: Translate = (key = '', query, options) => {
59 | const k = Array.isArray(key) ? key[0] : key
60 | const { nsSeparator = ':', loggerEnvironment = 'browser' } = config
61 |
62 | const { i18nKey, namespace = options?.ns ?? config.defaultNS } = splitNsKey(
63 | k,
64 | nsSeparator
65 | )
66 |
67 | const dic = (namespace && allNamespaces[namespace]) || {}
68 | const keyWithPlural = plural(
69 | pluralRules,
70 | dic,
71 | i18nKey,
72 | config,
73 | query,
74 | options
75 | )
76 | const dicValue = getDicValue(dic, keyWithPlural, config, options)
77 | const value =
78 | typeof dicValue === 'object'
79 | ? JSON.parse(JSON.stringify(dicValue))
80 | : dicValue
81 |
82 | const empty =
83 | typeof value === 'undefined' ||
84 | (typeof value === 'object' && !Object.keys(value).length) ||
85 | (value === '' && !allowEmptyStrings)
86 |
87 | const fallbacks =
88 | typeof options?.fallback === 'string'
89 | ? [options.fallback]
90 | : options?.fallback || []
91 |
92 | if (
93 | empty &&
94 | (loggerEnvironment === 'both' ||
95 | loggerEnvironment ===
96 | (typeof window === 'undefined' ? 'node' : 'browser'))
97 | ) {
98 | logger({ namespace, i18nKey })
99 | }
100 |
101 | // Fallbacks
102 | if (empty && Array.isArray(fallbacks) && fallbacks.length) {
103 | const [firstFallback, ...restFallbacks] = fallbacks
104 | if (typeof firstFallback === 'string') {
105 | return t(firstFallback, query, { ...options, fallback: restFallbacks })
106 | }
107 | }
108 |
109 | if (
110 | empty &&
111 | options &&
112 | // options.default could be a nullish value so check that the property exists
113 | options.hasOwnProperty('default') &&
114 | !fallbacks?.length
115 | ) {
116 | // if options.default is falsey there's no reason to do interpolation
117 | return options.default
118 | ? interpolateUnknown(options.default, query)
119 | : options.default
120 | }
121 |
122 | // no need to try interpolation
123 | if (empty) {
124 | return k
125 | }
126 |
127 | // this can return an empty string if either value was already empty
128 | // or it contained only an interpolation (e.g. "{{name}}") and the query param was empty
129 | return interpolateUnknown(value, query)
130 | }
131 |
132 | return t
133 | }
134 |
135 | /**
136 | * Get value from key (allow nested keys as parent.children)
137 | */
138 | function getDicValue(
139 | dic: I18nDictionary,
140 | key: string = '',
141 | config: I18nConfig,
142 | options: { returnObjects?: boolean; fallback?: string | string[] } = {
143 | returnObjects: false,
144 | }
145 | ): unknown | undefined {
146 | const { keySeparator = '.' } = config || {}
147 | const keyParts = keySeparator ? key.split(keySeparator) : [key]
148 |
149 | if (key === keySeparator && options.returnObjects) return dic
150 |
151 | const value: string | object = keyParts.reduce(
152 | (val: I18nDictionary | string, key: string) => {
153 | if (typeof val === 'string') {
154 | return {}
155 | }
156 |
157 | const res = val[key as keyof typeof val]
158 |
159 | // pass all truthy values or (empty) strings
160 | return res || (typeof res === 'string' ? res : {})
161 | },
162 | dic
163 | )
164 |
165 | if (
166 | typeof value === 'string' ||
167 | ((value as unknown) instanceof Object &&
168 | options.returnObjects &&
169 | Object.keys(value).length > 0)
170 | ) {
171 | return value
172 | }
173 |
174 | if (Array.isArray(value) && options.returnObjects) return value
175 | return undefined
176 | }
177 |
178 | /**
179 | * Control plural keys depending the {{count}} variable
180 | */
181 | function plural(
182 | pluralRules: Intl.PluralRules,
183 | dic: I18nDictionary,
184 | key: string,
185 | config: I18nConfig,
186 | query?: TranslationQuery | null,
187 | options?: {
188 | returnObjects?: boolean
189 | fallback?: string | string[]
190 | }
191 | ): string {
192 | if (!query || typeof query.count !== 'number') return key
193 |
194 | const numKey = `${key}_${query.count}`
195 | if (getDicValue(dic, numKey, config, options) !== undefined) return numKey
196 |
197 | const pluralKey = `${key}_${pluralRules.select(query.count)}`
198 | if (getDicValue(dic, pluralKey, config, options) !== undefined) {
199 | return pluralKey
200 | }
201 |
202 | const nestedNumKey = `${key}.${query.count}`
203 | if (getDicValue(dic, nestedNumKey, config, options) !== undefined)
204 | return nestedNumKey
205 |
206 | const nestedKey = `${key}.${pluralRules.select(query.count)}`
207 | if (getDicValue(dic, nestedKey, config, options) !== undefined)
208 | return nestedKey
209 |
210 | return key
211 | }
212 |
213 | /**
214 | * Replace {{variables}} to query values
215 | */
216 | function interpolation({
217 | text,
218 | query,
219 | config,
220 | lang,
221 | }: {
222 | text?: string
223 | query?: TranslationQuery | null
224 | config: I18nConfig
225 | lang?: string | undefined
226 | }): string {
227 | if (!text || !query) return text || ''
228 |
229 | const escapeRegex = (str: string) =>
230 | str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
231 | const {
232 | format = null,
233 | prefix = '{{',
234 | suffix = '}}',
235 | } = config.interpolation || {}
236 |
237 | const regexEnd =
238 | suffix === '' ? '' : `(?:[\\s,]+([\\w-]*))?\\s*${escapeRegex(suffix)}`
239 | return Object.keys(query).reduce((all, varKey) => {
240 | const regex = new RegExp(
241 | `${escapeRegex(prefix)}\\s*${varKey}${regexEnd}`,
242 | 'gm'
243 | )
244 | // $1 is the first match group
245 | return all.replace(regex, (_match, $1) => {
246 | // $1 undefined can mean either no formatting requested: "{{name}}"
247 | // or no format name given: "{{name, }}" -> ignore
248 | return $1 && format
249 | ? (format(query[varKey], $1, lang) as string)
250 | : (query[varKey] as string)
251 | })
252 | }, text)
253 | }
254 |
255 | function objectInterpolation({
256 | obj,
257 | query,
258 | config,
259 | lang,
260 | }: {
261 | obj: Record
262 | query?: TranslationQuery | null
263 | config: I18nConfig
264 | lang?: string
265 | }): any {
266 | if (!query || Object.keys(query).length === 0) return obj
267 | Object.keys(obj).forEach((key) => {
268 | if (obj[key] instanceof Object)
269 | objectInterpolation({
270 | obj: obj[key] as Record,
271 | query,
272 | config,
273 | lang,
274 | })
275 | if (typeof obj[key] === 'string')
276 | obj[key] = interpolation({
277 | text: obj[key] as string,
278 | query,
279 | config,
280 | lang,
281 | })
282 | })
283 |
284 | return obj
285 | }
286 |
287 | function missingKeyLogger({ namespace, i18nKey }: LoggerProps): void {
288 | if (process.env.NODE_ENV === 'production') return
289 |
290 | // This means that instead of "ns:value", "value" has been misspelled (without namespace)
291 | if (!namespace) {
292 | console.warn(
293 | `[next-translate] The text "${i18nKey}" has no namespace in front of it.`
294 | )
295 | return
296 | }
297 | console.warn(
298 | `[next-translate] "${namespace}:${i18nKey}" is missing in current namespace configuration. Try adding "${i18nKey}" to the namespace "${namespace}".`
299 | )
300 | }
301 |
--------------------------------------------------------------------------------
/src/useTranslation.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useMemo } from 'react'
2 | import { I18n } from '.'
3 | import wrapTWithDefaultNs from './wrapTWithDefaultNs'
4 | import I18nContext from './context'
5 | import createTranslation from './createTranslation'
6 |
7 | function useTranslationInPages(defaultNS?: string): I18n {
8 | const ctx = useContext(I18nContext)
9 | return useMemo(
10 | () => ({
11 | ...ctx,
12 | t: wrapTWithDefaultNs(ctx.t, defaultNS),
13 | }),
14 | [ctx, defaultNS]
15 | )
16 | }
17 |
18 | export default function useTranslation(defaultNS?: string): I18n {
19 | const appDir = globalThis.__NEXT_TRANSLATE__
20 | const useT = appDir?.config ? createTranslation : useTranslationInPages
21 | return useT(defaultNS)
22 | }
23 |
--------------------------------------------------------------------------------
/src/withTranslation.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import useTranslation from './useTranslation'
3 | import { NextComponentType } from 'next'
4 |
5 | /**
6 | * HOC to use the translations in no-functional components
7 | */
8 | export default function withTranslation(
9 | Component: React.ComponentType
| NextComponentType,
10 | defaultNS?: string
11 | ): React.ComponentType> {
12 | const WithTranslation: NextComponentType = (props: P) => {
13 | const i18n = useTranslation(defaultNS)
14 | return
15 | }
16 |
17 | WithTranslation.getInitialProps = async (ctx) => {
18 | const WrappedComponent = Component as NextComponentType
19 | if (WrappedComponent.getInitialProps) {
20 | return (await WrappedComponent.getInitialProps(ctx)) || {}
21 | } else {
22 | return {}
23 | }
24 | }
25 |
26 | WithTranslation.displayName = `withTranslation(${
27 | Component.displayName || ''
28 | })`
29 |
30 | return WithTranslation
31 | }
32 |
--------------------------------------------------------------------------------
/src/wrapTWithDefaultNs.tsx:
--------------------------------------------------------------------------------
1 | import { Translate } from '.'
2 |
3 | export default function wrapTWithDefaultNs(
4 | oldT: Translate,
5 | ns?: string
6 | ): Translate {
7 | if (typeof ns !== 'string') return oldT
8 |
9 | // Use default namespace if namespace is missing
10 | const t: Translate = (key, query, options) => {
11 | return oldT(key, query, { ns, ...options })
12 | }
13 |
14 | return t
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig-cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "CommonJS",
5 | "outDir": "./lib/cjs",
6 | "declaration": false,
7 | "declarationDir": null
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "module": "ES2020",
5 | "target": "es5",
6 | "jsx": "react",
7 | "removeComments": true,
8 | "moduleResolution": "node",
9 | "esModuleInterop": true,
10 | "noImplicitAny": true,
11 | "noImplicitReturns": true,
12 | "noImplicitThis": true,
13 | "declaration": true,
14 | "lib": ["esnext", "dom"],
15 | "forceConsistentCasingInFileNames": true,
16 | "outDir": "./lib/esm",
17 | "declarationDir": "./"
18 | },
19 | "include": ["./src/**/*.ts", "./src/**/*.tsx"],
20 | "exclude": ["node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------