├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── examples ├── basic │ ├── demo.js │ ├── images │ │ ├── ad.svg │ │ ├── af.svg │ │ ├── ai.svg │ │ ├── as.svg │ │ ├── aw.svg │ │ ├── bm.svg │ │ ├── bn.svg │ │ ├── bo.svg │ │ ├── br.svg │ │ ├── bt.svg │ │ ├── by.svg │ │ ├── bz.svg │ │ ├── cy.svg │ │ ├── dm.svg │ │ ├── do.svg │ │ ├── ec.svg │ │ ├── eg.svg │ │ ├── es.svg │ │ ├── fj.svg │ │ ├── fk.svg │ │ ├── gb-nir.svg │ │ ├── gb-wls.svg │ │ ├── gs.svg │ │ ├── gt.svg │ │ ├── hr.svg │ │ ├── ht.svg │ │ ├── im.svg │ │ ├── io.svg │ │ ├── ir.svg │ │ ├── kh.svg │ │ ├── ky.svg │ │ ├── kz.svg │ │ ├── li.svg │ │ ├── lk.svg │ │ ├── md.svg │ │ ├── me.svg │ │ ├── mp.svg │ │ ├── ms.svg │ │ ├── mt.svg │ │ ├── mx.svg │ │ ├── nf.svg │ │ ├── ni.svg │ │ ├── om.svg │ │ ├── pn.svg │ │ ├── pt.svg │ │ ├── py.svg │ │ ├── rs.svg │ │ ├── sa.svg │ │ ├── sh.svg │ │ ├── sm.svg │ │ ├── sv.svg │ │ ├── sx.svg │ │ ├── sz.svg │ │ ├── tc.svg │ │ ├── tm.svg │ │ ├── un.svg │ │ ├── va.svg │ │ ├── vg.svg │ │ ├── vi.svg │ │ └── zm.svg │ ├── index.html │ ├── index.jsx │ ├── package.json │ ├── style.css │ └── webpack.config.js └── screen.png ├── package.json └── src ├── TranslatableInput.js └── styles ├── flag-default.svg ├── flag-lang-default.svg ├── flag-lang-en.svg ├── react-translatable-input-subtag-lang-flags.styl └── react-translatable-input.styl /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "production": { 5 | "presets": ["babili"], 6 | "comments": false 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{json,js,jsx,html,css}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [.eslintrc] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "rules": { 10 | "consistent-return": 0, 11 | "comma-dangle": 0, 12 | "no-use-before-define": 0, 13 | "no-prototype-builtins": 0, 14 | "react/jsx-no-bind": 0, 15 | "react/jsx-filename-extension": 0, 16 | "react/prefer-stateless-function": 0 17 | }, 18 | # "plugins": [ 19 | # "import" 20 | # ], 21 | "settings": { 22 | "import/resolver": "node" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,macos 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional eslint cache 42 | .eslintcache 43 | 44 | # Optional REPL history 45 | .node_repl_history 46 | 47 | # Output of 'npm pack' 48 | *.tgz 49 | 50 | 51 | ### macOS ### 52 | *.DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | # Thumbnails 59 | ._* 60 | # Files that might appear in the root of a volume 61 | .DocumentRevisions-V100 62 | .fseventsd 63 | .Spotlight-V100 64 | .TemporaryItems 65 | .Trashes 66 | .VolumeIcon.icns 67 | .com.apple.timemachine.donotpresent 68 | # Directories potentially created on remote AFP share 69 | .AppleDB 70 | .AppleDesktop 71 | Network Trash Folder 72 | Temporary Items 73 | .apdisk 74 | 75 | # Custom 76 | lib 77 | dist 78 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | src 3 | docs 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Belka s.r.l. (www.belka.us) 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-translatable-input 2 | 3 | A [ReactJS](http://facebook.github.io/react/) input component that manages multiple languages. 4 | 5 | [![npm version](https://badge.fury.io/js/react-translatable-input.svg)](https://badge.fury.io/js/react-translatable-input) 6 | ![Alt text](https://img.shields.io/badge/license-MIT-green.svg?style=flat) 7 | 8 | ```bash 9 | $ npm install --save react-translatable-input 10 | ``` 11 | 12 | ## Demo 13 | **[http://belkalab.github.io/react-translatable-input/](http://belkalab.github.io/react-translatable-input)** 14 | 15 | ![react-translatable-input screenshot](examples/screen.png) 16 | 17 | 18 | ## Options 19 | 20 | | Prop | Type | Description | Default | 21 | |------|------|-------------|---------| 22 | | **lang** | React.PropTypes.string.isRequired | The current editing language | - | 23 | | **values** | React.PropTypes.object.isRequired | The object containing the translated strings | - | 24 | | textarea | React.PropTypes.bool | Use a textarea for a multi-line input? | false | 25 | | placeholder | React.PropTypes.string | The placeholder to show when the input field is empty | - | 26 | | classes | React.PropTypes.string | Additional HTML classes to pass to the component | - | 27 | | disabled | React.PropTypes.bool | Is the component disabled? | false | 28 | | showLanguageName | React.PropTypes.bool | Show the language name label next to the flag? | false | 29 | | langTranslator | React.PropTypes.func | Used to translate iso langage codes to language names when `showLanguageName` is true | - | 30 | 31 | #### The `values` object 32 | 33 | The most important prop to be passed is the `values` object, which must be a plain JS Object in the form `{ langTag: langValue }`. For example: 34 | 35 | ```js 36 | values = { 37 | 'it': 'Italian input', 38 | 'en-US': 'English (United States) input', 39 | 'en': 'English input', 40 | 'de': 'German input' 41 | }; 42 | ``` 43 | 44 | All the language tags must be [*BCP 47*](https://www.w3.org/International/articles/language-tags/index.en) compliant. Differently encoded language names will be filtered out and not shown in the component. 45 | The only exception to this rule is the `default` language, intended to be used as a general fallback language. If the `default` language is present, it will always be put on top of the available languages. 46 | 47 | ## Callbacks 48 | 49 | | Prop | Type | Syntax | Description | 50 | |------|------|--------|-------------| 51 | | onLanguageChange| React.PropTypes.func | function(selectedLanguage) {} | Callback on language selection | 52 | | onValueChange| React.PropTypes.func | function(newValue, editingLanguage) {} | Callback on text entered | 53 | | onKeyDown| React.PropTypes.func | function(event) {} | Callback on keydown when text input is focused | 54 | 55 | ## Build it yourself 56 | 57 | Clone and run 58 | 59 | ```bash 60 | $ npm install 61 | ``` 62 | 63 | ## Contributors 64 | [Giovanni Frigo](https://github.com/giovannifrigo), Developer @[Belka](https://github.com/BelkaLab) 65 | 66 | [Matteo Bertamini](https://github.com/bertuz), Former developer @[Belka](https://github.com/BelkaLab) 67 | 68 | ## License 69 | react-translatable-input is Copyright (c) 2016-2018 Belka srl. It is free software, and may be redistributed under the terms specified in the LICENSE file (TL;DR: MIT license). 70 | 71 | ## About Belka 72 | ![Alt text](http://s2.postimg.org/rcjk3hf5x/logo_rosso.jpg) 73 | 74 | [Belka](http://belka.us/en) is a Digital Agency specialized in design, mobile applications development and custom solutions. 75 | We love open source software! You can [see our projects](http://belka.us/en/portfolio/) or look at our case studies. 76 | 77 | Interested? [Hire us](http://belka.us/en/contacts/) to help build your next amazing project. 78 | 79 | [www.belka.us](http://belka.us/en) 80 | -------------------------------------------------------------------------------- /examples/basic/images/as.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/basic/images/aw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /examples/basic/images/br.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/basic/images/by.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /examples/basic/images/cy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/basic/images/gb-wls.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/basic/images/kh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /examples/basic/images/li.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/basic/images/md.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /examples/basic/images/ms.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /examples/basic/images/nf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/basic/images/pn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/basic/images/pt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/basic/images/sz.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/basic/images/vi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/basic/images/zm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | material-color-hash example 6 | 7 | 8 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |

Translatable input fields

39 |

One input field to translate them all

40 |
41 | 42 |
43 | 44 |
45 |

Proudly brought to you by Belka

46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/basic/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'react-translatable-input/dist/react-translatable-input.css'; 4 | import 'react-translatable-input/dist/react-translatable-input-subtag-lang-flags.css'; 5 | import TranslatableInput from 'react-translatable-input'; 6 | 7 | import tags from 'language-tags'; 8 | 9 | // an helper function to translate language codes to language name. 10 | // in real life, this would be managed by an i18n manager such as polyglot.js 11 | function translateLanguage(tagStr) { 12 | let langDesc; 13 | 14 | if (tagStr === 'default') { 15 | return 'Default'; 16 | } 17 | 18 | const tag = tags(tagStr); 19 | const subtagLang = tag.language(); 20 | 21 | if (subtagLang !== null) { 22 | langDesc = subtagLang.descriptions()[0]; 23 | const subtagRegion = tag.region(); 24 | 25 | if (subtagRegion !== null && subtagRegion !== undefined) { 26 | langDesc = `${langDesc} - ${subtagRegion.descriptions()[0]}`; 27 | } 28 | } else { 29 | langDesc = tag; 30 | } 31 | 32 | return langDesc; 33 | } 34 | 35 | const demoLanguages = ['it', 'en', 'en-US', 'de']; 36 | 37 | class Demo extends React.Component { 38 | constructor(props) { 39 | super(props); 40 | 41 | // Fill in demo data 42 | const title = { 43 | default: 'Default post title' 44 | }; 45 | demoLanguages.forEach((c) => { 46 | title[c] = `Post title in ${translateLanguage(c)}`; 47 | }); 48 | 49 | const description = { 50 | default: '' 51 | }; 52 | demoLanguages.forEach((c) => { 53 | description[c] = ''; 54 | }); 55 | 56 | const content = { 57 | default: 'Default field is used when the user language is not supported/can\'t be detected' 58 | }; 59 | demoLanguages.forEach((c) => { 60 | content[c] = `Post content in ${translateLanguage(c)}`; 61 | }); 62 | 63 | this.state = { 64 | title, 65 | description, 66 | content, 67 | editingLanguage: 'it' 68 | }; 69 | } 70 | 71 | handleValueChange(value, lang, stateName) { 72 | const state = this.state[stateName]; 73 | state[lang] = value; 74 | 75 | this.setState({ 76 | [stateName]: state 77 | }); 78 | } 79 | 80 | handleLanguageChange(editingLanguage) { 81 | this.setState({ 82 | editingLanguage 83 | }); 84 | } 85 | 86 | render() { 87 | const { title, description, content, editingLanguage } = this.state; 88 | const nameError = false; 89 | 90 | return ( 91 |
92 |
93 |

Create new post

94 |
95 | 96 |
97 | 98 | 99 | this.handleValueChange(value, lang, 'title')} 105 | onLanguageChange={lang => this.handleLanguageChange(lang)} 106 | showLanguageName 107 | placeholder={'Post title'} 108 | /> 109 | this.handleValueChange(value, lang, 'description')} 115 | onLanguageChange={lang => this.handleLanguageChange(lang)} 116 | langTranslator={lang => translateLanguage(lang)} 117 | placeholder={'Post description'} 118 | /> 119 |
120 | 121 |
122 | 123 | this.handleValueChange(value, lang, 'content')} 129 | onLanguageChange={lang => this.handleLanguageChange(lang)} 130 | langTranslator={lang => translateLanguage(lang)} 131 | showLanguageName 132 | textarea 133 | placeholder={'Post content'} 134 | /> 135 |
136 | 137 |
138 | 139 | translateLanguage(lang)} 144 | showLanguageName 145 | disabled 146 | /> 147 |
148 |
149 | ); 150 | } 151 | } 152 | 153 | ReactDOM.render( 154 | , 155 | document.getElementById('demo') 156 | ); 157 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-translatable-input-demo", 3 | "version": "0.1.0", 4 | "description": "A simple react-translatable-input demo", 5 | "main": "demo.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "postinstall": "NODE_ENV=production webpack" 9 | }, 10 | "author": "Belka (http://belka.us)", 11 | "license": "MIT", 12 | "dependencies": { 13 | "json-loader": "^0.5.4", 14 | "language-tags": "^1.0.5", 15 | "react": "^15.4.0", 16 | "react-dom": "^15.4.0", 17 | "react-translatable-input": "../.." 18 | }, 19 | "devDependencies": { 20 | "babel-core": "^6.18.2", 21 | "babel-loader": "^6.2.7", 22 | "babel-preset-es2015": "^6.16.0", 23 | "babel-preset-react": "^6.16.0", 24 | "css-loader": "^0.25.0", 25 | "file-loader": "^0.9.0", 26 | "style-loader": "^0.13.1", 27 | "url-loader": "^0.5.7", 28 | "webpack": "^1.13.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/basic/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 'Roboto', sans-serif; 4 | } 5 | 6 | h1, p { 7 | color: #212121; 8 | } 9 | 10 | p { 11 | font-size: 20px; 12 | line-height: 1.4em; 13 | } 14 | 15 | pre { 16 | font-size: 18px; 17 | } 18 | 19 | #demo { 20 | padding: 50px 0; 21 | background: #f9f4ee; 22 | margin: 50px 0; 23 | box-shadow: #4a4a4a 0 0 10px -2px 24 | } 25 | 26 | #demo div.line { 27 | margin: 0 auto; 28 | width: 750px; 29 | } 30 | 31 | #demo h2 { 32 | margin: 10px; 33 | color: #4a4a4a; 34 | } 35 | 36 | #demo .TranslatableInput { 37 | width: 720px; 38 | margin: 10px; 39 | } 40 | 41 | #demo .TranslatableInput.inline { 42 | display: inline-block; 43 | width: 350px; 44 | } 45 | 46 | #demo .TranslatableInput textarea { 47 | min-height: 200px; 48 | font-size: .8em; 49 | } 50 | 51 | #demo .TranslatableInput input { 52 | font-size: .8em; 53 | } 54 | 55 | #demo label { 56 | font-size: .8em; 57 | width: calc(50% - 20px); 58 | display: inline-block; 59 | margin: 0 10px; 60 | font-weight: bold; 61 | color: #4a4a4a; 62 | } 63 | 64 | div.desc { 65 | width: 950px; 66 | margin: 0 auto; 67 | } 68 | div.desc h1 { 69 | text-align: center; 70 | font-family: monospace; 71 | font-size: 3em; 72 | } 73 | div.desc h2 { 74 | text-align: center; 75 | font-family: monospace; 76 | } 77 | div.desc h3 { 78 | text-align: center; 79 | color: #6d6d6d 80 | } 81 | div.desc h4 { 82 | text-align: center; 83 | color: #404040; 84 | margin: 50px 0; 85 | border-top: 3px dashed rgba(178, 9, 9, .7); 86 | border-bottom: 3px dashed rgba(178, 9, 9, .7); 87 | padding: 50px; 88 | } 89 | div.desc h4 a { 90 | color: #B10909; 91 | font-weight: bolder; 92 | text-decoration: none; 93 | } 94 | 95 | div.desc p code { 96 | background: #f0f0f0; 97 | padding: 2px 5px; 98 | margin: 0 3px; 99 | } 100 | -------------------------------------------------------------------------------- /examples/basic/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | env: { 5 | NODE_ENV: 'production' 6 | }, 7 | entry: './index.jsx', 8 | output: { 9 | path: __dirname, 10 | filename: 'demo.js' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.jsx?$/, 16 | loader: 'babel-loader', 17 | exclude: /node_modules/, 18 | include: __dirname, 19 | query: { 20 | presets: ['es2015', 'react'] 21 | } 22 | }, 23 | { 24 | test: /\.css$/, 25 | loader: 'style-loader!css-loader' 26 | }, 27 | { 28 | test: /\.svg$/, 29 | loader: 'url?limit=8192&name=images/[name].[ext]' 30 | }, 31 | { 32 | test: /\.json$/, 33 | loader: 'json-loader' 34 | } 35 | ] 36 | }, 37 | plugins: [ 38 | new webpack.DefinePlugin({ 39 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 40 | }), 41 | new webpack.optimize.UglifyJsPlugin({ 42 | include: /\.js$/, 43 | compress: { 44 | warnings: false 45 | } 46 | }) 47 | ] 48 | }; 49 | -------------------------------------------------------------------------------- /examples/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BelkaLab/react-translatable-input/057fa6bf36fbd73b223abc5799eceb1cc07c13e7/examples/screen.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-translatable-input", 3 | "version": "0.2.1", 4 | "description": "A ReactJS input component that manages multiple languages", 5 | "main": "lib/TranslatableInput.js", 6 | "style": "dist/react-translatable-input.min.css", 7 | "scripts": { 8 | "clean": "rm -rf lib/ dist/", 9 | "build-all": "npm run build-style && npm run build-lib && npm run build-dist", 10 | "build-lib": "cross-env NODE_ENV=development babel src --out-dir lib", 11 | "build-dist": "cross-env NODE_ENV=production babel src/TranslatableInput.js > dist/react-translatable-input.min.js", 12 | "build-style": "mkdir -p dist; stylus --inline --print src/styles/react-translatable-input.styl > dist/react-translatable-input.css && stylus --inline --print --compress src/styles/react-translatable-input.styl > dist/react-translatable-input.min.css && stylus --inline --print src/styles/react-translatable-input-subtag-lang-flags.styl > dist/react-translatable-input-subtag-lang-flags.css && stylus --inline --print --compress src/styles/react-translatable-input-subtag-lang-flags.styl > dist/react-translatable-input-subtag-lang-flags.min.css", 13 | "lint": "eslint src", 14 | "prepublish": "npm run lint && npm run build-all" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/BelkaLab/react-translatable-input.git" 19 | }, 20 | "keywords": [ 21 | "react", 22 | "input", 23 | "translatable", 24 | "form", 25 | "multilanguage", 26 | "internazionalization", 27 | "i18n" 28 | ], 29 | "author": "Belka (http://belka.us)", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/BelkaLab/react-translatable-input/issues" 33 | }, 34 | "homepage": "https://github.com/BelkaLab/react-translatable-input#readme", 35 | "dependencies": { 36 | "flag-icon-css": "^2.4.0", 37 | "language-tags": "^1.0.5", 38 | "react-select": "^1.0.0-rc" 39 | }, 40 | "peerDependencies": { 41 | "react": "^15.4.1", 42 | "react-dom": "^15.4.1" 43 | }, 44 | "devDependencies": { 45 | "babel-cli": "^6.18.0", 46 | "babel-eslint": "^7.0.0", 47 | "babel-preset-babili": "0.0.8", 48 | "babel-preset-es2015": "^6.16.0", 49 | "babel-preset-react": "^6.16.0", 50 | "cross-env": "^3.1.3", 51 | "stylus": "^0.54.5", 52 | "eslint": "^3.8.1", 53 | "eslint-config-airbnb": "^12.0.0", 54 | "eslint-plugin-import": "^1.16.0", 55 | "eslint-plugin-jsx-a11y": "^2.2.2", 56 | "eslint-plugin-react": "^6.3.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/TranslatableInput.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved, import/extensions 2 | import React, { Component, PropTypes } from 'react'; 3 | import Select from 'react-select'; 4 | import 'react-select/dist/react-select.css'; 5 | import 'flag-icon-css/css/flag-icon.min.css'; 6 | import tags from 'language-tags'; 7 | 8 | const propTypes = { 9 | lang: PropTypes.string.isRequired, // The current editing language 10 | values: PropTypes.object.isRequired, // The object containing the translated strings 11 | textarea: PropTypes.bool, // Use a textarea for a multi-line input? 12 | 13 | onLanguageChange: PropTypes.func, // Callback on language selection 14 | onValueChange: PropTypes.func, // Callback on text entered 15 | onKeyDown: PropTypes.func, // Callback on keydown when text input is focused 16 | 17 | placeholder: PropTypes.string, // The placeholder to show when the input field is empty 18 | classes: PropTypes.string, // Additional HTML classes to pass to the component 19 | disabled: PropTypes.bool, // Is the component disabled? 20 | showLanguageName: PropTypes.bool, // Show the language name label next to the flag? 21 | langTranslator: PropTypes.func // Translate iso langage codes to language names 22 | }; 23 | 24 | class TranslatableInput extends Component { 25 | constructor(props) { 26 | super(props); 27 | 28 | this.state = { 29 | isFocused: false 30 | }; 31 | } 32 | 33 | keyPressed(e) { 34 | const { onKeyDown } = this.props; 35 | 36 | if (typeof (onKeyDown) === 'function') { 37 | onKeyDown(e); 38 | } 39 | } 40 | 41 | changeLanguage(lang) { 42 | const { onLanguageChange } = this.props; 43 | 44 | if (typeof (onLanguageChange) === 'function') { 45 | onLanguageChange(lang.value); 46 | } 47 | } 48 | 49 | changeValue(value) { 50 | const { onValueChange, lang } = this.props; 51 | 52 | if (typeof (onValueChange) === 'function') { 53 | onValueChange(value, lang); 54 | } 55 | } 56 | 57 | focused(isFocused) { 58 | this.setState({ 59 | isFocused 60 | }); 61 | } 62 | 63 | renderFlag(option) { 64 | const { showLanguageName, langTranslator } = this.props; 65 | const tag = tags(option.value); 66 | let langClasses = ''; 67 | 68 | if (tag.valid() === false) { 69 | // the default language 70 | const defaultName = typeof (langTranslator) === 'function' ? langTranslator('default') : 'default'; 71 | return ( 72 |
73 |
77 | { showLanguageName ?
{defaultName}
: null } 78 |
79 | ); 80 | } 81 | 82 | const langName = typeof (langTranslator) === 'function' ? langTranslator(option.value) : option.value; 83 | let regCode = 'default'; 84 | let langCode; 85 | 86 | if (typeof tag.find('region') === 'object') { 87 | regCode = tag.find('region').data.subtag; 88 | } else if (typeof tag.find('language') === 'object') { 89 | regCode = tag.find('language').data.subtag; 90 | langCode = tag.find('language').data.subtag; 91 | } else { 92 | langCode = 'default'; 93 | } 94 | 95 | if (langCode !== undefined) { 96 | langClasses = `flag-icon-lang-default flag-icon-lang-${langCode} `; 97 | } 98 | 99 | langClasses += `flag-icon flag-icon-${regCode}`; 100 | 101 | return ( 102 |
103 |
107 | { showLanguageName ?
{langName}
: null } 108 |
109 | ); 110 | } 111 | 112 | render() { 113 | const { values, lang, classes, showLanguageName, textarea } = this.props; 114 | const { isFocused } = this.state; 115 | 116 | const langOptions = Object.keys(values) 117 | .filter(l => tags(l).valid()) 118 | .map(tag => ({ label: tag, value: tag })); 119 | 120 | // put default language on top of the list, if present 121 | if (values.hasOwnProperty('default')) { 122 | langOptions.unshift({ label: 'default', value: 'default' }); 123 | } 124 | 125 | let componentClasses = 'TranslatableInput'; 126 | 127 | if (isFocused) { 128 | componentClasses += ' is-focused'; 129 | } 130 | 131 | if (showLanguageName) { 132 | componentClasses += ' has-language-name'; 133 | } 134 | 135 | if (textarea) { 136 | componentClasses += ' uses-textarea'; 137 | } 138 | 139 | if (typeof (classes) === 'string') { 140 | componentClasses += ` ${classes}`; 141 | } 142 | 143 | return ( 144 |
145 |