preact-i18n 🌎
![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fsynacor%2Fpreact-i18n.svg?type=shield)
Simple localization for Preact.
- Tiny: about 1.3kb gzipped
- Supports dictionary and key scopes/namespaces while maintaining a global dictionary
- Supports nested dictionaries:
- Wrap your component in a default dictionary and scope key
- Wrap it again later on (in an app!) to override the defaults
- Supports pluralization of strings using nested objects.
- Supports template
{{fields}}
in definition values - Has a companion ESLint plugin to help catch bugs early
Preact Version Support
By default, the master
branch of this repo supports preact 9 and below, and is published in normal patch/minor/major releases to the latest
tag in npm. Support for preact X (versions 10+ of preact) is handled in the preactX
branch and are always published to the preactx
tag in npm. When preact X obtains widespread adoption, the master
branch of this project will support preact X and a new major version under latest
tag will be published to in npm.
Installation
npm install --save preact-i18n
npm install --save-dev @types/preact-i18n
Getting Started
- Create a definition. Typically JSON files, we'll call ours
fr.json
:
{
"news": {
"title": "Nouvelles du Monde",
"totalStories": {
"none": "Aucun article",
"one": "Un article",
"many": "{{count}} articles"
}
}
}
- Expose the definition to your whole app via
<IntlProvider>
:
import { IntlProvider } from 'preact-i18n';
import definition from './fr.json';
render(
<IntlProvider definition={definition}>
<App />
</IntlProvider>
);
- Use
<Text />
to translate string literals:
import { Text } from 'preact-i18n';
const App = ({ stories=[] }) => (
<div class="app">
<h1>
{/* Default fallback text example: */}
<Text id="news.title">World News</Text>
</h1>
<footer>
{/* Pluralization example: */}
<Text
id="news.totalStories"
plural={stories.length}
fields={{
count: stories.length
}}
/>
</footer>
</div>
);
That's it!
Fallback Text
Rendering our example app with an empty definition (or without the Provider) will attempt to use any text contained within <Text>..</Text>
as fallback text.
In our example, this would mean rendering without a definition for news.title
would produce <h1>World News</h1>
.
If we provide a definition that has a title
key inside a news
object, that value will be rendered instead.
Pluralization and Templating
In our example, <footer>
is using <Text>
as a convenient way to do pluralization and templating. In our definition, news.totalStories
is an Object with pluralization keys. The values in that object will be selected based on an integer plural
prop passed to <Text>
.
Any definition value (including pluralization values) can contain {{field}}
placeholders. These placeholders get replaced with matched keys in an object passed as the fields
prop. In our example, the "many" plural form is such a template - it will render "5 articles"
when fields={{ count: 5 }}
.
The available forms for specifying pluralization values are as follows:
"key": { "singular":"apple", "plural":"apples" }
"key": { "none":"no apples", "one":"apple", "many":"apples" }
"key": { "zero":"no apples", "one":"apple", "other":"apples" }
"key": ["apples", "apple"]
Taking <Text id="news.totalStories" ..>
from our example:
<.. plural={0}>
renders Aucun article
(no articles)<.. plural={1}>
renders Un article
(one article)<.. plural={2} fields={{ count: 2 }}>
renders 2 articles
<.. plural={3} fields={{ count: 3 }}>
renders 3 articles
In addition to <Text>
, withText()
and <Localizer>
provide ways to translate more than just display text - HTML attributes, component props, arbitrary Strings, etc.
ESLint Plugin
A companion ESLint plugin exists, eslint-plugin-preact-i18n, which has several rules that help spot common issues like un-i18n'd text, misconfigured tags, and missing keys, that are beneficial in spotting defects early and ensuring that your application is properly i18n'd.
API
Table of Contents
IntlProvider
<IntlProvider>
is a nestable internationalization definition provider.
It exposes an Intl scope & definition into the tree,
making them available to descendant components.
Note: When nested, gives precedence to keys higher up the tree!
This means lower-level components can set their defaults by wrapping themselves
in an <IntlProvider>
, but still remain localizable by their parent components.
Parameters
props
props.scope
String? Nest definition
under a root key, and set the active scope for the tree (essentially prefixing all <Text />
keys).props.mark
Boolean If true
, all <Text>
elements will be shown with a red/green background indicating whether they have valid Intl keys. (optional, default false
)props.definition
Object Merge the given definition into the current intl definition, giving the current definition precedence (i.e., only adding keys, acting as defaults) (optional, default {}
)
Examples
let definition = {
foo: 'Le Feux'
};
<IntlProvider scope="weather" definition={definition}>
<Text key="foo">The Foo</Text>
</IntlProvider>
"Le Feux"
Localizer
<Localizer />
is a Compositional Component.
It "renders" out any <Text />
values in its child's props.
Parameters
props
Object
props.children
Object Child components with props to localize.
context
Object
context.intl
Object [internal] dictionary and scope info
Examples
<Localizer>
<input placeholder={<Text id="username.placeholder" />} />
</Localizer>
<input placeholder="foo" />
<Localizer>
<abbr title={<Text id="oss-title">Open Source Software</Text>}>
<Text id="oss">OSS</Text>
</abbr>
</Localizer>
<abbr title="Open Source Software">OSS</abbr>
MarkupText
<MarkupText>
is just like Text but it can also contain html markup in rendered strings. It wraps its contents in a <span>
tag.
Parameters
props
Object props
props.id
String Key to look up in intl dictionary, within any parent scopes ($scope1.$scope2.$id
)props.fields
Object Values to inject into template {{fields}}
. Values in the fields
object will be coerced to strings, with the exception of <Text/>
nodes which will be resolved to their translated value (optional, default {}
)props.plural
Number? Integer "count", used to select plural forms
context
Object
context.intl
Object [internal] dictionary and scope info
Examples
<MarkupText id="foo"><b>The Foo</b></MarkupText>
<span><b>The Foo</b></span>
<IntlProvider definition={{ foo:'Le Feux <b>{{bar}}</b>' }}>
<MarkupText id="foo" fields={{ bar: 'BEAR' }}>The Foo</MarkupText>
</IntlProvider>
<span>Le Feux <b>BEAR</b></span>
<IntlProvider scope="weather" definition={{ foo:'Le <a href="http://foo.com">Feux</a>' }}>
<MarkupText id="foo">The Foo</MarkupText>
</IntlProvider>
<span>Le <a href="http://foo.com">Feux</a></span>
<div><MarkupText /></div>
<div/>
Text
<Text>
renders internationalized text.
It attempts to look up translated values from a dictionary in context.
Template strings can contain {{field}}
placeholders,
which injects values from the fields
prop.
When string lookup fails, renders its children as fallback text.
Parameters
props
Object props
props.id
String Key to look up in intl dictionary, within any parent scopes ($scope1.$scope2.$id
)props.plural
Number? Integer "count", used to select plural formsprops.fields
Object Values to inject into template {{fields}}
. Values in the fields
object will be coerced to strings, with the exception of <Text/>
nodes which will be resolved to their translated value (optional, default {}
)props.children
context
Object
context.intl
Object [internal] dictionary and scope info
Examples
<Text id="foo">The Foo</Text>
"The Foo"
<IntlProvider definition={{ foo:'Le Feux {{bar}}' }}>
<Text id="foo" fields={{ bar: 'BEAR' }}>The Foo</Text>
</IntlProvider>
"Le Feux BEAR"
<IntlProvider scope="weather" definition={{ foo:'Le Feux' }}>
<Text id="foo">The Foo</Text>
</IntlProvider>
"Le Feux"
withText
@withText()
is a Higher Order Component, often used as a decorator.
It wraps a child component and passes it translations
based on a mapping to the dictionary & scope in context.
Parameters
Examples
@withText({
placeholder: 'user.placeholder'
})
class Foo {
render({ placeholder }) {
return <input placeholder={placeholder} />
}
}
@withText({
placeholder: <Text id="user.placeholder">fallback text</Text>
})
class Foo {
render({ placeholder }) {
return <input placeholder={placeholder} />
}
}
@withText('user.placeholder')
class Foo {
render({ placeholder }) {
return <input placeholder={placeholder} />
}
}
Works with functional components, too
const Foo = withText('user.placeholder')( props =>
<input placeholder={props.placeholder} />
)
getWrappedComponent() returns wrapped child Component
const Foo = () => <div/>;
const WrappedFoo = withText('user.placeholer')(Foo);
WrappedFoo.getWrappedComponent() === Foo;
intl
Higher-order function that creates an <IntlProvider />
wrapper component for the given component. It
takes two forms depending on how many arguments it's given:
It can take a functional form like:
intl(ComponentToWrap, options)
or it can take an annotation form like:
Parameters
Child
options
Object If there are two arguments, the second argument is Passed as props
to <IntlProvider />
options.scope
Nest definition
under a root key, and set the active scope for the tree (essentially prefixing all <Text />
keys).options.definition
Merge the given definition into the current intl definition, giving the current definition precedence (i.e., only adding keys, acting as defaults) (optional, default {}
)
License
![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fsynacor%2Fpreact-i18n.svg?type=large)