├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── intl │ ├── de.json │ ├── en.json │ └── fr.json ├── src ├── app │ ├── App.css │ ├── App.js │ └── App.test.js ├── index.css ├── index.js └── intl │ ├── IntlWrapper.js │ ├── intl.store.js │ └── setup.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .idea 14 | .DS_Store 15 | .env 16 | npm-debug.log 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 James Hill 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-router-intl-routing 2 | ==== 3 | 4 | An example repo demonstrating dynamic translation of the actual uri segments of the address bar. 5 | 6 | Although I see a few examples of integrating [react-router](https://react-router.now.sh/) and [react-intl](https://github.com/yahoo/react-intl), these by and large, focus 7 | on dynamically re-rendering the page content for a chosen language. 8 | 9 | This takes this concept a step further, in that it demonstrates how simple it is to also have each uri segment in the address bar translated in your chosen language as well. **Examples of this I've not found despite extensive searching!** 10 | 11 | The example uses [Mobx](https://mobxjs.github.io/mobx/) to glue things together, and also [create-react-app](https://github.com/facebookincubator/create-react-app) as a sensible shortcut to get up and running. As `create-react-app` does not - yet - support [decorators](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#can-i-use-decorators) or other experimental features not yet widely adopted from `babel` such as `property-initializers`, I'm afraid you don't get the sugary sweetness with Mobx. 12 | 13 | 14 | `npm start` to get started. 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-intl-routing", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "0.7.0" 7 | }, 8 | "dependencies": { 9 | "intl": "^1.2.5", 10 | "intl-locales-supported": "^1.0.0", 11 | "mobx": "^2.6.0", 12 | "mobx-react": "^3.5.8", 13 | "mobx-react-devtools": "^4.2.9", 14 | "react": "^15.3.2", 15 | "react-dom": "^15.3.2", 16 | "react-intl": "^2.1.5", 17 | "react-router": "^4.0.0-alpha.4", 18 | "whatwg-fetch": "^1.0.0" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test --env=jsdom" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhchill666/react-router-intl-routing/20195d65079ec3f70165b8654adfd2eb0e6d3b02/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Mobx, React Router 4 and React-Intl 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/intl/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale": "de", 3 | "formats": {}, 4 | "messages": { 5 | "home": "Zuhause", 6 | "about": "Etwa", 7 | "route-about": "etwa", 8 | "topics": "Themen", 9 | "route-topics": "themen", 10 | "topics-rendering": "Rendering mit React", 11 | "topics-components": "Komponenten", 12 | "topics-propsVState": "Props v. Bundesland", 13 | "topics-selectTopic": "Bitte wählen Sie ein Thema", 14 | "en": "Englisch", 15 | "fr": "Französisch", 16 | "de": "Deutsche" 17 | } 18 | } -------------------------------------------------------------------------------- /public/intl/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale": "en", 3 | "formats": {}, 4 | "messages": { 5 | "home": "Home", 6 | "about": "About", 7 | "route-about": "about", 8 | "topics": "Topics", 9 | "route-topics": "topics", 10 | "topics-rendering": "Rendering with React", 11 | "topics-components": "Components", 12 | "topics-propsVState": "Props v. State", 13 | "topics-selectTopic": "Please select a topic", 14 | "en": "English", 15 | "fr": "French", 16 | "de": "German" 17 | } 18 | } -------------------------------------------------------------------------------- /public/intl/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale": "fr", 3 | "formats": {}, 4 | "messages": { 5 | "home": "Domicile", 6 | "about": "Sur", 7 | "route-about": "sur", 8 | "topics": "Sujets", 9 | "route-topics": "sujets", 10 | "topics-rendering": "Rendu avec React", 11 | "topics-components": "Composants", 12 | "topics-propsVState": "Props v. Etat", 13 | "topics-selectTopic": "S'il vous plaît choisir un sujet", 14 | "en": "Anglais", 15 | "fr": "Francais", 16 | "de": "Almande" 17 | } 18 | } -------------------------------------------------------------------------------- /src/app/App.css: -------------------------------------------------------------------------------- 1 | .container { 2 | text-align: left; 3 | } 4 | 5 | .container header { 6 | background-color: #333333; 7 | overflow: hidden; 8 | } 9 | 10 | .container header ul { 11 | margin: 0; 12 | padding: 0; 13 | list-style-type: none; 14 | } 15 | 16 | .container header ul li { 17 | float: left; 18 | } 19 | 20 | .container header ul li a { 21 | display: block; 22 | color: white; 23 | text-align: center; 24 | padding: 16px; 25 | text-decoration: none; 26 | } 27 | 28 | .container header ul li a:hover { 29 | background-color: #111111; 30 | } 31 | 32 | .container header select { 33 | float: right; 34 | display: block; 35 | margin: 16px; 36 | } 37 | 38 | .container section { 39 | padding: 0 16px; 40 | } 41 | -------------------------------------------------------------------------------- /src/app/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage, intlShape } from 'react-intl'; 3 | import { Match, Link } from 'react-router' 4 | import intl from '../intl/intl.store' 5 | import './App.css'; 6 | 7 | const App = (props, { intl: { formatMessage }}) => ( 8 |
9 |
10 | 25 | 32 |
33 |
34 | 35 | 36 | 37 |
38 |
39 | ) 40 | 41 | export const Home = () => ( 42 |
43 |

44 |
45 | ) 46 | 47 | export const About = () => ( 48 |
49 |

50 |
51 | ) 52 | 53 | export const Topic = ({ params }) => ( 54 |
55 |

{params.topicId}

56 |
57 | ) 58 | 59 | export const Topics = ({ pathname }) => ( 60 |
61 |

62 | 79 | 80 | 81 | ( 82 |

83 | )}/> 84 |
85 | ) 86 | 87 | App.contextTypes = { 88 | intl: intlShape 89 | } 90 | 91 | export default App; 92 | -------------------------------------------------------------------------------- /src/app/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto'); 2 | 3 | body { 4 | font-family: "Roboto", Arial; 5 | font-weight: 200; 6 | display: block; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | h1, h2, h3 { 12 | font-weight: 100; 13 | } 14 | 15 | a { 16 | color: hsl(200, 50%, 50%); 17 | } 18 | 19 | a.active { 20 | color: hsl(20, 50%, 50%); 21 | } 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router' 4 | import { Provider } from 'mobx-react' 5 | import IntlWrapper from './intl/IntlWrapper' 6 | import intl from './intl/intl.store' 7 | import App from './app/App' 8 | 9 | import './index.css' 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | , 19 | document.getElementById('root') 20 | ) 21 | -------------------------------------------------------------------------------- /src/intl/IntlWrapper.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { IntlProvider } from 'react-intl' 3 | import { observer } from "mobx-react" 4 | 5 | const IntlWrapper = ({ children, intl }) => ( 6 | 7 | {children} 8 | 9 | ) 10 | 11 | IntlWrapper.propTypes = { 12 | children: PropTypes.element.isRequired, 13 | intl: PropTypes.object.isRequired 14 | } 15 | 16 | export default observer(['intl'], IntlWrapper) -------------------------------------------------------------------------------- /src/intl/intl.store.js: -------------------------------------------------------------------------------- 1 | import { extendObservable } from 'mobx' 2 | import en from '../../public/intl/en.json' 3 | import './setup' 4 | 5 | class Intl { 6 | constructor() { 7 | this.switchLocale = this.switchLocale.bind(this) 8 | extendObservable(this, { 9 | locale: 'en', 10 | loading: 'false', 11 | data: en 12 | }); 13 | } 14 | 15 | /** 16 | * @param locale 17 | */ 18 | switchLocale(locale = 'en') { 19 | const url = `/intl/${locale}.json` 20 | this.loading = true 21 | return fetch(url) 22 | .then(resp => resp.json()) 23 | .then(json => this.setLocale(json)) 24 | } 25 | 26 | /** 27 | * @param messages 28 | * @param local 29 | */ 30 | setLocale(json) { 31 | this.locale = json.locale 32 | this.loading = false 33 | this.data = json 34 | } 35 | } 36 | 37 | let store = new Intl() 38 | export default store -------------------------------------------------------------------------------- /src/intl/setup.js: -------------------------------------------------------------------------------- 1 | import areIntlLocalesSupported from 'intl-locales-supported'; 2 | import { addLocaleData } from 'react-intl'; 3 | import Intl from 'intl'; 4 | 5 | export const enabledLanguages = [ 6 | 'en', 7 | 'fr', 8 | 'de' 9 | ] 10 | 11 | // here we need to bring in the 'Intl' browser polyfill, all Intl.~locale 12 | // language specific polyfills, and react-intl's langauge specific data. 13 | if (global.Intl) { 14 | // Determine if the built-in `Intl` has the locale data we need. 15 | if (!areIntlLocalesSupported(enabledLanguages)) { 16 | // `Intl` exists, but it doesn't have the data we need, so load the 17 | // polyfill and patch the constructors we need with the polyfill's. 18 | global.Intl.NumberFormat = Intl.NumberFormat; 19 | global.Intl.DateTimeFormat = Intl.DateTimeFormat; 20 | } 21 | } else { 22 | // No `Intl`, so use and load the polyfill. 23 | global.Intl = Intl; 24 | } 25 | 26 | // the language specific Intl polyfills and react-intl language data are pretty 27 | // small (~2k each), so we'll add to bundle and not worry about loading on demand. 28 | // On the other hand, the language resource bundles for each language are pretty 29 | // big, so we'll lazy load these as needed 30 | import 'intl/locale-data/jsonp/en'; 31 | import 'intl/locale-data/jsonp/fr'; 32 | import 'intl/locale-data/jsonp/de'; 33 | 34 | import en from 'react-intl/locale-data/en' 35 | import fr from 'react-intl/locale-data/fr' 36 | import de from 'react-intl/locale-data/de' 37 | 38 | [en, fr, de].forEach(addLocaleData) 39 | 40 | --------------------------------------------------------------------------------