├── .github ├── FUNDING.yml ├── renovate.json └── workflows │ ├── checks.yml │ └── release.yml ├── .gitignore ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── Consumer.md ├── Provider.md ├── Trans.md ├── context.md ├── createTranslations.md ├── useT.md └── withT.md ├── package.json ├── src ├── __stories__ │ ├── Trans.story.tsx │ ├── interpolations.story.tsx │ ├── precreated.story.tsx │ ├── readme.story.tsx │ ├── useT.story.tsx │ └── withT.story.tsx ├── createTranslations.ts ├── index.ts └── types.ts ├── tsconfig.json └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: streamich 2 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "lockFileMaintenance": { 5 | "enabled": true, 6 | "automerge": true 7 | }, 8 | "rangeStrategy": "replace", 9 | "postUpdateOptions": ["yarnDedupeHighest"], 10 | "packageRules": [ 11 | { 12 | "matchUpdateTypes": ["minor", "patch"], 13 | "matchCurrentVersion": "!/^0/", 14 | "automerge": true 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | checks: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [20.x] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | cache: yarn 22 | - run: yarn 23 | - run: yarn test 24 | - run: yarn build 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write # to be able to publish a GitHub release 12 | issues: write # to be able to comment on released issues 13 | pull-requests: write # to be able to comment on released pull requests 14 | id-token: write # to enable use of OIDC for npm provenance 15 | strategy: 16 | matrix: 17 | node-version: [20.x] 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: yarn 25 | - run: yarn 26 | - run: yarn test 27 | - run: yarn build 28 | - name: Semantic Release 29 | uses: cycjimmy/semantic-release-action@v4 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # build output 64 | lib/ 65 | 66 | .DS_Store 67 | 68 | src/parser.ts 69 | 70 | .cache/ 71 | .puppet-master/ 72 | 73 | storybook-static/ 74 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamich/use-t/fd7a24f60ebdc3aa91130c2667ca5f0659a7048a/.storybook/addons.js -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import {configure} from '@storybook/react'; 2 | 3 | const req = require.context('../src/__stories__/', true, /.*\.(stories|story)\.(js|jsx|ts|tsx)?$/); 4 | 5 | const loadStories = () => { 6 | req.keys().forEach((filename) => req(filename)); 7 | }; 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const SRC_PATH = path.join(__dirname, '../src'); 4 | 5 | module.exports = { 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.tsx?$/, 10 | loader: 'ts-loader', 11 | include: [ 12 | SRC_PATH, 13 | ] 14 | } 15 | ] 16 | }, 17 | 18 | resolve: { 19 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 20 | enforceExtension: false 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.6.2](https://github.com/streamich/use-/compare/v1.6.1...v1.6.2) (2019-11-22) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * 🐛 improve TypeScript typings of withT() HOC ([f463493](https://github.com/streamich/use-/commit/f463493)) 7 | 8 | ## [1.6.1](https://github.com/streamich/use-/compare/v1.6.0...v1.6.1) (2019-08-26) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * 🐛 interpolate to React fragments ([6604875](https://github.com/streamich/use-/commit/6604875)) 14 | 15 | # [1.6.0](https://github.com/streamich/use-/compare/v1.5.0...v1.6.0) (2019-04-21) 16 | 17 | 18 | ### Features 19 | 20 | * 🎸 allow to change locale through props ([31e91b5](https://github.com/streamich/use-/commit/31e91b5)) 21 | 22 | # [1.5.0](https://github.com/streamich/use-/compare/v1.4.0...v1.5.0) (2019-04-05) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * 🐛 export TS typings, fix withT() typings ([190332a](https://github.com/streamich/use-/commit/190332a)) 28 | 29 | 30 | ### Features 31 | 32 | * 🎸 use useT inside , translate inline text ([a391a66](https://github.com/streamich/use-/commit/a391a66)) 33 | 34 | # [1.4.0](https://github.com/streamich/use-/compare/v1.3.0...v1.4.0) (2019-01-08) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * 🐛 translate only strings in list ([f1e5ed6](https://github.com/streamich/use-/commit/f1e5ed6)) 40 | 41 | 42 | ### Features 43 | 44 | * 🎸 allow non-functional children in component ([aa8ae78](https://github.com/streamich/use-/commit/aa8ae78)) 45 | * 🎸 use simple t() in trans list children ([5cbcb57](https://github.com/streamich/use-/commit/5cbcb57)) 46 | 47 | # [1.3.0](https://github.com/streamich/use-/compare/v1.2.0...v1.3.0) (2018-12-21) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * 🐛 export correctly default translation function ([48266d5](https://github.com/streamich/use-/commit/48266d5)) 53 | 54 | 55 | ### Features 56 | 57 | * 🎸 export default translation function as T ([0ac3429](https://github.com/streamich/use-/commit/0ac3429)) 58 | * 🎸 implement FaCC and HOC without hooks ([7956b3d](https://github.com/streamich/use-/commit/7956b3d)) 59 | 60 | # [1.2.0](https://github.com/streamich/use-/compare/v1.1.1...v1.2.0) (2018-12-01) 61 | 62 | 63 | ### Features 64 | 65 | * allow 0 to indicate it should not be translated ([9633ef5](https://github.com/streamich/use-/commit/9633ef5)) 66 | 67 | ## [1.1.1](https://github.com/streamich/use-/compare/v1.1.0...v1.1.1) (2018-12-01) 68 | 69 | 70 | ### Bug Fixes 71 | 72 | * 🐛 load initial language if not def and in loader ([a27b47f](https://github.com/streamich/use-/commit/a27b47f)) 73 | 74 | # [1.1.0](https://github.com/streamich/use-/compare/v1.0.0...v1.1.0) (2018-12-01) 75 | 76 | 77 | ### Features 78 | 79 | * 🎸 precreate React primitives ([5eda1ea](https://github.com/streamich/use-/commit/5eda1ea)) 80 | 81 | # 1.0.0 (2018-11-06) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * 🐛 load correct locale, if async locale changed meantime ([560c770](https://github.com/streamich/use-/commit/560c770)) 87 | * 🐛 make sure default locale exists in translation map ([c694f65](https://github.com/streamich/use-/commit/c694f65)) 88 | 89 | 90 | ### Features 91 | 92 | * 🎸 add namespaces ([dcf24b5](https://github.com/streamich/use-/commit/dcf24b5)) 93 | * 🎸 don't provide interpolation a translation function ([e1b9697](https://github.com/streamich/use-/commit/e1b9697)) 94 | * 🎸 fallback to default locale if no translation found ([e0c58b6](https://github.com/streamich/use-/commit/e0c58b6)) 95 | * 🎸 improve component ([ce0460b](https://github.com/streamich/use-/commit/ce0460b)) 96 | * 🎸 improve interpolations, add tagged literals support ([9f2d6b8](https://github.com/streamich/use-/commit/9f2d6b8)) 97 | * 🎸 improve typings ([e9e0cb6](https://github.com/streamich/use-/commit/e9e0cb6)) 98 | * 🎸 improve useT, allow usage without ([d143fce](https://github.com/streamich/use-/commit/d143fce)) 99 | * 🎸 improve withT HOC ([09acebd](https://github.com/streamich/use-/commit/09acebd)) 100 | * 🎸 initial implementation ([f9a1fa9](https://github.com/streamich/use-/commit/f9a1fa9)) 101 | * 🎸 store everything in context ([34a5ad1](https://github.com/streamich/use-/commit/34a5ad1)) 102 | 103 | 104 | ### BREAKING CHANGES 105 | 106 | * Change how interpolations work. 107 | * interpolations don't receive translation function anymore 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 |
4 |
5 | 🗺 6 |
7 | use-t 8 |

9 | Translations for React. 10 |
11 |
12 |
13 |
14 |
15 | 16 | 17 |

Installation

18 |
19 |
npm i use-t
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |

Reference

28 | 29 | 30 | ```js 31 | import {Provider, useT, withT, Trans, Consumer, context} from 'use-t'; 32 | ``` 33 | 34 | - [``](./docs/Provider.md) 35 | - [`useT()`](./docs/useT.md) 36 | - [`withT()`](./docs/withT.md) 37 | - [``](./docs/Trans.md) 38 | - [``](./docs/Consumer.md) 39 | - [`context`](./docs/context.md) 40 | - [`createTranslations()`](./docs/createTranslations.md) 41 | 42 | 43 |
44 |
45 | 46 | 47 |

Example

48 | 49 | ```jsx 50 | import {Provider, useT} from 'use-t'; 51 | 52 | const Hello = (props) => { 53 | const [t] = useT(); 54 | return ( 55 |
56 |
57 | {t('Hello')}, {props.name}! 58 |
59 |
60 | {t.t('hello_user')`Hello, ${props.name}!`} 61 |
62 |
63 | ); 64 | }; 65 | 66 | `Hi, ${name}!` 71 | } 72 | } 73 | }}> 74 | 75 | 76 | ``` 77 | 78 | 79 |
80 |
81 | 82 | 83 |

License

84 | 85 |

86 | Unlicense — public domain. 87 |

88 | -------------------------------------------------------------------------------- /docs/Consumer.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | React context consumer FaCC that will fetch state of [``](./Provider.md). 4 | 5 | ```jsx 6 | {state => /*...*/} 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/Provider.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | Context provider which keeps track of all translations and provides 4 | methods to change language and to load more translations. Wrap your application 5 | with this component. 6 | 7 | ```jsx 8 | 9 | 10 | 11 | ``` 12 | 13 | 14 | ## Props 15 | 16 | ```jsx 17 | // Preloaded translation map. 18 | const preloaded = { 19 | en: { 20 | main: { 21 | Hello: 'Hello', 22 | welcome: 'Welcome!', 23 | }, 24 | }, 25 | }; 26 | 27 | { /*...*/ }} 33 | > 34 | 35 | 36 | ``` 37 | 38 | 39 | ### Reference 40 | 41 | - `locale` — initial selected locale, defaults to `'en'`. 42 | - `defaultLocale` — locale to fall-back to, if translation is not found in current locale, defaults to `'en'`. 43 | - `ns` — default namespace, defaults to `'main'`. 44 | - `map` — collection of initial preloaded translations, in format `{locale: {namespace: {key: value}}}`. 45 | - `loader` — method to be called when new translations are loaded, receives two arguments: 46 | locale and namespace; should return a promise that resolves to `{key: value}` map. 47 | 48 | 49 | ## State 50 | 51 | `` component provides its state using React context. The state object has the following attributes. 52 | 53 | - `locale` — currently selected locale. 54 | - `map` — map of all translations in `{locale: {namespace: {key: value}}}` form. 55 | - `load` — function that can be called to preload translations `await load(locale, namespace)`. 56 | - `setLocale` — function to change current locale `setLocale('de')` 57 | - `createT` — factory that creates a translation function `t` given namespaces `createT(['main'])`. 58 | -------------------------------------------------------------------------------- /docs/Trans.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | [![React Universal Interface](https://img.shields.io/badge/React-Universal%20Interface-green.svg)](https://github.com/streamich/react-universal-interface) 4 | 5 | A render-prop that provides [*Universal Interface*](https://github.com/streamich/react-universal-interface). 6 | It provides an object with translation function `t` and translation state `T`. 7 | 8 | ```jsx 9 | {({t, T}) => 10 | {t('Hello')}, user! {t('welcome')} 11 |
12 | 13 | 14 | }
15 | ``` 16 | 17 | It also accepts pure strings. 18 | 19 | ```jsx 20 | Hello 21 | ``` 22 | 23 | Or a mix of strings and functions. 24 | 25 | ```jsx 26 | {t => t('Hello')}! 27 | ``` 28 | 29 | 30 | ## Props 31 | 32 | - `ns` — a namespace or an array of namespaces, defaults to `'main'`. 33 | -------------------------------------------------------------------------------- /docs/context.md: -------------------------------------------------------------------------------- 1 | # `context` 2 | 3 | React context object `{Provider, Consumer}` used in [``](./Provider.md). 4 | -------------------------------------------------------------------------------- /docs/createTranslations.md: -------------------------------------------------------------------------------- 1 | # `createTranslations()` 2 | 3 | Creates translation context, hook, higher order component, and render prop that you 4 | can use together to manage translations in your app. 5 | 6 | ```js 7 | import createTranslations from 'use-t/lib/createTranslations'; 8 | 9 | const { 10 | Provider, 11 | useT, 12 | withT, 13 | Trans, 14 | Consumer, 15 | context, 16 | } = createTranslations(); 17 | ``` 18 | 19 | Wrap your app in [``](./Provider.md) component. 20 | 21 | ```jsx 22 | const preloaded = { 23 | en: { 24 | main: { 25 | Hello: 'Hello', 26 | } 27 | }, 28 | fr: { 29 | main: { 30 | Hello: 'Bonjour', 31 | } 32 | }, 33 | }; 34 | 35 | 36 | 37 | 38 | ``` 39 | 40 | Now, in your components use [`useT`](./useT.md), [`withT`](./withT.md), or [``](./Trans.md) to translate your app. 41 | 42 | ```jsx 43 | const Hello = (props) => { 44 | const [t] = useT(); 45 | return ( 46 |
47 | {t('Hello')}, {props.name}! 48 |
49 | ); 50 | }; 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/useT.md: -------------------------------------------------------------------------------- 1 | # `useT` 2 | 3 | React hook that returns you a translation function and state of the [``](./Provider.md). 4 | 5 | ```js 6 | const [t, state] = useT(); 7 | ``` 8 | 9 | You can specify a namespace or a list of namespaces to be used to create the translation function `t`. 10 | 11 | ```js 12 | useT('errors'); 13 | useT(['main', 'errors']); 14 | ``` 15 | 16 | If you don't specify the namespace(s), the default namespace will be used. 17 | 18 | 19 | ## Translation Function `t` 20 | 21 | `t` function can be used to translate texts. 22 | 23 | ```jsx 24 |
{t('Hello')}
25 | ``` 26 | 27 | Your translation can be functions. 28 | 29 | ```jsx 30 | `You have ${num} likes.`}}}} /> 31 | 32 |
{t('you_have_likes', 5)}
33 | // You have 5 likes. 34 | ``` 35 | 36 | If your translations are functions you can also use tagged template literals. 37 | 38 | ```jsx 39 | `Hi, ${name}!`}}}} /> 40 | 41 |
{t.t('hello_user')`Hello, ${props.name}!`}
42 | // Hi, ! 43 | ``` 44 | 45 | In this case, if translation was not found, it would return `Hi, !`. 46 | 47 | 48 | ## Context State `state` 49 | 50 | `state` is the state provided by [``](./Provider.md). 51 | 52 | Change current locale. 53 | 54 | ```js 55 | state.setLocale('fr'); 56 | ``` 57 | 58 | Pre-load translations. 59 | 60 | ```js 61 | await state.load('fr', 'error'); 62 | ``` 63 | 64 | `useT` will also work when `` is not wrapper around your React tree. It is useful 65 | when you want to just design presentational components. 66 | 67 | 68 | ### Reference 69 | 70 | ```js 71 | state.setLocale(locale); 72 | state.load(locale, namespace); 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/withT.md: -------------------------------------------------------------------------------- 1 | # `withT` 2 | 3 | React higher order component that injects `t` translation function and [`T` state of the ``](./Provider.md#state) 4 | into the props of your component. 5 | 6 | ```jsx 7 | const Demo = ({t, T}) => { 8 | return ( 9 |
10 | {t('Hello')}, user! {t('welcome')} 11 |
12 | 13 | 14 |
15 | ); 16 | }; 17 | 18 | export default withT(Demo); 19 | ``` 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-t", 3 | "version": "1.6.2", 4 | "description": "React translations", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib/" 8 | ], 9 | "types": "lib/index.d.ts", 10 | "typings": "lib/index.d.ts", 11 | "scripts": { 12 | "start": "yarn storybook", 13 | "test": "echo hmm...", 14 | "build": "tsc", 15 | "clean": "rimraf lib storybook-static", 16 | "storybook": "start-storybook -p 6008", 17 | "storybook:build": "build-storybook", 18 | "storybook:upload": "gh-pages -d storybook-static", 19 | "storybook:clean": "rimraf storybook-static", 20 | "release": "semantic-release" 21 | }, 22 | "author": "@streamich", 23 | "license": "Unlicense", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/streamich/use-t" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/streamich/use-t/issues" 30 | }, 31 | "homepage": "https://github.com/streamich/use-t#readme", 32 | "devDependencies": { 33 | "@babel/core": "^7.4.3", 34 | "@semantic-release/changelog": "^3.0.1", 35 | "@semantic-release/git": "^7.0.5", 36 | "@semantic-release/npm": "^5.0.5", 37 | "@storybook/react": "^4.0.2", 38 | "@types/node": "^11.13.0", 39 | "@types/react": "^16.8.12", 40 | "babel-core": "^6.26.3", 41 | "babel-loader": "^8.0.4", 42 | "react": "^16.8.6", 43 | "react-dom": "^16.8.6", 44 | "rimraf": "^2.6.2", 45 | "ts-loader": "^5.3.3", 46 | "ts-node": "^8.0.3", 47 | "tslib": "^2.7.0", 48 | "typescript": "^3.7.2" 49 | }, 50 | "peerDependencies": { 51 | "react": "*", 52 | "react-dom": "*" 53 | }, 54 | "config": { 55 | "commitizen": { 56 | "path": "git-cz" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/__stories__/Trans.story.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | import createTranslations from '../createTranslations'; 4 | 5 | const {Provider, Trans} = createTranslations(); 6 | 7 | const Demo: React.SFC<{t: any, T: any}> = ({t, T}) => { 8 | return ( 9 |
10 | {t('Hello')}, user! {t('welcome')} 11 |
12 | 13 | 14 |
15 | ); 16 | }; 17 | 18 | storiesOf('Trans', module) 19 | .add('Switch preloaded translations as FaCC', () => 20 | 28 | {(t, T) => 29 | 30 | } 31 | Hello 32 | yo {'welcome'} ... {t => t('welcome')}! 33 | 34 | ) 35 | .add('Switch preloaded translations as "children" render prop', () => 36 | 44 | 45 | 46 | } /> 47 | 48 | ) 49 | .add('Missing language', () => 50 | 55 | {(t, T) => 56 | 57 | } 58 | 59 | ) 60 | .add('Load translations dynamically, 2 sec delay', () => 61 | new Promise(resolve => { 68 | setTimeout(() => { 69 | resolve({Hello: 'Bonjour', welcome: 'Lala!'}); 70 | }, 2000); 71 | })} 72 | > 73 | {(t, T) => 74 | 75 | } 76 | 77 | ) 78 | .add('Without provider', () => 79 | {(t, T) => 80 | 81 | } 82 | ) 83 | .add('Text as children', () => 84 | 92 |
93 | ( 94 | <> 95 | 96 | 97 | 98 | )} /> 99 | Hello, user! welcome 100 |
101 |
102 | ) 103 | -------------------------------------------------------------------------------- /src/__stories__/interpolations.story.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | import createTranslations from '../createTranslations'; 4 | 5 | const {Provider, useT} = createTranslations(); 6 | 7 | const Hello = (props) => { 8 | const [t] = useT(); 9 | return ( 10 |
11 | {t('Hello')}, {props.name}! {t('you_have_likes', 3)} 12 |
13 | {t.t('hello_user')`Hello, ${props.name}!`} 14 |
15 |
16 | {t.t('missing')`Hello, ${props.name}!`} 17 |
18 |
19 | Some text: 20 |

21 | {t.t('login_footer_text')` 22 | By signing-in above, you acknowledge that you have read and 23 | understood, and agree to our ${{t('Terms of Use')}} 24 | and ${{t('Privacy Policy')}}.`} 25 |

26 |
27 |
28 | No translation: 29 |

30 | {t.t('no_translation')` 31 | By signing-in above, you acknowledge that you have read and 32 | understood, and agree to our ${{t('Terms of Use')}} 33 | and ${{t('Privacy Policy')}}.`} 34 |

35 |
36 |
37 | ); 38 | }; 39 | 40 | storiesOf('Interpolations', module) 41 | .add('Example', () => 42 | t`You have ${0} likes.`, 47 | hello_user: t => t`Hi, ${0}!`, 48 | login_footer_text: t => t`Basically, you agree to: ${0} and ${1}.`, 49 | } 50 | } 51 | }}> 52 | 53 | 54 | ); 55 | -------------------------------------------------------------------------------- /src/__stories__/precreated.story.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | import {Provider, useT} from '..'; 4 | 5 | const Demo = () => { 6 | const [t, {setLocale}] = useT(); 7 | return ( 8 |
9 | {t('Hello')}, user! {t('welcome')} 10 |
11 | 12 | 13 |
14 | ); 15 | }; 16 | 17 | storiesOf('useT precreated', module) 18 | .add('Switch preloaded translations', () => 19 | 27 | 28 | 29 | ) 30 | .add('Missing language', () => 31 | 36 | 37 | 38 | ) 39 | .add('Load translations dynamically, 2 sec delay', () => 40 | new Promise(resolve => { 47 | setTimeout(() => { 48 | resolve({Hello: 'Bonjour', welcome: 'Lala!'}); 49 | }, 2000); 50 | })} 51 | > 52 | 53 | 54 | ) 55 | .add('Without provider', () => 56 | 57 | ) 58 | -------------------------------------------------------------------------------- /src/__stories__/readme.story.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | import createTranslations from '../createTranslations'; 4 | 5 | const {Provider, useT} = createTranslations(); 6 | 7 | const Hello = (props) => { 8 | const [t] = useT(); 9 | return ( 10 |
11 |
12 | {t('Hello')}, {props.name}! 13 |
14 |
15 | {t.t('hello_user')`Hello, ${props.name}!`} 16 |
17 |
18 | ); 19 | }; 20 | 21 | storiesOf('Readme', module) 22 | .add('Example', () => 23 | `Hi, ${name}!`}}}}> 24 | 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /src/__stories__/useT.story.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | import createTranslations from '../createTranslations'; 4 | 5 | const {Provider, useT, withT} = createTranslations(); 6 | 7 | const Demo = () => { 8 | const [t, {setLocale}] = useT(); 9 | return ( 10 |
11 | {t('Hello')}, user! {t('welcome')} 12 |
13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | storiesOf('useT', module) 20 | .add('Switch preloaded translations', () => 21 | 29 | 30 | 31 | ) 32 | .add('Missing language', () => 33 | 38 | 39 | 40 | ) 41 | .add('Load translations dynamically, 2 sec delay', () => 42 | new Promise(resolve => { 49 | setTimeout(() => { 50 | resolve({Hello: 'Bonjour', welcome: 'Lala!'}); 51 | }, 2000); 52 | })} 53 | > 54 | 55 | 56 | ) 57 | .add('Without provider', () => 58 | 59 | ) 60 | .add('Initial language not default, loaded with loader', () => 61 | { 70 | return {Hello: 'Bonjour', welcome: 'Lala!'}; 71 | }} 72 | > 73 | 74 | 75 | ) 76 | -------------------------------------------------------------------------------- /src/__stories__/withT.story.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | import createTranslations from '../createTranslations'; 4 | 5 | const {Provider, withT} = createTranslations(); 6 | 7 | const Demo_: React.SFC<{t: any, T: any}> = ({t, T}) => { 8 | return ( 9 |
10 | {t('Hello')}, user! {t('welcome')} 11 |
12 | 13 | 14 |
15 | ); 16 | }; 17 | 18 | const Demo = withT(Demo_); 19 | 20 | storiesOf('withT', module) 21 | .add('Switch preloaded translations', () => 22 | 30 | 31 | 32 | ) 33 | .add('Missing language', () => 34 | 39 | 40 | 41 | ) 42 | .add('Load translations dynamically', () => 43 | Promise.resolve({Hello: 'Bonjour', welcome: 'Lala!'})} 50 | > 51 | 52 | 53 | ) 54 | .add('Without provider', () => 55 | 56 | ) 57 | -------------------------------------------------------------------------------- /src/createTranslations.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {ProviderProps, ProviderState, TransProps, Result, UseT, TranslatorFn, WithT} from './types'; 3 | 4 | const {createContext, createElement, Fragment} = React; 5 | 6 | const defaultInterpolate = (strs: TemplateStringsArray, ...args: React.ReactNode[]) => { 7 | const list: React.ReactNode[] = []; 8 | let i = 0; 9 | for (; i < args.length; i++) { 10 | list.push(strs![i]); 11 | list.push(args[i]); 12 | } 13 | list.push(strs![i]); 14 | return createElement(Fragment, {}, ...list); 15 | }; 16 | 17 | const translationInterpolate = (values: React.ReactNode[]) => (strs: TemplateStringsArray, ...args: number[]) => { 18 | const list: React.ReactNode[] = []; 19 | let i = 0; 20 | for (; i < args.length; i++) { 21 | list.push(strs![i]); 22 | list.push(values[args[i]]); 23 | } 24 | list.push(strs![i]); 25 | return createElement(Fragment, {}, ...list); 26 | }; 27 | 28 | export const createTranslations = (ns: string = 'main'): Result => { 29 | const context = createContext({} as any); 30 | const {Consumer} = context; 31 | const Provider = class extends React.Component { 32 | static defaultProps = { 33 | locale: 'en', 34 | defaultLocale: 'en', 35 | ns, 36 | }; 37 | 38 | state: ProviderState; 39 | 40 | constructor (props) { 41 | super(props); 42 | const {map = {}, locale, defaultLocale, ns} = props; 43 | 44 | // Normalize translation map. 45 | if (!map[defaultLocale]) map[defaultLocale] = {[ns]: {}}; 46 | else if (!map[defaultLocale][ns]) map[defaultLocale][ns] = {}; 47 | 48 | this.state = { 49 | locale, 50 | ns, 51 | map, 52 | load: this.load, 53 | setLocale: this.setLocale, 54 | createT: this.createT, 55 | }; 56 | 57 | if (locale !== defaultLocale) { 58 | this.load(locale, ns); 59 | } 60 | } 61 | 62 | shouldComponentUpdate (nextProps) { 63 | if (nextProps.locale !== this.props.locale) { 64 | this.setLocale(nextProps.locale); 65 | } 66 | return true; 67 | }; 68 | 69 | load = async (locale: string, ns: string) => { 70 | if (!this.state.map[locale]) { 71 | this.state.map[locale] = {}; 72 | } 73 | if (!this.state.map[locale][ns]) { 74 | this.state.map[locale][ns] = {}; 75 | this.setState({...this.state}); 76 | const translations = await this.props.loader!(locale, ns); 77 | this.state.map[locale][ns] = translations; 78 | this.setState({...this.state}); 79 | } 80 | }; 81 | 82 | setLocale = (locale: string) => { 83 | if (locale === this.state.locale) return; 84 | if (!this.state.map[locale]) 85 | this.state.map[locale] = {}; 86 | this.setState({ 87 | locale, 88 | }); 89 | }; 90 | 91 | createT = (nss: string[] = [this.props.ns as string]): TranslatorFn => { 92 | const {locale} = this.state; 93 | const translationsNamespaced = this.state.map[locale]; 94 | for (const ns of nss) { 95 | if (!translationsNamespaced[ns]) { 96 | this.load(locale, ns).catch(err => console.error(err)); 97 | } 98 | } 99 | 100 | const t: TranslatorFn = ((key: string, ...args: any[]) => { 101 | for (const currentLocale of [locale, this.props.defaultLocale]) { 102 | if (!currentLocale) break; 103 | const translationsNamespaced = this.state.map[currentLocale]; 104 | for (const namespace of nss) { 105 | const translations = translationsNamespaced[namespace]; 106 | const value = translations[key]; 107 | if (value !== undefined) { 108 | return typeof value === 'function' 109 | ? value(translationInterpolate(args)) : (value || key); 110 | } 111 | } 112 | } 113 | 114 | return key; 115 | }) as TranslatorFn; 116 | t.t = key => (strs?: TemplateStringsArray, ...args: any[]) => { 117 | const result = t(key, ...args); 118 | if (typeof result === 'object') return result; 119 | else if (result !== key) return createElement(Fragment, {}, result); 120 | else return defaultInterpolate(strs!, ...args); 121 | }; 122 | 123 | return t; 124 | }; 125 | 126 | render () { 127 | return React.createElement(context.Provider, { 128 | value: this.state, 129 | children: this.props.children, 130 | }); 131 | } 132 | }; 133 | 134 | const defaultT: TranslatorFn = (k => k) as TranslatorFn; 135 | defaultT.t = key => (strs, ...args) => defaultInterpolate(strs!, args); 136 | 137 | const useT: UseT = (namespaces?: string | string[]) => { 138 | const nss: string[] = namespaces instanceof Array ? namespaces : [namespaces || ns]; 139 | const state = (React as any).useContext(context) as ProviderState; 140 | return [state.createT ? state.createT(nss) : defaultT, state]; 141 | }; 142 | 143 | const withT: WithT = (Comp, nss = ns) => { 144 | if (!Array.isArray(nss)) nss = [nss]; 145 | return (props => { 146 | const [t, T] = useT(nss as string[]); 147 | return React.createElement(Comp, {...(props as any), t, T}); 148 | }); 149 | }; 150 | 151 | const Trans: React.FC = (props) => { 152 | const {children} = props; 153 | const nss: string[] = props.ns instanceof Array ? props.ns : [props.ns || ns]; 154 | const [t, T] = useT(nss); 155 | 156 | return (typeof children === 'function' 157 | ? children(t, T) 158 | : children instanceof Array 159 | ? React.createElement(React.Fragment, null, ...children.map(item => 160 | typeof item === 'function' 161 | ? (item as any)(t) 162 | : typeof item === 'string' 163 | ? t(item) 164 | : item || null 165 | )) 166 | : typeof children === 'string' 167 | ? t(children) 168 | : children) || null; 169 | }; 170 | 171 | return { 172 | Consumer, 173 | Provider, 174 | context, 175 | useT, 176 | Trans, 177 | withT, 178 | T: defaultT, 179 | }; 180 | }; 181 | 182 | export default createTranslations; 183 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import createTranslations from './createTranslations'; 2 | 3 | export * from './types'; 4 | export const {Consumer, Provider, Trans, context, useT, withT, T} = createTranslations(); 5 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // Translation function `t`. 2 | export interface TranslatorFn { 3 | (key: string, ...args: any[]): string; 4 | t: (key: string) => (strs?: TemplateStringsArray, ...args: any[]) => React.ReactElement; 5 | } 6 | 7 | // Props of React components with translation function. 8 | export interface PropsWithT { 9 | t: TranslatorFn; 10 | } 11 | 12 | // Collection of translations. 13 | export interface Translations { 14 | [key: string]: 0 | string | ((...args: any[]) => any); 15 | } 16 | 17 | // Collecion of translations by namespace. 18 | export interface TranslationsNamespaced { 19 | [namespace: string]: Translations; 20 | } 21 | 22 | // Collection of namespaced translations by locale. 23 | export interface TranslationMap { 24 | [locale: string]: TranslationsNamespaced; 25 | } 26 | 27 | export interface ProviderProps { 28 | locale?: string; // Selected locale. 29 | defaultLocale?: string; // Default locale. 30 | ns?: string; // Default namespace. 31 | loader?: (locale: string, namespace: string) => Promise; 32 | map?: TranslationMap; // Preloaded translations. 33 | } 34 | 35 | export interface ProviderState { 36 | locale: string; // Active locale. 37 | ns: string; // Active namespace. 38 | map: TranslationMap; 39 | load: (locale: string, namespace: string) => Promise; 40 | setLocale: (locale: string) => void; 41 | createT: (namespaces: string[]) => TranslatorFn; 42 | } 43 | 44 | export interface TransProps { 45 | ns?: string | string[]; 46 | children: any | React.ReactChild | ((t: TranslatorFn, T: ProviderState) => React.ReactChild); 47 | } 48 | 49 | // React hook. 50 | export type UseT = (namespaces?: string[]) => [TranslatorFn, ProviderState]; 51 | 52 | export interface WithTProps { 53 | t: TranslatorFn; 54 | T: ProviderState; 55 | } 56 | 57 | // Higher order component. 58 | export type WithT =

(Comp: React.ComponentType

, ns?: string | string[]) => React.FC>; 59 | 60 | export interface Result { 61 | Provider: React.ComponentClass; 62 | Consumer: any; 63 | context: React.Context; 64 | useT: UseT; 65 | withT: WithT; 66 | Trans: React.SFC; 67 | T: TranslatorFn; 68 | } 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "jsx": "react", 7 | "declaration": true, 8 | "pretty": true, 9 | "rootDir": "src", 10 | "sourceMap": false, 11 | "strict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noImplicitAny": false, 16 | "noFallthroughCasesInSwitch": true, 17 | "importHelpers": true, 18 | "outDir": "lib", 19 | "lib": ["es2018", "dom"] 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "lib", 24 | "**/__tests__/**/*", 25 | "**/__stories__/**/*", 26 | "*.test.ts", 27 | "*.test.tsx" 28 | ] 29 | } 30 | --------------------------------------------------------------------------------