├── .babelrc.js ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── __test__ ├── API.spec.js ├── Base.spec.js ├── I18n.spec.js ├── Localize.spec.js ├── Translate.spec.js └── index.spec.js ├── eslint.config.mjs ├── example ├── example.js └── run.js ├── package.json ├── src ├── components │ ├── Base.js │ ├── I18n.js │ ├── Localize.js │ └── Translate.js ├── index.js └── lib │ ├── localize.js │ ├── settings.js │ ├── translate.js │ └── utils.js ├── tsconfig.types.json ├── types ├── index.d.ts ├── react-i18nify-tests.tsx └── tsconfig.json └── yarn.lock /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-react', '@babel/preset-env'], 3 | env: { 4 | es: { 5 | presets: ['@babel/preset-react', ['@babel/preset-env', { modules: false }]], 6 | }, 7 | cjs: { 8 | presets: ['@babel/preset-react', ['@babel/preset-env', {}]], 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [22] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: yarn --frozen-lockfile 27 | - run: yarn build 28 | - run: yarn prettier:check 29 | - run: yarn lint 30 | - run: yarn typecheck 31 | - run: yarn test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /npm-debug.log 3 | /build 4 | /es 5 | /cjs 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | cjs 3 | es 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "printWidth": 200, 6 | "useTabs": false, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "arrowParens": "always" 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sealninja 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React I18nify 2 | 3 | Simple i18n translation and localization components and helpers for React. 4 | 5 | [![NPM version](https://img.shields.io/npm/v/react-i18nify.svg?style=flat-square)](https://npmjs.org/package/react-i18nify) 6 | [![Downloads](https://img.shields.io/npm/dm/react-i18nify.svg?style=flat-square)](https://npmjs.org/package/react-i18nify) 7 | 8 | A working example of this package can be found [here at RunKit](https://runkit.com/npm/react-i18nify). 9 | 10 | ## Migration guide 11 | 12 | ### Upgrading to v6 13 | 14 | `react-i18nify` v6 uses `dayjs` for date localization instead of `date-fns`, to make `react-i18nify` smaller and simpler to use. Migrating to this version requires the following changes to your project: 15 | 16 | - Replace locale imports. E.g., `import nl from 'date-fns/locale/nl';` needs to be replaced with `import 'dayjs/locale/nl';` 17 | - Remove calls to `addLocale` and `addLocales`, these are not needed anymore. 18 | - Update date formatting strings. For example, `MM-dd-yyyy` is now `MM-DD-YYYY`. See for more information the [day.js documentation](https://day.js.org/docs/en/display/format). 19 | 20 | The v5 documentation can still be found [here](https://github.com/sealninja/react-i18nify/blob/v5/README.md). 21 | 22 | ## Installation 23 | 24 | Install by using npm: 25 | 26 | ``` 27 | npm i react-i18nify 28 | ``` 29 | 30 | ## Getting started 31 | 32 | Start by setting the translations and locale to be used: 33 | 34 | ```javascript 35 | import { setTranslations, setLocale } from 'react-i18nify'; 36 | 37 | setTranslations({ 38 | en: { 39 | application: { 40 | title: 'Awesome app with i18n!', 41 | hello: 'Hello, %{name}!' 42 | }, 43 | date: { 44 | long: 'MMMM do, yyyy' 45 | }, 46 | export: 'Export %{count} items', 47 | export_0: 'Nothing to export', 48 | export_1: 'Export %{count} item', 49 | two_lines:
Line 1
Line 2
50 | }, 51 | nl: { 52 | application: { 53 | title: 'Toffe app met i18n!', 54 | hello: 'Hallo, %{name}!' 55 | }, 56 | date: { 57 | long: 'd MMMM yyyy' 58 | }, 59 | export: 'Exporteer %{count} dingen', 60 | export_0: 'Niks te exporteren', 61 | export_1: 'Exporteer %{count} ding', 62 | two_lines:
Regel 1
Regel 2
63 | } 64 | }); 65 | 66 | setLocale('nl'); 67 | ``` 68 | 69 | Now you're all set up to unleash the power of `react-i18nify`! 70 | 71 | ## Components 72 | 73 | The easiest way to translate or localize in your React application is by using the `Translate` and `Localize` components: 74 | 75 | ```javascript 76 | import { Translate, Localize } from 'react-i18nify'; 77 | 78 | 79 | // => Toffe app met i18n! 80 | 81 | // => Hallo, Aad! 82 | 83 | // => Exporteer 1 ding 84 | 85 | // => Exporteer 2 dingen 86 | 87 | // =>
Regel 1
Regel 2
88 | 89 | 90 | // => 7 april 2016 91 | 92 | // => 3 september 2015 93 | 94 | // => 7 jaar geleden 95 | 96 | // => € 3,33 97 | ``` 98 | 99 | ## Helpers 100 | 101 | If for some reason, you cannot use the components, you can use the `translate` and `localize` helpers instead: 102 | 103 | ```javascript 104 | import { translate, localize } from 'react-i18nify'; 105 | 106 | translate('application.title'); 107 | // => Toffe app met i18n! 108 | translate('application.hello', { name: 'Aad' }); 109 | // => Hallo, Aad!' 110 | translate('export', { count: 0 }); 111 | // => Niks te exporteren 112 | translate('application.unknown_translation'); 113 | // => unknown_translation 114 | translate('application', { name: 'Aad' }); 115 | // => {hello: 'Hallo, Aad!', title: 'Toffe app met i18n!'} 116 | 117 | localize(1385856000000, { dateFormat: 'date.long' }); 118 | // => 1 december 2013 119 | localize(Math.PI, { maximumFractionDigits: 2 }); 120 | // => 3,14 121 | localize('huh', { dateFormat: 'date.long' }); 122 | // => null 123 | ``` 124 | 125 | If you want these helpers to be re-rendered automatically when the locale or translations change, you have to wrap them in a `` component using its `render` prop: 126 | 127 | ```javascript 128 | import { I18n, translate } from 'react-i18nify'; 129 | 130 | } />; 131 | ``` 132 | 133 | ## Date localization 134 | 135 | `react-i18nify` uses [day.js](https://github.com/iamkun/dayjs/) internally to handle date localization. To reduce the base bundle size, `day.js` localizations are not loaded by default. If you need date localization, you can manually import them. For a list of available locales, refer to the [day.js list of locales](https://github.com/iamkun/dayjs/tree/dev/src/locale). 136 | 137 | ```javascript 138 | import 'dayjs/locale/en'; 139 | import 'dayjs/locale/nl'; 140 | import 'dayjs/locale/it'; 141 | ``` 142 | 143 | ## API Reference 144 | 145 | ### `` 146 | 147 | React translate component, with the following props: 148 | 149 | - `value` (string) 150 | 151 | The translation key to translate. 152 | 153 | - Other props 154 | 155 | All other provided props will be used as replacements for the translation. 156 | 157 | ### `` 158 | 159 | React localize component, with the following props: 160 | 161 | - `value` (number|string|object) 162 | 163 | The number or date to localize. 164 | 165 | - `dateFormat` (string) 166 | 167 | The translation key for providing the format string. Only needed for localizing dates. 168 | For the full list of formatting tokens which can be used in the format string, see the [day.js documentation](https://day.js.org/docs/en/display/format). 169 | 170 | - `parseFormat` (string) 171 | 172 | An optional formatting string for parsing the value when localizing dates. 173 | For the full list of formatting tokens which can be used in the parsing string, see the [day.js documentation](https://day.js.org/docs/en/parse/string-format). 174 | 175 | - `options` (object) 176 | 177 | When localizing numbers, the localize component supports all options as provided by the Javascript built-in `Intl.NumberFormat` object. 178 | For the full list of options, see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat. 179 | 180 | ### `` 181 | 182 | React I18n wrapper component, with the following prop: 183 | 184 | - `render` (func) 185 | 186 | The return value of the provide function will be rendered and automatically re-render when the locale or translations change. 187 | 188 | ### `setLocale(locale, rerenderComponents = true)` 189 | 190 | The used locale can be set with this function. By default, changing the locale will re-render all components. 191 | This behavior can be prevented by providing `false` as a second argument. 192 | 193 | ### `getLocale()` 194 | 195 | Get the currently used locale. 196 | 197 | ### `setTranslations(translations, rerenderComponents = true)` 198 | 199 | The used translations can be set with this function. By default, changing the translations will re-render all components. 200 | This behavior can be prevented by providing `false` as a second argument. 201 | 202 | ### `getTranslations()` 203 | 204 | Get the currently used translations. 205 | 206 | ### `setLocaleGetter(fn)` 207 | 208 | Alternatively to using `setLocale`, you can provide a callback to return the locale with `setLocaleGetter`: 209 | 210 | ```javascript 211 | import { setLocaleGetter } from 'react-i18nify'; 212 | 213 | const localeFunction = () => 'nl'; 214 | 215 | setLocaleGetter(localeFunction); 216 | ``` 217 | 218 | ### `setTranslationsGetter(fn)` 219 | 220 | Alternatively to using `setTranslations`, you can provide a callback to return the translations with `setTranslationsGetter`: 221 | 222 | ```javascript 223 | import { setTranslationsGetter } from 'react-i18nify'; 224 | 225 | const translationsFunction = () => ({ 226 | en: { ... }, 227 | nl: { ... } 228 | }); 229 | 230 | setTranslationsGetter(translationsFunction); 231 | ``` 232 | 233 | ### `setHandleMissingTranslation(fn)` 234 | 235 | By default, when a translation is missing, the translation key will be returned in a slightly formatted way, 236 | as can be seen in the `translate('application.unknown_translation');` example above. 237 | You can however overwrite this behavior by setting a function to handle missing translations. 238 | 239 | ```javascript 240 | import { setHandleMissingTranslation, translate } from 'react-i18nify'; 241 | 242 | setHandleMissingTranslation((key, replacements, options, err) => `Missing translation: ${key}`); 243 | 244 | translate('application.unknown_translation'); 245 | // => Missing translation: application.unknown_translation 246 | ``` 247 | 248 | ### `setHandleFailedLocalization(fn)` 249 | 250 | By default, when a localization failed, `null` will be returned, 251 | as can be seen in the `localize('huh', { dateFormat: 'date.long' });` example above. 252 | You can however overwrite this behavior by setting a function to handle failed localizations. 253 | 254 | ```javascript 255 | import { setHandleFailedLocalization, localize } from 'react-i18nify'; 256 | 257 | setHandleFailedLocalization((value, options, err) => `Failed localization: ${value}`); 258 | 259 | localize('huh', { dateFormat: 'date.long' }); 260 | // => Failed localization: huh 261 | ``` 262 | 263 | ### `translate(key, replacements = {})` 264 | 265 | Helper function to translate a `key`, given an optional set of `replacements`. See the above Helpers section for examples. 266 | 267 | ### `localize(value, options)` 268 | 269 | Helper function to localize a `value`, given a set of `options`. See the above Helpers section for examples. 270 | 271 | For localizing dates, the `day.js` library is used. 272 | A `dateFormat` option can be used for providing a translation key with the format string. 273 | For the full list of formatting tokens which can be used in the format string, see the [day.js documentation](https://day.js.org/docs/en/display/format). 274 | Moreover, `parseFormat` option can be used for providing a formatting string for parsing the value. 275 | For the full list of formatting tokens which can be used in the parsing string, see the [day.js documentation](https://day.js.org/docs/en/parse/string-format). 276 | 277 | For number formatting, the localize helper supports all options as provided by the Javascript built-in `Intl.NumberFormat` object. 278 | For the full list of options, see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat. 279 | 280 | ### `t(key, replacements = {})` 281 | 282 | Alias for `translate`. 283 | 284 | ### `l(value, options)` 285 | 286 | Alias for `localize`. 287 | 288 | ### `forceComponentsUpdate()` 289 | 290 | This function can be called to force a re-render of all I18n components. 291 | 292 | ## Example application with SSR 293 | 294 | An example application with server-side rendering using features of `react-i18nify` can be found at https://github.com/sealninja/react-ssr-example. 295 | 296 | ## License 297 | 298 | MIT 299 | -------------------------------------------------------------------------------- /__test__/API.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, test, expect, beforeEach */ 2 | 3 | import dayjs from 'dayjs'; 4 | import 'dayjs/locale/nl'; 5 | import 'dayjs/locale/it'; 6 | import 'dayjs/locale/zh'; 7 | import 'dayjs/locale/en'; 8 | import 'dayjs/locale/en-gb'; 9 | import utc from 'dayjs/plugin/utc'; 10 | import timezonePlugin from 'dayjs/plugin/timezone'; 11 | import { getLocale, getTranslations, setLocale, setTranslations, setLocaleGetter, setTranslationsGetter, setHandleMissingTranslation, translate, localize, t, l } from '../src'; 12 | 13 | dayjs.extend(utc); 14 | dayjs.extend(timezonePlugin); 15 | 16 | describe('API', () => { 17 | describe('setLocale', () => { 18 | setLocale('zh'); 19 | const result = getLocale(); 20 | expect(result).toEqual('zh'); 21 | }); 22 | 23 | describe('setTranslations', () => { 24 | setTranslations({ 25 | en: { 26 | hello: 'Hello, %{name}!', 27 | }, 28 | }); 29 | const result = getTranslations(); 30 | expect(result).toEqual({ 31 | en: { 32 | hello: 'Hello, %{name}!', 33 | }, 34 | }); 35 | }); 36 | 37 | describe('setLocaleGetter', () => { 38 | setLocaleGetter(() => 'zh'); 39 | const result = getLocale(); 40 | expect(result).toEqual('zh'); 41 | }); 42 | 43 | describe('setLocaleGetter', () => { 44 | setTranslationsGetter(() => ({ 45 | en: { 46 | hello: 'Hello, %{name}!', 47 | }, 48 | })); 49 | const result = getTranslations(); 50 | expect(result).toEqual({ 51 | en: { 52 | hello: 'Hello, %{name}!', 53 | }, 54 | }); 55 | }); 56 | 57 | describe('setHandleMissingTranslation', () => { 58 | setHandleMissingTranslation((key) => `Missing translation: ${key}`); 59 | const result = t('application.unknown_translation'); 60 | expect(result).toEqual('Missing translation: application.unknown_translation'); 61 | }); 62 | 63 | describe('translate', () => { 64 | beforeEach(() => { 65 | setTranslations({ 66 | en: { 67 | application: { 68 | hello: 'Hello, %{name}!', 69 | empty: '', 70 | }, 71 | }, 72 | nl: { 73 | application: { 74 | hello: 'Hallo, %{name}!', 75 | empty: '', 76 | }, 77 | }, 78 | }); 79 | setLocale('en'); 80 | }); 81 | 82 | [translate, t].forEach((translateFunction) => { 83 | test('should support fallback locale', () => { 84 | setLocale('nl-argh'); 85 | const result1 = translateFunction('application.hello', { name: 'Aad' }); 86 | expect(result1).toEqual('Hallo, Aad!'); 87 | }); 88 | 89 | test('should handle dynamic placeholder', () => { 90 | const result1 = translateFunction('application.hello', { name: 'Aad' }); 91 | expect(result1).toEqual('Hello, Aad!'); 92 | 93 | const result2 = translateFunction('application.hello', { name: 'Piet' }); 94 | expect(result2).toEqual('Hello, Piet!'); 95 | }); 96 | 97 | test('should handle nested dynamic placeholder', () => { 98 | const result1 = translateFunction('application', { name: 'Aad' }); 99 | expect(result1).toEqual({ hello: 'Hello, Aad!', empty: '' }); 100 | 101 | const result2 = translateFunction('application', { name: 'Piet' }); 102 | expect(result2).toEqual({ hello: 'Hello, Piet!', empty: '' }); 103 | }); 104 | 105 | test('should handle empty translation', () => { 106 | const result1 = translateFunction('application.empty'); 107 | expect(result1).toEqual(''); 108 | }); 109 | 110 | test('should support providing locale', () => { 111 | const result1 = translateFunction('application.hello', { name: 'Aad' }, { locale: 'nl' }); 112 | expect(result1).toEqual('Hallo, Aad!'); 113 | }); 114 | }); 115 | }); 116 | 117 | describe('localize', () => { 118 | beforeEach(() => { 119 | setTranslations({ 120 | en: { 121 | dates: { 122 | short: 'MM-DD-YYYY', 123 | long: 'MMMM Do, YYYY', 124 | }, 125 | }, 126 | nl: { 127 | dates: { 128 | long: 'D MMMM YYYY', 129 | }, 130 | }, 131 | }); 132 | setLocale('en'); 133 | }); 134 | 135 | [localize, l].forEach((localizeFunction) => { 136 | test('should return null when locale invalid', () => { 137 | setLocale('argh'); 138 | const result = localizeFunction(1517774664107, { dateFormat: 'dates.long' }); 139 | expect(result).toEqual(null); 140 | }); 141 | 142 | test('should return null when locale not loaded', () => { 143 | setLocale('fr'); 144 | const result = localizeFunction('2014-30-12', { parseFormat: 'YYYY-DD-MM', dateFormat: 'dates.short' }); 145 | expect(result).toEqual(null); 146 | }); 147 | 148 | test('should return null when localization failed', () => { 149 | const result = localizeFunction('huh', { parseFormat: 'YYYY-DD-MM', dateFormat: 'dates.short' }); 150 | expect(result).toEqual(null); 151 | }); 152 | 153 | test('should support parseFormat', () => { 154 | const result = localizeFunction('2014-30-12', { parseFormat: 'YYYY-DD-MM', dateFormat: 'dates.short' }); 155 | expect(result).toEqual('12-30-2014'); 156 | }); 157 | 158 | test('should support providing locale', () => { 159 | const result = localizeFunction(1517774664107, { locale: 'nl', dateFormat: 'dates.long' }); 160 | expect(result).toEqual('4 februari 2018'); 161 | }); 162 | 163 | test('should support distance to now', () => { 164 | const result = localizeFunction(new Date(new Date().setFullYear(new Date().getFullYear() - 3)).getTime(), { locale: 'nl', dateFormat: 'distance-to-now' }); 165 | expect(result).toEqual('3 jaar geleden'); 166 | }); 167 | 168 | test('should support distance to now in days', () => { 169 | const result = localizeFunction(new Date(new Date().setHours(new Date().getHours() - 30)).getTime(), { locale: 'nl', dateFormat: 'distance-to-now' }); 170 | expect(result).toEqual('een dag geleden'); 171 | }); 172 | 173 | test('should support dayjs with custom timezone', () => { 174 | const result = localizeFunction(dayjs.utc('2022-07-01T03:00:00.000Z').tz('America/Chihuahua'), { locale: 'nl', dateFormat: 'DD MMM YYYY, HH:mm Z' }); 175 | expect(result).toEqual('30 jun 2022, 21:00 -06:00'); 176 | }); 177 | 178 | test('should return date when locale can fall back', () => { 179 | setLocale('nl-be'); 180 | const result = localizeFunction(1517774664107, { dateFormat: 'LL' }); 181 | expect(result).toEqual('4 februari 2018'); 182 | }); 183 | 184 | test('should return date when provided locale can fall back', () => { 185 | const result = localizeFunction(1517774664107, { locale: 'zh-tw', dateFormat: 'LL' }); 186 | expect(result).toEqual('2018年2月4日'); 187 | }); 188 | 189 | test('should return date for regional locale with region uppercase', () => { 190 | setLocale('en-GB'); 191 | const result = localizeFunction(1517774664107, { dateFormat: 'LL' }); 192 | expect(result).toEqual('4 February 2018'); 193 | }); 194 | 195 | test('should return date for regional locale with region lowercase', () => { 196 | setLocale('en-gb'); 197 | const result = localizeFunction(1517774664107, { dateFormat: 'LL' }); 198 | expect(result).toEqual('4 February 2018'); 199 | }); 200 | }); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /__test__/Base.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, test, expect */ 2 | 3 | import Base from '../src/components/Base'; 4 | 5 | describe('Base.jsx', () => { 6 | test('should export component', () => { 7 | expect(Base).toBeDefined(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /__test__/I18n.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, test, expect, beforeAll, beforeEach */ 2 | 3 | import React from 'react'; 4 | import { renderToString } from 'react-dom/server'; 5 | import { setLocale, setTranslations, t, I18n } from '../src'; 6 | 7 | describe('I18n.jsx', () => { 8 | beforeAll(() => { 9 | setTranslations({ 10 | en: { 11 | application: { 12 | title: 'Awesome app with i18n!', 13 | }, 14 | }, 15 | nl: { 16 | application: { 17 | title: 'Toffe app met i18n!', 18 | }, 19 | }, 20 | }); 21 | }); 22 | 23 | describe(' component', () => { 24 | beforeEach(() => { 25 | setLocale('en'); 26 | }); 27 | 28 | test('should handle locale switching for attributes', () => { 29 | const component = } />; 30 | 31 | expect(renderToString(component)).toEqual(''); 32 | setLocale('nl'); 33 | expect(renderToString(component)).toEqual(''); 34 | }); 35 | 36 | test('should handle locale switching for children', () => { 37 | const component = {t('application.title')}} />; 38 | 39 | expect(renderToString(component)).toEqual('Awesome app with i18n!'); 40 | setLocale('nl'); 41 | expect(renderToString(component)).toEqual('Toffe app met i18n!'); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /__test__/Localize.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, test, expect, beforeAll, beforeEach */ 2 | 3 | import React from 'react'; 4 | import 'dayjs/locale/nl'; 5 | import 'dayjs/locale/en'; 6 | import { renderToString } from 'react-dom/server'; 7 | import { setLocale, setTranslations, Localize } from '../src'; 8 | 9 | describe('Localize.jsx', () => { 10 | beforeAll(() => { 11 | setTranslations({ 12 | en: { 13 | date: 'MMMM Do, YYYY', 14 | }, 15 | nl: { 16 | date: 'D MMMM YYYY', 17 | }, 18 | }); 19 | }); 20 | 21 | describe(' component', () => { 22 | beforeEach(() => { 23 | setLocale('en'); 24 | }); 25 | test('should handle date localization', () => { 26 | const component = ; 27 | expect(renderToString(component)).toMatch('July 4th, 2016'); 28 | }); 29 | 30 | test('should handle NL date localization', () => { 31 | setLocale('nl'); 32 | const component = ; 33 | expect(renderToString(component)).toMatch('4 juli 2016'); 34 | }); 35 | 36 | test('should handle locale switching', () => { 37 | const component = ; 38 | expect(renderToString(component)).toMatch('July 4th, 2016'); 39 | setLocale('nl'); 40 | expect(renderToString(component)).toMatch('4 juli 2016'); 41 | }); 42 | 43 | test('should handle date localization with parseFormat', () => { 44 | const component = ; 45 | expect(renderToString(component)).toMatch('July 4th, 2016'); 46 | }); 47 | 48 | test('should handle number localization', () => { 49 | const component = ( 50 | 59 | ); 60 | expect(renderToString(component)).toMatch('$3.33'); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /__test__/Translate.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, test, expect, beforeAll, beforeEach */ 2 | 3 | import React from 'react'; 4 | import { renderToString } from 'react-dom/server'; 5 | import { setLocale, setTranslations, Translate } from '../src'; 6 | 7 | describe('Translate.jsx', () => { 8 | beforeAll(() => { 9 | setTranslations({ 10 | en: { 11 | application: { 12 | title: 'Awesome app with i18n!', 13 | hello: 'Hello, %{name}!', 14 | }, 15 | export: 'Export %{count} items', 16 | export_0: 'Nothing to export', 17 | export_1: 'Export %{count} item', 18 | }, 19 | nl: { 20 | application: { 21 | title: 'Toffe app met i18n!', 22 | }, 23 | }, 24 | }); 25 | }); 26 | 27 | describe(' component', () => { 28 | beforeEach(() => { 29 | setLocale('en'); 30 | }); 31 | 32 | test('should handle translation', () => { 33 | const component = ; 34 | expect(renderToString(component)).toMatch('Awesome app with i18n!'); 35 | }); 36 | 37 | test('should handle NL translation', () => { 38 | setLocale('nl'); 39 | const component = ; 40 | expect(renderToString(component)).toMatch('Toffe app met i18n!'); 41 | }); 42 | 43 | test('should handle locale switching', () => { 44 | const component = ; 45 | expect(renderToString(component)).toMatch('Awesome app with i18n!'); 46 | setLocale('nl'); 47 | expect(renderToString(component)).toMatch('Toffe app met i18n!'); 48 | }); 49 | 50 | test('should handle dynamic placeholder', () => { 51 | const component = ; 52 | expect(renderToString(component)).toMatch('Hello, Aad!'); 53 | }); 54 | 55 | test('should handle pluralization', () => { 56 | expect(renderToString()).toMatch('Nothing to export'); 57 | expect(renderToString()).toMatch('Export 1 item'); 58 | expect(renderToString()).toMatch('Export 4 items'); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /__test__/index.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, test, expect */ 2 | 3 | import { 4 | getLocale, 5 | setLocale, 6 | setLocaleGetter, 7 | getTranslations, 8 | setTranslations, 9 | setTranslationsGetter, 10 | setHandleMissingTranslation, 11 | setHandleFailedLocalization, 12 | translate, 13 | localize, 14 | t, 15 | l, 16 | forceComponentsUpdate, 17 | Translate, 18 | Localize, 19 | I18n, 20 | } from '../src'; 21 | 22 | describe('index.js', () => { 23 | const exportedFunctions = [ 24 | getLocale, 25 | setLocale, 26 | setLocaleGetter, 27 | getTranslations, 28 | setTranslations, 29 | setTranslationsGetter, 30 | setHandleMissingTranslation, 31 | setHandleMissingTranslation, 32 | setHandleFailedLocalization, 33 | translate, 34 | localize, 35 | t, 36 | l, 37 | forceComponentsUpdate, 38 | Translate, 39 | Localize, 40 | I18n, 41 | ]; 42 | 43 | exportedFunctions.forEach((exportedFunction) => { 44 | test(`should export ${exportedFunction.name} function`, () => { 45 | expect(exportedFunction).toBeDefined(); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-plugin-prettier'; 2 | import babelParser from '@babel/eslint-parser'; 3 | import path from 'node:path'; 4 | import { fileURLToPath } from 'node:url'; 5 | import js from '@eslint/js'; 6 | import { FlatCompat } from '@eslint/eslintrc'; 7 | import { fixupPluginRules, fixupConfigRules } from '@eslint/compat'; 8 | import importPlugin from 'eslint-plugin-import'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | const compat = new FlatCompat({ 13 | baseDirectory: __dirname, 14 | recommendedConfig: js.configs.recommended, 15 | allConfig: js.configs.all, 16 | }); 17 | 18 | export default [ 19 | ...fixupConfigRules(compat.extends('airbnb', 'airbnb/hooks')), 20 | ...compat.extends('prettier', 'plugin:prettier/recommended'), 21 | { 22 | plugins: { 23 | prettier, 24 | import: fixupPluginRules(importPlugin), 25 | }, 26 | 27 | languageOptions: { 28 | globals: { 29 | fetch: 'readonly', 30 | JSX: true, 31 | }, 32 | 33 | parser: babelParser, 34 | }, 35 | 36 | settings: { 37 | 'import/resolver': { 38 | node: {}, 39 | exports: {}, 40 | }, 41 | }, 42 | 43 | rules: { 44 | 'react/jsx-filename-extension': 'off', 45 | 'no-underscore-dangle': 'off', 46 | 'react/forbid-prop-types': 'off', 47 | 'react/jsx-fragments': 'off', 48 | 'import/no-named-as-default': 'off', 49 | 'import/no-named-as-default-member': 'off', 50 | }, 51 | }, 52 | ]; 53 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOMServer = require('react-dom/server'); 3 | require('dayjs/locale/nl'); 4 | require('dayjs/locale/en'); 5 | 6 | let ReactI18nfiy = null; 7 | 8 | try { 9 | ReactI18nfiy = require('react-i18nify'); 10 | } catch (e) { 11 | ReactI18nfiy = require('../cjs/index.js'); 12 | } 13 | 14 | const { setTranslations, setLocale, setHandleMissingTranslation, setHandleFailedLocalization, translate, localize, Translate, Localize, I18n } = ReactI18nfiy; 15 | 16 | setTranslations({ 17 | en: { 18 | application: { 19 | title: 'Awesome app with i18n!', 20 | hello: 'Hello, %{name}!', 21 | }, 22 | date: { 23 | long: 'MMMM do, yyyy', 24 | }, 25 | export: 'Export %{count} items', 26 | export_0: 'Nothing to export', 27 | export_1: 'Export %{count} item', 28 | }, 29 | nl: { 30 | application: { 31 | title: 'Toffe app met i18n!', 32 | hello: 'Hallo, %{name}!', 33 | }, 34 | date: { 35 | long: 'd MMMM yyyy', 36 | }, 37 | export: 'Exporteer %{count} dingen', 38 | export_0: 'Niks te exporteren', 39 | export_1: 'Exporteer %{count} ding', 40 | }, 41 | }); 42 | 43 | setLocale('nl'); 44 | 45 | console.log(translate('application.title')); 46 | console.log(translate('application.hello', { name: 'Aad' })); 47 | console.log(translate('export', { count: 0 })); 48 | console.log(translate('application', { name: 'Aad' })); 49 | 50 | console.log(localize(1385856000000, { dateFormat: 'date.long' })); 51 | console.log(localize(Math.PI, { maximumFractionDigits: 2 })); 52 | console.log(localize('huh', { dateFormat: 'date.long' })); 53 | 54 | setHandleMissingTranslation((key, replacements, options, err) => `Missing translation: ${key}`); 55 | 56 | console.log(translate('application.unknown_translation')); 57 | 58 | setHandleFailedLocalization((value, options, err) => `Failed localization: ${value}`); 59 | 60 | console.log(localize('huh', { dateFormat: 'date.long' })); 61 | 62 | function AwesomeComponent() { 63 | return ( 64 | 65 |

66 | 67 |

68 |
69 | 70 |
71 |
    72 |
  • 73 | 74 |
  • 75 |
  • 76 | 77 |
  • 78 |
79 |

80 | 81 |

82 |

83 | 84 |

85 |

86 | 95 |

96 |

97 | 98 |

99 | } /> 100 |
101 | ); 102 | } 103 | 104 | console.log(ReactDOMServer.renderToString()); 105 | -------------------------------------------------------------------------------- /example/run.js: -------------------------------------------------------------------------------- 1 | require('@babel/register'); 2 | require('./example'); 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-i18nify", 3 | "version": "6.2.0", 4 | "description": "Simple i18n translation and localization components and helpers for React.", 5 | "main": "./cjs/index.js", 6 | "module": "./es/index.js", 7 | "types": "./types/index.d.ts", 8 | "scripts": { 9 | "test": "jest --verbose", 10 | "test:watch": "npm test -- --watch", 11 | "lint": "eslint src/**/*.js __test__/**/*.js", 12 | "lint:fix": "npm run lint -- --fix", 13 | "typecheck": "tsc --project tsconfig.types.json --noEmit", 14 | "build": "rimraf cjs es && npx browserslist --update-db && NODE_ENV=cjs babel src -d cjs && NODE_ENV=es babel src -d es", 15 | "prepare": "npm run build", 16 | "prettier": "prettier --write --loglevel error \"**/*.+(js|jsx|json|yml|yaml|css|ts|tsx|md|mdx|html)\"", 17 | "prettier:check": "prettier --check \"**/*.+(js|jsx|json|yml|yaml|css|ts|tsx|md|mdx|html)\"" 18 | }, 19 | "files": [ 20 | "cjs", 21 | "es", 22 | "src", 23 | "example", 24 | "types/index.d.ts" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/sealninja/react-i18nify.git" 29 | }, 30 | "author": "Sealninja", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/sealninja/react-i18nify/issues" 34 | }, 35 | "homepage": "https://sealninja.com", 36 | "keywords": [ 37 | "react", 38 | "i18n", 39 | "translation", 40 | "localization", 41 | "components", 42 | "helpers", 43 | "javascript", 44 | "flux", 45 | "redux" 46 | ], 47 | "runkitExampleFilename": "example/example.js", 48 | "browserslist": "> 0.5%, last 2 versions, not op_mini all, not dead", 49 | "peerDependencies": { 50 | "dayjs": "^1.11.6", 51 | "react": "^16.8.0 || ^17.x || ^18.x || ^19.x" 52 | }, 53 | "dependencies": { 54 | "prop-types": "15.8.1" 55 | }, 56 | "devDependencies": { 57 | "@babel/cli": "7.27.1", 58 | "@babel/core": "7.27.4", 59 | "@babel/eslint-parser": "7.27.5", 60 | "@babel/preset-env": "7.27.1", 61 | "@babel/preset-react": "7.27.1", 62 | "@babel/register": "7.27.1", 63 | "@eslint/compat": "1.2.9", 64 | "@eslint/eslintrc": "3.3.1", 65 | "@eslint/js": "9.28.0", 66 | "@types/react": "19.1.6", 67 | "dayjs": "1.11.13", 68 | "eslint": "9.28.0", 69 | "eslint-config-airbnb": "19.0.4", 70 | "eslint-config-prettier": "10.1.5", 71 | "eslint-plugin-import": "2.31.0", 72 | "eslint-plugin-jsx-a11y": "6.10.2", 73 | "eslint-plugin-prettier": "5.4.1", 74 | "eslint-plugin-react": "7.37.5", 75 | "eslint-plugin-react-hooks": "5.2.0", 76 | "jest": "29.7.0", 77 | "prettier": "3.5.3", 78 | "react": "19.1.0", 79 | "react-dom": "19.1.0", 80 | "rimraf": "6.0.1", 81 | "typescript": "5.8.3" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/components/Base.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Base extends React.Component { 4 | static instances = []; 5 | 6 | static rerenderAll() { 7 | Base.instances.forEach((instance) => instance.forceUpdate()); 8 | } 9 | 10 | componentDidMount() { 11 | Base.instances.push(this); 12 | } 13 | 14 | componentWillUnmount() { 15 | Base.instances.splice(Base.instances.indexOf(this), 1); 16 | } 17 | } 18 | 19 | export const forceComponentsUpdate = () => { 20 | Base.rerenderAll(); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/I18n.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import BaseComponent from './Base'; 3 | 4 | class I18n extends BaseComponent { 5 | render = () => this.props.render(); 6 | } 7 | 8 | I18n.propTypes = { 9 | render: PropTypes.func.isRequired, 10 | }; 11 | 12 | export default I18n; 13 | -------------------------------------------------------------------------------- /src/components/Localize.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import localize from '../lib/localize'; 3 | import BaseComponent from './Base'; 4 | 5 | class Localize extends BaseComponent { 6 | render() { 7 | const { value, dateFormat, parseFormat, options = {} } = this.props; 8 | const localization = localize(value, { ...options, dateFormat, parseFormat }); 9 | 10 | return localization; 11 | } 12 | } 13 | 14 | Localize.propTypes = { 15 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]).isRequired, 16 | dateFormat: PropTypes.string, 17 | parseFormat: PropTypes.string, 18 | options: PropTypes.object, 19 | }; 20 | 21 | export default Localize; 22 | -------------------------------------------------------------------------------- /src/components/Translate.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import translate from '../lib/translate'; 3 | import BaseComponent from './Base'; 4 | 5 | class Translate extends BaseComponent { 6 | render() { 7 | const { value, locale, ...otherProps } = this.props; 8 | const translation = translate(value, otherProps, { locale }); 9 | 10 | return translation; 11 | } 12 | } 13 | 14 | Translate.propTypes = { 15 | value: PropTypes.string.isRequired, 16 | locale: PropTypes.string, 17 | }; 18 | 19 | export default Translate; 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { forceComponentsUpdate } from './components/Base.js'; 2 | export { default as Translate } from './components/Translate.js'; 3 | export { default as Localize } from './components/Localize.js'; 4 | export { default as I18n } from './components/I18n.js'; 5 | export { getLocale, setLocale, setLocaleGetter, getTranslations, setTranslations, setTranslationsGetter, setHandleMissingTranslation, setHandleFailedLocalization } from './lib/settings'; 6 | export { default as translate } from './lib/translate'; 7 | export { default as t } from './lib/translate'; 8 | export { default as localize } from './lib/localize'; 9 | export { default as l } from './lib/localize'; 10 | export { replace as translateReplace } from './lib/utils'; 11 | -------------------------------------------------------------------------------- /src/lib/localize.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import customParseFormat from 'dayjs/plugin/customParseFormat'; 3 | import advancedFormat from 'dayjs/plugin/advancedFormat'; 4 | import localizedFormat from 'dayjs/plugin/localizedFormat'; 5 | import relativeTime from 'dayjs/plugin/relativeTime'; 6 | 7 | import { getLocale, handleFailedLocalization } from './settings'; 8 | import translate from './translate'; 9 | 10 | dayjs.extend(customParseFormat); 11 | dayjs.extend(advancedFormat); 12 | dayjs.extend(localizedFormat); 13 | dayjs.extend(relativeTime); 14 | 15 | export default (value, options = {}) => { 16 | const locale = options.locale || getLocale(); 17 | 18 | if (options.dateFormat) { 19 | try { 20 | let dayJsLocale = locale.toLowerCase(); 21 | if (dayJsLocale === 'no') dayJsLocale = 'nb'; // Bokmål as default Norwegian 22 | 23 | const parsedDate = (options.parseFormat ? dayjs(value, translate(options.parseFormat, {}, { locale, returnKeyOnError: true }), dayJsLocale) : dayjs(value)).locale(dayJsLocale); 24 | if (!dayJsLocale.startsWith(parsedDate.locale())) throw new Error('Invalid locale'); 25 | 26 | if (!parsedDate.isValid()) throw new Error('Invalid date'); 27 | 28 | if (options.dateFormat === 'distance-to-now') { 29 | return parsedDate.fromNow(); 30 | } 31 | return parsedDate.format(translate(options.dateFormat, {}, { locale, returnKeyOnError: true })); 32 | } catch (err) { 33 | return handleFailedLocalization(value, options, err); 34 | } 35 | } 36 | if (typeof value === 'number') { 37 | try { 38 | let intlLocale = locale; 39 | if (intlLocale.toLowerCase() === 'ar') intlLocale = 'ar-EG'; // work-around for Chrome 40 | return new Intl.NumberFormat(intlLocale, options).format(value); 41 | } catch (err) { 42 | return handleFailedLocalization(value, options, err); 43 | } 44 | } 45 | return value; 46 | }; 47 | -------------------------------------------------------------------------------- /src/lib/settings.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: "off" */ 2 | 3 | import BaseComponent from '../components/Base'; 4 | 5 | const settings = { 6 | localeKey: 'en', 7 | translationsObject: {}, 8 | getTranslations: null, 9 | getLocale: null, 10 | handleMissingTranslation: (text) => text.split('.').pop(), 11 | handleFailedLocalization: () => null, 12 | }; 13 | 14 | export const getLocale = () => (settings.getLocale ? settings.getLocale() : settings.localeKey); 15 | 16 | export const setLocale = (locale, rerenderComponents = true) => { 17 | settings.localeKey = locale; 18 | settings.getLocale = null; 19 | if (rerenderComponents) { 20 | BaseComponent.rerenderAll(); 21 | } 22 | }; 23 | 24 | export const handleMissingTranslation = (...args) => settings.handleMissingTranslation(...args); 25 | export const handleFailedLocalization = (...args) => settings.handleFailedLocalization(...args); 26 | 27 | export const getTranslations = () => (settings.getTranslations ? settings.getTranslations() : settings.translationsObject); 28 | 29 | export const setTranslations = (translations, rerenderComponents = true) => { 30 | settings.translationsObject = translations; 31 | settings.getTranslations = null; 32 | if (rerenderComponents) { 33 | BaseComponent.rerenderAll(); 34 | } 35 | }; 36 | 37 | export const setLocaleGetter = (fn) => { 38 | if (typeof fn !== 'function') { 39 | throw new Error('Locale getter must be a function'); 40 | } 41 | settings.getLocale = fn; 42 | }; 43 | 44 | export const setTranslationsGetter = (fn) => { 45 | if (typeof fn !== 'function') { 46 | throw new Error('Translations getter must be a function'); 47 | } 48 | settings.getTranslations = fn; 49 | }; 50 | 51 | export const setHandleMissingTranslation = (fn) => { 52 | if (typeof fn !== 'function') { 53 | throw new Error('Handle missing translation must be a function'); 54 | } 55 | settings.handleMissingTranslation = fn; 56 | }; 57 | 58 | export const setHandleFailedLocalization = (fn) => { 59 | if (typeof fn !== 'function') { 60 | throw new Error('Handle failed localization must be a function'); 61 | } 62 | settings.handleFailedLocalization = fn; 63 | }; 64 | -------------------------------------------------------------------------------- /src/lib/translate.js: -------------------------------------------------------------------------------- 1 | import { fetchTranslation, replace } from './utils'; 2 | import { getLocale, getTranslations, handleMissingTranslation } from './settings'; 3 | 4 | export default (key, replacements = {}, options = {}) => { 5 | const locale = options.locale || getLocale(); 6 | let translation = ''; 7 | try { 8 | const translationLocale = getTranslations()[locale] ? locale : locale.split('-')[0]; 9 | translation = fetchTranslation(getTranslations(), `${translationLocale}.${key}`, replacements.count); 10 | } catch (err) { 11 | if (options.returnNullOnError) return null; 12 | if (options.returnKeyOnError) return key; 13 | return handleMissingTranslation(key, replacements, options, err); 14 | } 15 | return replace(translation, replacements); 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const replace = (translation, replacements) => { 4 | if (typeof translation === 'string') { 5 | let result = translation; 6 | Object.keys(replacements).forEach((replacement) => { 7 | result = result.split(`%{${replacement}}`).join(replacements[replacement] ?? ''); 8 | }); 9 | return result; 10 | } 11 | if (React.isValidElement(translation)) { 12 | return translation; 13 | } 14 | if (typeof translation === 'object') { 15 | const result = {}; 16 | Object.keys(translation).forEach((translationKey) => { 17 | result[translationKey] = replace(translation[translationKey], replacements); 18 | }); 19 | return result; 20 | } 21 | return null; 22 | }; 23 | 24 | export const fetchTranslation = (translations, key, count = null) => { 25 | const _index = key.indexOf('.'); 26 | if (typeof translations === 'undefined') { 27 | throw new Error('not found'); 28 | } 29 | if (_index > -1) { 30 | return fetchTranslation(translations[key.substring(0, _index)], key.substr(_index + 1), count); 31 | } 32 | if (count !== null) { 33 | if (translations[`${key}_${count}`]) { 34 | // when key = 'items_3' if count is 3 35 | return translations[`${key}_${count}`]; 36 | } 37 | if (count !== 1 && translations[`${key}_plural`]) { 38 | // when count is not simply singular, return _plural 39 | return translations[`${key}_plural`]; 40 | } 41 | } 42 | if (translations[key] != null) { 43 | return translations[key]; 44 | } 45 | throw new Error('not found'); 46 | }; 47 | -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "noEmit": true, 5 | "skipLibCheck": false, 6 | "forceConsistentCasingInFileNames": true, 7 | "types": ["react"], 8 | "moduleResolution": "bundler", 9 | "lib": ["dom", "esnext"], 10 | "target": "ESNext" 11 | }, 12 | "include": ["types/**/*.d.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 3.7 2 | import React = require('react'); 3 | 4 | export class I18n extends React.Component {} 5 | 6 | // Localization 7 | export type LocaleGetter = () => string; 8 | export function getLocale(): string | undefined; 9 | export function setLocale(locale: string, rerenderComponents?: boolean): void; 10 | export function setLocaleGetter(fn: LocaleGetter): void; 11 | 12 | export interface LocalizeDateOptions { 13 | locale?: string; 14 | parseFormat?: string; 15 | dateFormat?: string; 16 | } 17 | export type LocalizeNumberOptions = Intl.NumberFormatOptions; 18 | export function localize(value: string | number, options?: LocalizeDateOptions): string; 19 | export function localize(value: number, options?: LocalizeNumberOptions): string; 20 | export function l(value: string | number, options?: LocalizeDateOptions): string; 21 | export function l(value: number, options?: LocalizeNumberOptions): string; 22 | 23 | export type LocalizeDateProps = { 24 | value: string | number; 25 | } & LocalizeDateOptions; 26 | export type LocalizeNumberProps = { 27 | value: number; 28 | } & LocalizeNumberOptions; 29 | export class Localize extends React.Component {} 30 | 31 | // Translations 32 | export type Translations = Record; 33 | // The `count` key has some special behavior, so we need to support number. See src/lib/utils.js#L36 34 | // The value gets piped into `Array.join`, so the number will get coerced to a string. Should be ok. 35 | export type Replacements = Record; 36 | 37 | export type TranslationsGetter = () => Translations; 38 | export function getTranslations(): Translations | undefined; 39 | export function setTranslations(transations: Translations, rerenderComponents?: boolean): void; 40 | export function setTranslationsGetter(fn: TranslationsGetter): void; 41 | 42 | export type ReplacementsGetter = (key: string, replacements: Replacements) => string; 43 | export function setHandleMissingTranslation(fn: ReplacementsGetter): void; 44 | 45 | export interface TranslateOptions { 46 | locale?: string; 47 | returnNullOnError?: boolean; 48 | returnKeyOnError?: boolean; 49 | } 50 | 51 | export function translate(key: string, replacements?: Replacements, options?: TranslateOptions): string; 52 | export function t(key: string, replacements?: Replacements, options?: TranslateOptions): string; 53 | 54 | export type TranslateProps = { 55 | value: string; 56 | } & Replacements; 57 | export class Translate extends React.Component {} 58 | 59 | // Utility 60 | export function forceComponentsUpdate(): void; 61 | -------------------------------------------------------------------------------- /types/react-i18nify-tests.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | I18n, 4 | getLocale, 5 | setLocale, 6 | setLocaleGetter, 7 | localize, 8 | l, 9 | Localize, 10 | getTranslations, 11 | setTranslations, 12 | setTranslationsGetter, 13 | setHandleMissingTranslation, 14 | Replacements, 15 | translate, 16 | t, 17 | Translate, 18 | } from 'react-i18nify'; 19 | 20 | ; 21 | 22 | getLocale(); // $ExpectType string | undefined 23 | 24 | setLocale('en'); // $ExpectType void 25 | setLocale('en', true); // $ExpectType void 26 | setLocale(123); // $ExpectError 27 | setLocale('en', 123); // $ExpectError 28 | 29 | setLocaleGetter(() => 'en'); // $ExpectType void 30 | setLocaleGetter(() => 123); // $ExpectError 31 | 32 | localize('11-11-2019'); // $ExpectType string 33 | localize('11-11-2019', { dateFormat: 'mm/dd/YYYY', parseFormat: 'mm-dd-YYYY' }); // $ExpectType string 34 | localize(1234567890, { dateFormat: 'mm/dd/YYYY', parseFormat: 'mm-dd-YYYY' }); // $ExpectType string 35 | localize('11-11-2019', { maximumFractionDigits: 2 }); // $ExpectError 36 | 37 | localize(1234); // $ExpectType string 38 | localize(1234, { maximumFractionDigits: 2 }); // $ExpectType string 39 | 40 | localize(true); // $ExpectError 41 | 42 | l('11-11-2019'); // $ExpectType string 43 | l('11-11-2019', { dateFormat: 'mm/dd/YYYY', parseFormat: 'mm-dd-YYYY' }); // $ExpectType string 44 | l(1234567890, { dateFormat: 'mm/dd/YYYY', parseFormat: 'mm-dd-YYYY' }); // $ExpectType string 45 | l('11-11-2019', { maximumFractionDigits: 2 }); // $ExpectError 46 | 47 | l(1234); // $ExpectType string 48 | l(1234, { maximumFractionDigits: 2 }); // $ExpectType string 49 | 50 | l(true); // $ExpectError 51 | 52 | ; // $ExpectError 53 | ; 54 | ; 55 | ; 56 | ; 57 | 58 | // getTranslations(); // $ExpectType Translations | undefined 59 | 60 | setTranslations({ foo: 'bar', baz: { foo: 'bar' } }); // $ExpectType void 61 | setTranslations({ foo: 'bar' }, true); // $ExpectType void 62 | setTranslations('asdf'); // $ExpectError 63 | setTranslations({ foo: 'bar' }, 'asdf'); // $ExpectError 64 | 65 | setTranslationsGetter(() => ({ foo: 'bar' })); // $ExpectType void 66 | setTranslationsGetter(() => 'asdf'); // $ExpectError 67 | 68 | setHandleMissingTranslation(() => 'asdf'); // $ExpectType void 69 | setHandleMissingTranslation((key: string) => 'asdf'); // $ExpectType void 70 | // $ExpectType void 71 | setHandleMissingTranslation((key: string, replacements: Replacements) => 'asdf'); 72 | 73 | translate('foo.bar'); // $ExpectType string 74 | translate('foo.bar', { asdf: 'baz' }); // $ExpectType string 75 | translate('foo.bar', { count: 1234 }); // $ExpectType string 76 | translate('foo.bar', { asdf: true }); // $ExpectError 77 | t('foo.bar'); // $ExpectType string 78 | t('foo.bar', { asdf: 'baz' }); // $ExpectType string 79 | t('foo.bar', { count: 1234 }); // $ExpectType string 80 | t('foo.bar', { asdf: true }); // $ExpectError 81 | 82 | ; // $ExpectError 83 | ; 84 | ; 85 | ; 86 | ; // $ExpectError 87 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6"], 5 | "esModuleInterop": true, 6 | "noImplicitAny": true, 7 | "noImplicitThis": true, 8 | "strictNullChecks": true, 9 | "strictFunctionTypes": true, 10 | "noEmit": true, 11 | "baseUrl": ".", 12 | "jsx": "preserve", 13 | "paths": { "react-i18nify": ["."] } 14 | } 15 | } 16 | --------------------------------------------------------------------------------