├── .github
├── FUNDING.yml
└── workflows
│ └── tests.yml
├── .gitignore
├── LICENSE
├── demo
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
└── src
│ ├── Text.js
│ ├── detect.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── normalize.js
│ └── serviceWorker.js
├── index.min.js
├── package.json
├── readme.md
├── rollup.config.js
└── src
├── find.js
├── index.js
├── index.test.js
├── normalize.js
└── normalize.test.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: https://www.paypal.me/franciscopresencia/19
2 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | strategy:
10 | matrix:
11 | node-version: [14.x, 16.x, 18.x]
12 |
13 | steps:
14 | - uses: actions/checkout@v1
15 | - name: Use Node.js ${{ matrix.node-version }}
16 | uses: actions/setup-node@v1
17 | with:
18 | node-version: ${{ matrix.node-version }}
19 | - name: install dependencies
20 | run: npm install
21 | - name: npm test
22 | run: npm test
23 | env:
24 | CI: true
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Only for apps
9 | package-lock.json
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # nyc test coverage
24 | .nyc_output
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # Bower dependency directory (https://bower.io/)
30 | bower_components
31 |
32 | # node-waf configuration
33 | .lock-wscript
34 |
35 | # Compiled binary addons (https://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # TypeScript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 |
63 | # next.js build output
64 | .next
65 |
66 | .DS_Store
67 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Francisco Presencia
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 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.3.2",
7 | "react-dom": "^16.6.0",
8 | "react-scripts": "2.1.1",
9 | "react-text-translate": "^0.1.0"
10 | },
11 | "scripts": {
12 | "start": "react-scripts start",
13 | "build": "react-scripts build",
14 | "test": "react-scripts test",
15 | "eject": "react-scripts eject"
16 | },
17 | "eslintConfig": {
18 | "extends": "react-app"
19 | },
20 | "browserslist": [
21 | ">0.2%",
22 | "not dead",
23 | "not ie <= 11",
24 | "not op_mini all"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/demo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/franciscop/react-text/2b40d7ae3f2329a676ef4202514407b85f2426bd/demo/public/favicon.ico
--------------------------------------------------------------------------------
/demo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
46 |
47 | ), document.getElementById('root'));
48 |
--------------------------------------------------------------------------------
/demo/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/demo/src/normalize.js:
--------------------------------------------------------------------------------
1 | export default (...args) => {
2 | // Allow for an array of arguments that are all objects
3 | const dictionary = Object.assign({}, ...args);
4 |
5 | let languages;
6 |
7 | Object.keys(dictionary).forEach(key => {
8 | const entry = dictionary[key];
9 |
10 | // Already parsed
11 | if (typeof entry === 'function') return;
12 |
13 | // Retrieve a list of the languages, that must be the same in every entry
14 | const langs = Object.keys(entry);
15 | if (!languages) languages = langs;
16 | if (JSON.stringify(languages) !== JSON.stringify(langs)) {
17 | throw new Error(`Wrong amount of languages, expected '${languages}' but got '${langs}'`);
18 | }
19 |
20 | for (let lang of languages) {
21 | const value = dictionary[key][lang];
22 | if (typeof value === 'string') {
23 | entry[lang] = (() => value);
24 | }
25 | }
26 |
27 | dictionary[key] = (lang, ...args) => {
28 | const fn = entry[lang] || Object.values(entry)[0];
29 | return fn(Object.assign({}, ...args));
30 | };
31 | });
32 |
33 | return dictionary;
34 | };
35 |
--------------------------------------------------------------------------------
/demo/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read http://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/index.min.js:
--------------------------------------------------------------------------------
1 | import e,{createContext as t}from"react";var r=(...e)=>{const t=Object.assign({},...e);let r;return Object.keys(t).reduce((e,n)=>{const o=Object.keys(t[n]);if(r||(r=o),JSON.stringify(r.sort())!==JSON.stringify(o.sort()))throw new Error(`Wrong amount of languages, expected '${r}' but got '${o}'`);return{...e,[n]:r.reduce((e,r)=>{const o=t[n][r];if(o&&o.normalized)return{...e,[r]:o};if("function"==typeof o){const t=(e={})=>o(e);return t.normalized=!0,{...e,[r]:t}}if("string"==typeof o){const t=()=>o;return t.normalized=!0,{...e,[r]:t}}throw new Error(`Value cannot be of type "${typeof o}"`)},{})}},{})},n=({id:e,dictionary:t,language:r,...n})=>{if(!e){const t=Object.entries(n).filter(([e,t])=>"boolean"==typeof t).map(([e])=>e);if(!t.length)throw new Error("Please make sure to pass an id");if(t.length>1)throw new Error(`Please only pass a single id/boolean, detected '${t}'`);e=t[0]}const o=Object.keys(t);if(!o.includes(e))throw new Error(`Couldn't find '${e}' in the dictionary. Available ids: ${o}`);if(void 0===t[e][r])throw new Error(`The language '${r}' is not available (attempted with id '${e}')`);return t[e][r](n)};const{Provider:o,Consumer:i}=t({});let a=({children:e})=>e;if("undefined"!=typeof require)try{a=require("react-native").Text}catch(e){}export default({language:t,dictionary:l={},children:c,render:s,component:u,...d})=>e.createElement(i,null,i=>c?(t=t||i.language,l={...i.dictionary,...r(l)},e.createElement(o,{value:{language:t,dictionary:l}},c)):u?!0===d[i.language]?u:null:s?s(n({...i,...d})):e.createElement(a,null,n({...i,...d})));
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-text",
3 | "version": "3.2.5",
4 | "description": "React translation library with plain objects as dictionaries",
5 | "homepage": "https://github.com/franciscop/react-text#readme",
6 | "repository": "https://github.com/franciscop/react-text.git",
7 | "bugs": "https://github.com/franciscop/react-text/issues",
8 | "funding": "https://www.paypal.me/franciscopresencia/19",
9 | "author": "Francisco Presencia (https://francisco.io/)",
10 | "license": "MIT",
11 | "scripts": {
12 | "build": "rollup -c",
13 | "size": "echo $(gzip -c index.min.js | wc -c) bytes",
14 | "start": "jest --watch",
15 | "test": "jest --coverage"
16 | },
17 | "keywords": [
18 | "react",
19 | "translate",
20 | "dictionary",
21 | "language",
22 | "i18n"
23 | ],
24 | "main": "index.min.js",
25 | "files": [],
26 | "devDependencies": {
27 | "@babel/core": "^7.15.0",
28 | "@babel/preset-env": "^7.15.0",
29 | "@babel/preset-react": "^7.14.5",
30 | "babel-loader": "^8.2.2",
31 | "babel-polyfill": "^6.26.0",
32 | "jest": "^28.1.0",
33 | "jest-environment-jsdom": "^28.1.0",
34 | "react": "^18.2.0",
35 | "react-test": "^0.19.0",
36 | "rollup": "^1.32.1",
37 | "rollup-plugin-babel": "^4.4.0",
38 | "rollup-plugin-terser": "^5.2.0"
39 | },
40 | "peerDependencies": {
41 | "react": ">=16.8.0"
42 | },
43 | "babel": {
44 | "presets": [
45 | "@babel/preset-env",
46 | "@babel/preset-react"
47 | ]
48 | },
49 | "jest": {
50 | "testEnvironment": "jsdom"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # React Text [](https://www.npmjs.com/package/react-text) [](https://github.com/franciscop/react-text/blob/master/.github/workflows/tests.yml) [](https://github.com/franciscop/react-text/blob/master/index.min.js)
2 |
3 | React and React Native translation library with plain objects as dictionaries:
4 |
5 | ```js
6 | import Text from 'react-text';
7 | import dictionary from './dictionary';
8 |
9 | export default () => (
10 |
11 |
12 |
13 |
14 | );
15 | //
こんにちは、世界!
16 | //
さよなら、FRANCISCOさん!
17 | ```
18 |
19 | Contents:
20 |
21 | - [**Getting started**](#getting-started): Introduction to how to start using `react-text` with React.
22 | - [**Dictionary**](#dictionary): define the translations and some basic transformations.
23 | - [**Configuration**](#configuration): set the language and inject the dictionary.
24 | - [**Translate**](#translate): use `` with a id to create translations with optional properties.
25 | - [**Render**](#render): inject a translated string into a React component. Useful for `alt={}` and similar.
26 | - [**Component**](#component): renders only for the right language.
27 |
28 |
29 |
30 |
31 |
32 |
33 | ## Getting started
34 |
35 | First let's install the package with npm:
36 |
37 | ```bash
38 | npm install react-text
39 | ```
40 |
41 | Then we define [a dictionary](#dictionary) of the text to translate:
42 |
43 | ```js
44 | // ./dictionary.js
45 | export default {
46 | greetings: {
47 | en: 'Hello world!',
48 | es: '¡Hola mundo!',
49 | ja: 'こんにちは、世界!'
50 | }
51 | };
52 | ```
53 |
54 | To use those, we need to create a wrapping `` with two options: [the **dictionary** and the **language**](#configuration). Then inside it, we create a self-closing `` tag with the property as the previously defined key of the object:
55 |
56 | ```js
57 | // ./Example.js
58 | import Text from 'react-text';
59 | import dictionary from './dictionary';
60 |
61 | export default () => (
62 |
63 |
64 |
65 | );
66 | // ~> ¡Hola mundo!
67 | ```
68 |
69 |
70 |
71 |
72 |
73 |
74 | ## Dictionary
75 |
76 | The dictionary is defined as an object of objects. The first level (`greetings`) is what we call the `id`, the second is the language (`en`) and finally we have the values (`Hello world` and functions):
77 |
78 | ```js
79 | // ./dictionary.js
80 | export default {
81 | greetings: {
82 | en: 'Hello world!',
83 | es: '¡Hola mundo!',
84 | ja: 'こんにちは、世界!'
85 | },
86 | farewell: {
87 | en: ({ name = 'World' }) => `Hello ${name}!`,
88 | es: ({ name = 'Mundo'}) => `¡Adiós ${name}!`,
89 | ja: ({ name = '世界' }) => `さよなら、${name.toUpperCase()}さん!`
90 | }
91 | };
92 | ```
93 |
94 | All the languages must be the same in all the entries, otherwise it will throw an error. The order is important as well, since the first language will be considered the default one if it cannot be found otherwise.
95 |
96 |
97 |
98 |
99 |
100 |
101 | ## Configuration
102 |
103 | Once we have the dictionary, we have to determine how and where to inject it, as well as specifying the language. This will be done by creating a `` element **with children**:
104 |
105 | ```js
106 | import Text from 'react-text';
107 | import dictionary from './dictionary';
108 |
109 | export default () => (
110 |
111 | {/* Here the language will be English */}
112 |
113 | );
114 | ```
115 |
116 | For React Native, any of the [usual props](https://facebook.github.io/react-native/docs/text#props) of `...` can be passed here.
117 |
118 | They can be set at different levels, which is specially useful if you want to split the dictionary into different pages:
119 |
120 | ```js
121 | import Text from 'react-text';
122 | import dictionaryA from './dictionaryA';
123 | import dictionaryB from './dictionaryB';
124 |
125 | export default () => (
126 |
127 |
128 | {/* English for Dictionary A */}
129 |
130 |
131 |
132 | {/* English for Dictionary B */}
133 |
134 |
162 | ```
163 |
164 | The language would normally be a variable that comes from your own code:
165 |
166 | ```js
167 | import Text from 'react-text';
168 | import dictionary from './dictionary';
169 |
170 | export default ({ language = 'en' }) => (
171 |
172 | {/* Here the language will be English */}
173 |
174 | );
175 | ```
176 |
177 | The language can also be nested, and it will use the most specific (innermost):
178 |
179 | ```js
180 | const dictionary = { greetings: {
181 | en: 'Hello world!',
182 | es: '¡Hola mundo!',
183 | ja: 'こんにちは、世界!'
184 | }};
185 |
186 | export default () => (
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | );
197 | //
Hello world!
198 | //
こんにちは、世界!
199 | //
¡Hola mundo!
200 | ```
201 |
202 | While nesting dictionaries is totally fine and expected, nesting languages might get messy and it's recommended to avoid it if possible. Use a global store like Redux to handle the language instead and inject it at the root level:
203 |
204 | ```js
205 | // LanguagePicker.js
206 | // Example implementation with Redux and an action creator
207 | const setLanguage = payload => ({ type: 'SET_LANGUAGE', payload });
208 | export default connect(({ language }) => ({ language }))(({ language, dispatch }) => {
209 |
210 |
211 |
212 |
213 |
Current language: {language}
214 |
215 | });
216 |
217 | // reducers/index.js
218 | export default combineReducers({
219 | // ...
220 | language: (state = 'en', { type, payload }) => {
221 | return (type === 'SET_LANGUAGE') ? payload : state;
222 | }
223 | });
224 | ```
225 |
226 |
227 |
228 |
229 |
230 |
231 | ## Translate
232 |
233 | With the dictionary and language injected, use `` with a **self-closing tag** and the right id:
234 |
235 | ```js
236 | const dictionary = {
237 | greetings: {
238 | en: 'Hello world!',
239 | es: '¡Hola mundo!',
240 | ja: 'こんにちは、世界!'
241 | }
242 | };
243 |
244 | // Usage; the prop 'greetings' will correspond to the dictionary id 'greetings'
245 | export default () => (
246 |
247 |
248 |
249 | );
250 | // ~>
こんにちは、世界!
251 | ```
252 |
253 | **Valid id names**: any prop except `id`, [`children`](#configuration), [`render`](#render) and [`component`](#component) since these have special meaning in React-Text. Click on those keywords to see how they are used. The ids are case-sensitive.
254 |
255 | The dictionary can also be a function, which will be called when rendering. The advantage is that it will receive any prop that you pass to the element. You can then localize the text properly depending on the language, and even provide defaults easily:
256 |
257 | ```js
258 | const dictionary = {
259 | greetings: {
260 | en: ({ name = 'World' }) => `Hello ${name}!`,
261 | es: ({ name = 'Mundo' }) => `¡Hola ${name}!`,
262 | ja: ({ name = '世界' }) => `こんにちは、${name.toUpperCase()}さん!`
263 | }
264 | };
265 |
266 | // The prop passed as `name` will be received in the dictionary
267 | export default () => (
268 |
269 |
270 |
271 | );
272 | // ~> こんにちは、FRANCISCOさん!
273 | ```
274 |
275 | You can also use the `id` prop instead of just writing the id as a prop. These two work exactly the same:
276 |
277 | ```js
278 | export default () => (
279 |
280 |
281 |
282 |
283 | );
284 | ```
285 |
286 | This however works much better for dynamic ids, since those would be messy otherwise:
287 |
288 | ```js
289 | const key = 'greetings';
290 |
291 | export default () => (
292 |
293 |
294 |
295 |
296 | );
297 | ```
298 |
299 | > Note: the props that you can pass can be either strings or numbers, but right now you cannot pass a boolean like ``. We might lift this limitation in the future.
300 |
301 |
302 |
303 |
304 |
305 |
306 | ## Render
307 |
308 | Injects the plain text into a function. Useful for those times when you can only pass plain text and not a component:
309 |
310 | ```js
311 | // These both render to the exact same thing:
312 |
313 |
{text}
} />
314 | // ~>
Hello world
315 | ```
316 |
317 | The next example can only be achieved with `render()` since it will pass the plain representation as specified in the dictionary:
318 |
319 | ```js
320 | } />
321 | // ~>
322 | ```
323 |
324 | If you try to do the same with `` you will get an unexpected result, since `` renders a React component:
325 |
326 | ```js
327 | // ERROR - this does not work as expected
328 | } />
329 | // ~>
330 | ```
331 |
332 |
333 |
334 |
335 |
336 |
337 | ## Component
338 |
339 | When trying to do a switch between more complex fragments, or display one part only for one language, we can do so by using the `component` prop:
340 |
341 | ```js
342 |
343 |
345 | {/* Large block of text in English here */}
346 |
347 | )} />
348 |
350 | {/* Large block of text in Spanish here */}
351 |
352 | )} />
353 |
355 | {/* Large block of text in Japanese here */}
356 |
357 | )} />
358 |
359 | ```
360 |
361 | Note that, when using `component={...}`, we are using **the language as a key**. This would be the internal equivalent of you doing:
362 |
363 | ```js
364 | // dictionary.js
365 | // NOTE: DO NOT DO THIS, this is just a visualization of how it works internally
366 | export default {
367 | en: { en: ({ component }) => component, es: '', ja: '' },
368 | es: { en: '', es: ({ component }) => component, ja: '' },
369 | ja: { en: '', es: '', ja: ({ component }) => component }
370 | };
371 | ```
372 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from "rollup-plugin-babel";
2 | import { terser } from "rollup-plugin-terser";
3 |
4 | export default {
5 | input: "src/index.js",
6 | output: { file: "index.min.js", format: "esm" },
7 | external: ["react"],
8 | plugins: [
9 | babel({
10 | exclude: "node_modules/**",
11 | presets: [
12 | ["@babel/env", { targets: { node: 12 } }],
13 | "@babel/preset-react",
14 | ],
15 | }),
16 | terser(),
17 | ],
18 | };
19 |
--------------------------------------------------------------------------------
/src/find.js:
--------------------------------------------------------------------------------
1 |
2 | export default ({ id, dictionary, language, ...props }) => {
3 | if (!id) {
4 | const keys = Object.entries(props).filter(([k, v]) => typeof v === 'boolean').map(([k]) => k);
5 | if (!keys.length) throw new Error('Please make sure to pass an id');
6 | if (keys.length > 1) throw new Error(`Please only pass a single id/boolean, detected '${keys}'`);
7 | id = keys[0];
8 | }
9 | const ids = Object.keys(dictionary);
10 | if (!ids.includes(id)) {
11 | throw new Error(`Couldn't find '${id}' in the dictionary. Available ids: ${ids}`);
12 | }
13 | if (typeof dictionary[id][language] === 'undefined') {
14 | throw new Error(`The language '${language}' is not available (attempted with id '${id}')`);
15 | }
16 | return dictionary[id][language](props);
17 | };
18 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { createContext } from "react";
2 | import normal from "./normalize";
3 | import find from "./find";
4 | const { Provider, Consumer } = createContext({});
5 |
6 | let Text = ({ children }) => children;
7 | // Attempt to load React Native if it's a dependency so that it is wrapped there
8 | if (typeof require !== "undefined") {
9 | try {
10 | Text = require("react-native").Text;
11 | } catch (error) {}
12 | }
13 |
14 | export default ({
15 | language,
16 | dictionary = {},
17 | children,
18 | render,
19 | component,
20 | ...props
21 | }) => (
22 |
23 | {(value) => {
24 | const withText = ({ children }) => {children};
25 |
26 | // We only care about language or dictionary at the same level when we have children
27 | if (children) {
28 | language = language || value.language;
29 | dictionary = { ...value.dictionary, ...normal(dictionary) };
30 | return {children};
31 | }
32 |
33 | // The component prop was passed, render only for the right language
34 | if (component) return props[value.language] === true ? component : null;
35 |
36 | // The render prop was passed, render into the children instead
37 | if (render) return render(find({ ...value, ...props }));
38 |
39 | // Normal id value
40 | return {find({ ...value, ...props })};
41 | }}
42 |
43 | );
44 |
--------------------------------------------------------------------------------
/src/index.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import $ from "react-test";
3 | import Text from ".";
4 |
5 | const dictionary = {
6 | greetings: {
7 | en: "Hello world!",
8 | es: "¡Hola mundo!",
9 | ja: "こんにちは、世界!",
10 | },
11 | farewell: {
12 | en: ({ name = "World" }) => `Hello ${name}!`,
13 | es: ({ name = "Mundo" }) => `¡Adiós ${name}!`,
14 | ja: ({ name = "世界" }) => `さよなら、${name.toUpperCase()}さん!`,
15 | },
16 | };
17 |
18 | describe("text", () => {
19 | it("can render correctly", () => {
20 | const text = $(
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ).text();
32 | expect(text).toBe("こんにちは、世界!さよなら、FRANCISCOさん!");
33 | });
34 |
35 | it("can render a non-translated string", () => {
36 | // Note: need the here to match it properly
37 | const text = $(
38 |