react-intl-db
Advanced tools
Comparing version 0.0.2 to 0.0.3
{ | ||
"name": "react-intl-db", | ||
"version": "0.0.2", | ||
"version": "0.0.3", | ||
"description": "Load app translations automatically partitioned by domain", | ||
@@ -5,0 +5,0 @@ "main": "lib/domaindb.js", |
129
README.txt
react-intl-db | ||
============= | ||
Load i18n domains for Format.js's React integration. | ||
Introduction | ||
------------ | ||
To install the required dependencies:: | ||
This package contains i18n domain and message id support for the | ||
[React integration](http://formatjs.io/react/) of | ||
[Format.js](http://formatjs.io). It also makes it possible to load | ||
translation messages dynamically from a backend or other source. | ||
$ npm install | ||
Usage | ||
----- | ||
First we can define a loader that shows how to load the messages from | ||
the server. It should return a `Promise`: | ||
function loader(localeId, domainId) { | ||
// return a promise with the messages | ||
} | ||
The loader gets the id of the locale to be loaded and the id of the | ||
domain. The locale is an ISO locale; something like `en-US`. The | ||
domain id you make up yourself. Typically each UI application would | ||
use its own separate domain string. | ||
The loader should return a promise that resolves to an object with | ||
message id keys and message values, for this particular locale and | ||
domain. The loader could for instance load its information from the | ||
server: | ||
function loader(localeId, domainId) { | ||
return fetch(`http://example.com/i18n/${localeId}/${domainId}`); | ||
} | ||
The next step is to create an `IntDomainDatabase` with this loader: | ||
import {IntlDomainDatabase} from `react-intl-db`; | ||
i18ndb = new IntlDomainDatabase(loader); | ||
You can supply the `i18ndb` with default messages as well. These | ||
messages are used when no loader argument is passed in, or if the | ||
loader returns a promise that resolves to `null` or `undefined` for | ||
that domain. | ||
i18ndb.defaultMessages({ | ||
domainId: 'mydomain', | ||
messages: { | ||
'hello': "Hello world!", | ||
'photos': ('{name} took {numPhotos, plural,' + | ||
' =0 {no photos}' + | ||
' =1 {one photo}' + | ||
' other {# photos}' + | ||
'} on {takenDate, date, long}.') | ||
} | ||
}) | ||
We can now start using this i18ndb with React. You first need to make | ||
the root component of the application aware of the i18n db: | ||
const IntlApp = i18ndb.makeIntl(App) | ||
You now have a new `IntlApp` React component that is like `App``, but | ||
takes the `locales` prop. This should contain the locale you want | ||
the application to use: | ||
<IntlApp locales="en-US" /> | ||
`IntlApp` now also makes sure that the right messages are loaded using | ||
the loader before rendering begins. | ||
We can now create a `Format` component for a particular domain that a | ||
UI application can then use: | ||
const Format = i18ndb.makeFormat('myapp') | ||
The argument to `makeFormat` is the name of the application's domain. | ||
Once you have the `Format` component you can use it with a `messageId` prop: | ||
<Format messageId="hello" /> | ||
When you use this in code, the system automatically looks up the message id | ||
`hello` for the domain `myapp` and inserts it. | ||
Some messages take variables. You can pass them in like you do for | ||
`FormattedMessage` from `react-intl`: | ||
<Format | ||
messageId="photos" | ||
name="Annie" | ||
numPhotos={1000} | ||
takenDate={Date.now()} /> | ||
Sometimes you need to be able render a formatted message in code, | ||
for instance when you want to set the value of a prop. You can do by | ||
creating a helper function: | ||
const formatStr = i18ndb.makeFormatStr('myapp') | ||
You use it by passing the component (typically `this`) as the first | ||
argument, as it needs this to obtain the locale information from the | ||
context: | ||
render() { | ||
return <input value={formatStr(this, 'myMessageId')} />; | ||
} | ||
Limitation | ||
---------- | ||
Currently `react-intl-db` only properly supports a single locale per | ||
application, not a list of locales. You can instead make sure the | ||
right fallback is happening in the loader. I'm also happy to receive | ||
code that lists this limitation! | ||
Example application | ||
------------------- | ||
There is example code included in `src/example.jsx`. | ||
First install the required dependencies: | ||
$ npm install | ||
To try this out can use the webpack-dev-server, which automatically | ||
rebuilds the bundle and serves the content in ``build`` like this:: | ||
rebuilds the bundle and serves the content in ``build`` like this: | ||
$ webpack-dev-server --progress --colors --content-base build | ||
$ webpack-dev-server --progress --colors --content-base build | ||
You can then access the app http://localhost:8080 | ||
You can then access the example app on http://localhost:8080 | ||
@@ -6,4 +6,4 @@ import React from 'react'; | ||
export class IntlDomainDatabase { | ||
constructor(defaultDomains, loader) { | ||
this.defaultDomains = defaultDomains; | ||
constructor(loader) { | ||
this.defaultDomains = {}; | ||
this.locales = {}; | ||
@@ -16,2 +16,5 @@ this.loader = loader; | ||
} | ||
defaultMessages(data) { | ||
this.defaultDomains[data.domainId] = data.messages; | ||
} | ||
loadMessages(localeId, domainId) { | ||
@@ -27,4 +30,8 @@ let domains = this.locales[localeId]; | ||
if (this.loader === undefined) { | ||
return Promise.reject(new Error( | ||
"Loader not defined and cannot find domain: " + domainId)); | ||
const messages = this.defaultDomains[domainId]; | ||
if (!messages) { | ||
return Promise.reject(new Error( | ||
"Loader not defined and cannot find domain: " + domainId)); | ||
} | ||
return Promise.resolve(messages); | ||
} | ||
@@ -63,2 +70,25 @@ return this.loader(localeId, domainId).then(messages => { | ||
} | ||
getMessageById(localeId, domainId, path) { | ||
const messages = this.getMessages(localeId, domainId); | ||
const pathParts = path.split('.'); | ||
let message; | ||
try { | ||
message = pathParts.reduce((obj, pathPart) => { | ||
return obj[pathPart]; | ||
}, messages); | ||
} finally { | ||
if (message === undefined) { | ||
throw new ReferenceError( | ||
'Could not find Intl message: ' + path); | ||
} | ||
} | ||
return message; | ||
} | ||
makeFormatStr(domainId) { | ||
const db = this; | ||
this.neededDomainIds.add(domainId); | ||
return function(component, path) { | ||
return db.getMessageById(getLocaleId(component), domainId, path); | ||
}; | ||
} | ||
makeFormat(domainId) { | ||
@@ -75,23 +105,3 @@ const db = this; | ||
getMessageById(path) { | ||
const locales = this.props.locales || this.context.locales; | ||
let localeId; | ||
if (Array.isArray(locales)) { | ||
localeId = locales[0]; | ||
} else { | ||
localeId = locales; | ||
} | ||
const messages = db.getMessages(localeId, domainId); | ||
const pathParts = path.split('.'); | ||
let message; | ||
try { | ||
message = pathParts.reduce((obj, pathPart) => { | ||
return obj[pathPart]; | ||
}, messages); | ||
} finally { | ||
if (message === undefined) { | ||
throw new ReferenceError( | ||
'Could not find Intl message: ' + path); | ||
} | ||
} | ||
return message; | ||
return db.getMessageById(getLocaleId(this), domainId, path); | ||
}, | ||
@@ -130,1 +140,9 @@ render() { | ||
} | ||
function getLocaleId(component) { | ||
const locales = component.props.locales || component.context.locales; | ||
if (Array.isArray(locales)) { | ||
return locales[0]; | ||
} | ||
return locales; | ||
} |
@@ -5,3 +5,4 @@ import chai from 'chai'; | ||
import dummy from 'intl'; | ||
import {FormattedMessage} from 'react-intl'; | ||
import {FormattedMessage } from 'react-intl'; | ||
import ReactIntl from 'react-intl'; | ||
import { IntlDomainDatabase } from '../src/domaindb'; | ||
@@ -16,3 +17,3 @@ | ||
} | ||
const db = new IntlDomainDatabase({}, myloader); | ||
const db = new IntlDomainDatabase(myloader); | ||
return db.loadMessages('en-US', 'foo').then((messages) => { | ||
@@ -28,3 +29,3 @@ assert.deepEqual(messages, {'foo': 'bar'}); | ||
} | ||
const db = new IntlDomainDatabase({}, myloader); | ||
const db = new IntlDomainDatabase(myloader); | ||
db.loadMessages('en-US', 'foo').then((messages) => { | ||
@@ -45,3 +46,3 @@ assert.equal(loads, 1); | ||
} | ||
const db = new IntlDomainDatabase({}, myloader); | ||
const db = new IntlDomainDatabase(myloader); | ||
db.loadMessages('en-US', 'a').then((messages) => { | ||
@@ -74,3 +75,3 @@ assert.equal(loads.a, 1); | ||
} | ||
const db = new IntlDomainDatabase({}, myloader); | ||
const db = new IntlDomainDatabase(myloader); | ||
db.loadMessages('en', 'a').then((messages) => { | ||
@@ -93,10 +94,10 @@ assert.equal(loads.en.a, 1); | ||
test("default domains", function(done) { | ||
let defaultDomains = { | ||
'a': {'aDefault': 'A_DEFAULT'}, | ||
'b': {'bDefault': 'B_DEFAULT'} | ||
}; | ||
function myloader(localeId, domainId) { | ||
return Promise.resolve(null); | ||
} | ||
const db = new IntlDomainDatabase(defaultDomains, myloader); | ||
const db = new IntlDomainDatabase(myloader); | ||
db.defaultMessages({domainId: 'a', | ||
messages: {'aDefault': 'A_DEFAULT'}}); | ||
db.defaultMessages({domainId: 'b', | ||
messages: {'bDefault': 'B_DEFAULT'}}); | ||
db.loadMessages('en-US', 'a').then((messages) => { | ||
@@ -108,3 +109,3 @@ assert.deepEqual(messages, {'aDefault': 'A_DEFAULT'}); | ||
test("no loader", function(done) { | ||
const db = new IntlDomainDatabase({}); | ||
const db = new IntlDomainDatabase(); | ||
db.loadMessages('en-US', 'a').catch((e) => { | ||
@@ -116,2 +117,11 @@ assert.equal(e.message, | ||
}); | ||
test("no loader with default domains", function(done) { | ||
const db = new IntlDomainDatabase(); | ||
db.defaultMessages({domainId: 'a', | ||
messages: {'aDefault': 'A_DEFAULT'}}); | ||
db.loadMessages('en-US', 'a').then((messages) => { | ||
assert.deepEqual(messages, {'aDefault': 'A_DEFAULT'}); | ||
done(); | ||
}); | ||
}); | ||
test("no messages for domain", function(done) { | ||
@@ -121,3 +131,3 @@ function myloader(localeId, domainId) { | ||
} | ||
const db = new IntlDomainDatabase({}, myloader); | ||
const db = new IntlDomainDatabase(myloader); | ||
db.loadMessages('en-US', 'a').catch((e) => { | ||
@@ -135,3 +145,3 @@ assert.equal(e.message, | ||
} | ||
const db = new IntlDomainDatabase({}, myloader); | ||
const db = new IntlDomainDatabase(myloader); | ||
@@ -149,3 +159,5 @@ db.neededDomainIds.add('a'); | ||
const renderer = React.addons.TestUtils.createRenderer(); | ||
const db = new IntlDomainDatabase({'a': {'foo': 'bar'}}); | ||
const db = new IntlDomainDatabase(); | ||
db.defaultMessages({domainId: 'a', | ||
messages: {'foo': 'bar'}}); | ||
const Format = React.createFactory(db.makeFormat('a')); | ||
@@ -162,2 +174,25 @@ const formatted = Format({'messageId': 'foo', 'locales': 'en-US'}); | ||
test("makeFormatStr with default", function() { | ||
const renderer = React.addons.TestUtils.createRenderer(); | ||
const db = new IntlDomainDatabase(); | ||
db.defaultMessages({domainId: 'a', | ||
messages: {'foo': 'bar'}}); | ||
const formatStr = db.makeFormatStr('a'); | ||
const Element = React.createClass({ | ||
mixins: [ReactIntl.IntlMixin], | ||
render() { | ||
return formatStr(this, 'foo'); | ||
} | ||
}); | ||
const ElementFactory = React.createFactory(Element); | ||
const formatted = ElementFactory({'locales': 'en-US'}); | ||
renderer.render(formatted); | ||
const output = renderer.getRenderOutput(); | ||
assert.deepEqual( | ||
output, | ||
'bar'); | ||
}); | ||
test("makeFormat with loader", function(done) { | ||
@@ -180,3 +215,3 @@ const locales = { | ||
} | ||
const db = new IntlDomainDatabase({}, myloader); | ||
const db = new IntlDomainDatabase(myloader); | ||
@@ -183,0 +218,0 @@ const renderer = React.addons.TestUtils.createRenderer(); |
2076368
26554
135