├── .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 | [](https://badge.fury.io/js/react-translatable-input)
6 | 
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 | 
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 | 
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 |
34 |
--------------------------------------------------------------------------------
/examples/basic/images/aw.svg:
--------------------------------------------------------------------------------
1 |
187 |
--------------------------------------------------------------------------------
/examples/basic/images/br.svg:
--------------------------------------------------------------------------------
1 |
46 |
--------------------------------------------------------------------------------
/examples/basic/images/by.svg:
--------------------------------------------------------------------------------
1 |
60 |
--------------------------------------------------------------------------------
/examples/basic/images/cy.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/examples/basic/images/gb-wls.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/examples/basic/images/kh.svg:
--------------------------------------------------------------------------------
1 |
75 |
--------------------------------------------------------------------------------
/examples/basic/images/li.svg:
--------------------------------------------------------------------------------
1 |
44 |
--------------------------------------------------------------------------------
/examples/basic/images/md.svg:
--------------------------------------------------------------------------------
1 |
74 |
--------------------------------------------------------------------------------
/examples/basic/images/ms.svg:
--------------------------------------------------------------------------------
1 |
40 |
--------------------------------------------------------------------------------
/examples/basic/images/nf.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/examples/basic/images/pn.svg:
--------------------------------------------------------------------------------
1 |
70 |
--------------------------------------------------------------------------------
/examples/basic/images/pt.svg:
--------------------------------------------------------------------------------
1 |
58 |
--------------------------------------------------------------------------------
/examples/basic/images/sz.svg:
--------------------------------------------------------------------------------
1 |
50 |
--------------------------------------------------------------------------------
/examples/basic/images/vi.svg:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/examples/basic/images/zm.svg:
--------------------------------------------------------------------------------
1 |
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 |
181 | );
182 | }
183 | }
184 |
185 | TranslatableInput.propTypes = propTypes;
186 |
187 | export default TranslatableInput;
188 |
--------------------------------------------------------------------------------
/src/styles/flag-default.svg:
--------------------------------------------------------------------------------
1 |
2 |
20 |
--------------------------------------------------------------------------------
/src/styles/flag-lang-default.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/styles/flag-lang-en.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/styles/react-translatable-input-subtag-lang-flags.styl:
--------------------------------------------------------------------------------
1 | // must be on the root: specificity is too high otherwise,
2 | // by overriding other flag styles other libraries set
3 | // and we rely upon
4 | .flag-icon-lang-default
5 | background-image url(flag-default.svg)
6 |
7 | .flag-icon-lang-default
8 | background-image url(flag-lang-default.svg)
9 |
10 | .TranslatableInput
11 | .Select
12 | // Here you can place all your lang image preferences by adding a class.
13 | // If missing, it fallbacks to flag-lang-default.svg
14 | .flag-icon-lang-en
15 | background-image url(flag-lang-en.svg)
--------------------------------------------------------------------------------
/src/styles/react-translatable-input.styl:
--------------------------------------------------------------------------------
1 | $borderRadius = 5px
2 | $langSelectWidth = 60px
3 | $langSelectWideWidth = 150px
4 |
5 | .TranslatableInput
6 | border 1px solid #ccc
7 | border-radius $borderRadius
8 | min-width 260px
9 |
10 | .Select
11 | display inline-block
12 | width $langSelectWidth !important
13 | margin 0
14 | float left
15 | .Select-control
16 | height 35px
17 | box-shadow none
18 | border none
19 | border-radius $borderRadius 0 0 $borderRadius
20 | .Select-option
21 | position relative
22 | min-height 35px
23 | .Select-value
24 | background-color #eee
25 | height 100%
26 | width 100%
27 | border-right 1px solid #ccc
28 | border-radius $borderRadius 0 0 $borderRadius
29 | .flag-icon
30 | position absolute
31 | top 50%
32 | transform translateY(-60%)
33 | &:disabled
34 | opacity .7
35 | .language-name
36 | padding 0 10px 0 30px
37 | overflow hidden
38 | text-overflow ellipsis
39 | &:disabled
40 | opacity .7
41 |
42 | input
43 | display inline-block
44 | margin 0
45 | height 35px
46 | width "calc(100% - %s)" % $langSelectWidth
47 | border none
48 | border-radius 0 $borderRadius $borderRadius 0
49 | padding 0 10px
50 | box-sizing border-box
51 | outline none
52 | &:disabled
53 | background-color #f1f1f1
54 |
55 | textarea
56 | display block
57 | margin 0
58 | height 35px
59 | width 100%
60 | border none
61 | border-radius 0 0 $borderRadius $borderRadius
62 | padding 10px
63 | box-sizing border-box
64 | resize vertical
65 | outline none
66 | &:disabled
67 | background-color #f1f1f1
68 |
69 | &.is-focused
70 | border-color #08c #0099e6 #0099e6
71 | box-shadow inset 0 1px 2px rgba(0,0,0,.1),0 0 5px -1px rgba(0,136,204,.5)
72 |
73 | &.error
74 | border-color #FF4F0D
75 | box-shadow inset 0 1px 2px rgba(0,0,0,.1),0 0 5px -1px rgba(204, 0, 0, 0.5)
76 |
77 | .flag-icon-default
78 | background-image url(flag-default.svg)
79 |
80 | &.has-language-name
81 | min-width 350px
82 |
83 | .Select
84 | width $langSelectWideWidth !important
85 | input
86 | width "calc(100% - %s)" % $langSelectWideWidth
87 |
88 | &.uses-textarea
89 | min-width 350px
90 |
91 | .Select
92 | display inline-block
93 | width 100% !important
94 | margin 0
95 | float left
96 | .Select-control
97 | height 35px
98 | box-shadow none
99 | border none
100 | border-radius $borderRadius $borderRadius 0 0
101 | .Select-value
102 | border-right none
103 | border-radius $borderRadius $borderRadius 0 0
104 |
--------------------------------------------------------------------------------