rosetta
Version:
A general purpose internationalization library in 298 bytes
362 lines (256 loc) • 10.2 kB
Markdown
<div align="center">
<img src="logo.jpg" alt="rosetta" height="130" />
</div>
<div align="center">
<a href="https://npmjs.org/package/rosetta">
<img src="https://badgen.now.sh/npm/v/rosetta" alt="version" />
</a>
<a href="https://github.com/lukeed/rosetta/actions">
<img src="https://github.com/lukeed/rosetta/workflows/CI/badge.svg" alt="CI" />
</a>
<a href="https://codecov.io/gh/lukeed/rosetta">
<img src="https://badgen.net/codecov/c/github/lukeed/rosetta" alt="codecov" />
</a>
</div>
<div align="center">A general purpose internationalization library in 298 bytes!</div>
## Features
* Simple and Familiar API
* Unobstrusive and Unopinionated
* Less than 300 bytes – including dependencies!
## Install
```
$ npm install --save rosetta
```
## Usage
```js
import rosetta from 'rosetta';
const i18n = rosetta({
en: {
intro: {
welcome: 'Welcome, {{username}}!',
text: 'I hope you find this useful.',
},
support(obj) {
let hour = Math.floor(Math.random() * 3) + 9;
let str = `For questions, I'm available on ${obj.date.toLocaleDateString()}`;
str += `, any time after ${hour}:00.`
return str;
}
}
});
// set default language
i18n.locale('en');
// add new language
i18n.set('pt', {
intro: {
welcome: obj => `Benvind${obj.feminine ? 'a' : 'o'}, ${obj.username}!`,
text: 'Espero que você ache isso útil.'
}
});
// append extra key(s) to existing language
i18n.set('pt', {
support(obj) {
let hour = Math.floor(Math.random() * 3) + 9;
let str = `Se tiver perguntas, estou disponível em ${obj.date.toLocaleDateString()}`;
str += `, qualquer hora depois às ${hour}:00.`
return str;
}
});
const data = {
feminine: false,
username: 'lukeed',
date: new Date()
};
// Retrieve translations
// NOTE: Relies on "en" default
i18n.t('intro.welcome', data); //=> 'Welcome, lukeed!'
i18n.t('intro.text', data); //=> 'I hope you find this useful.'
i18n.t('support', data); //=> 'For questions, I'm available on 4/8/2020, any time after 11:00.'
// Retrieve translations w/ lang override
i18n.t('intro.welcome', data, 'pt'); //=> 'Benvindo, lukeed!'
// Change default language key
i18n.locale('pt');
// Retrieve translations w/ new defaults
i18n.t('intro.text', data); //=> 'Espero que você ache isso útil.'
i18n.t('intro.text', data, 'en'); //=> 'I hope you find this useful.'
```
## API
### rosetta(dict?)
Returns: `Rosetta`
Initializes a new `Rosetta` instance.<br>You may optionally provide an initial translation object.
### rosetta.locale(lang?)
Returns: `String`
Sets the language code for the `Rosetta` instance.<br>This will cause all [`rossetta.t()`](#rosettatkey-params-lang) lookups to assume this `lang` code.
The function will return the currently active `lang` code. This means that a setting a new value will reply with the same value. Additionally, calling `locale()` without any argument will return the `lang` code that the `Rosetta` instance was last given.
#### lang
Type: `String`<br>
Required: `false`
The language code to choose.<br>
If `locale()` is called without an argument (or with a falsey value), then the current `lang` code is returned.
### rosetta.set(lang, table)
Merge (or override) translation keys into the `lang` collection.
#### lang
Type: `String`
The language code to target.
#### table
Type: `Object`
A new record of key-values to merge into the `lang`'s dictionary.
Each key within the `table` can correspond to a function or a string template.
When using a function, it will receive the entire data input (see [`params`](#params)).<br>You are required to ensure the function returns a (string) value of your liking.
When using a string template, anything within double curly brackets (`{{ example }}`) will be interpreted as a key path and interpolated via [`templite`](https://github.com/lukeed/templite). The key path can use dot-notation to access nested values from the data input (see [`params`](#params)). Additionally, if a key path did not resolve to a value, an empty string is injected.
```js
const ctx = rosetta({
en: {
foo: (obj) => `function sees "${obj.value || '~DEFAULT~'}"`,
bar: 'template sees "{{value}}"'
}
});
ctx.t('foo', {}, 'en');
//=> 'function sees "~DEFAULT~"
ctx.t('foo', { value: 123 }, 'en');
//=> 'function sees "123"
ctx.t('bar', {}, 'en');
//=> 'template sees ""
ctx.t('bar', { value: 123 }, 'en');
//=> 'template sees "123"
```
### rosetta.table(lang)
Returns: `Object` or `undefined`
Retrieve the the `lang`'s full dictionary/table of translation keys.
If the language does not exist (aka, no translations have been provided for it), you'll receive `undefined`.<br>Otherwise, you'll receive the full object as it exists within the `Rosetta` instance. See [`table`](#table).
> **Important:** Manipulating this object is any way will mutate and affect your `Rosetta` instance. Be careful!
#### lang
Type: `String`
The language code's table to retrieve.
### rosetta.t(key, params?, lang?)
Returns: `String`
Retrieve the value for a given `key`.
> **Important:** In the normal/default mode, an empty string will be returned for unknown keys.<br>Conversely, in ["debug" mode](#debugging), an error message will be printed and `undefined` will be returned for unknown keys.
#### key
Type: `String` or `Array<String|Number>`
The identifier to retrieve.
A `key` can access nested properties via:
* a string that with dot notation — `'foo.bar[1].baz'`
* an array of individual key segments — `['foo', 'bar', 1, 'baz']`
> **Important:** You are expected to know & traverse your own dictionary structure correctly.
```js
const ctx = rosetta({
en: {
fruits: {
apple: 'apple',
}
}
});
ctx.locale('en');
ctx.t('fruits.apple'); //=> 'apple'
ctx.t(['fruits', 'apple']); //=> 'apple'
```
### params
Type: `any`<br>
Optional: `true`
The data object argument to pass your dictionary keys' string templates and/or functions.
> **Note:** If your *string template* tries to access a key that doesn't exist, an empty string is injected.
```js
const ctx = rosetta({
es: {
hello: '¡Hola {{name}}!'
},
en: {
hello(obj) {
return obj.name === 'lukeed' ? 'wazzzuppp' : `Hello, ${obj.name}!`;
},
},
pt: {
hello: 'Oi {{person}}, tudo bem?' // <-- key is wrong
},
});
const user1 = { name: 'lukeed' };
const user2 = { name: 'Billy' };
ctx.t('hello', user1, 'es'); //=> '¡Hola lukeed!'
ctx.t('hello', user1, 'en'); //=> 'wazzzuppp'
ctx.t('hello', user2, 'en'); //=> 'Hello, Billy!'
ctx.t('hello', user1, 'pt'); //=> 'Oi , tudo bem?'
```
### lang
Type: `String`<br>
Optional: `true`
A language code override without changing the entire `Rosetta` instance's default language.
```js
const ctx = rosetta();
ctx.locale('en'); //=> set default
ctx.t('greeting', 'lukeed');
//=> (en) 'Hello lukeed!'
ctx.t('greeting', 'lukeed', 'es');
//=> (es) '¡Hola lukeed!'
ctx.t('bye');
//=> (en) 'Cya'
```
## Debugging
There is a "debug" mode included for **development** environments.
The **only** difference with "debug" mode is that [`rossetta.t()`](#rosettatkey-params-lang) will log an error to the console when attempting to access a `key` that does not exist. Conversely, the main/default runtime will quietly return an an empty string for consistent output.
Otherwise, the [API](#api) is _exactly_ the same as the main/default export!<br>This makes it easy to alias or swap the versions for development vs production bundles. Checkout the [Configuration](#configuration) section below for recipes.
```js
// debug mode
import rosetta from 'rosetta/debug';
const i18n = rosetta({
en: {
hello: 'hello'
}
});
i18n.locale('en');
i18n.t('hello');
//=> 'hello'
i18n.t('foobar');
// [rosetta] Missing the "foobar" key within the "en" dictionary
//=> undefined
```
> **Note:** With the non-"debug" runtime, an empty string would be returned for the `foobar` key.
#### Configuration
Here are quick configuration recipes for Rollup and webpack that allow you to choose the right version of `rosetta` for your current environment _without changing you application code_.
With both recipes, you will import `rosetta` like this:
```js
import rosetta from 'rosetta';
```
It is up to the bundler to change what `'rosetta'` resolves to...
***Rollup***
You will need to install [`/plugin-alias`](https://github.com/rollup/plugins/tree/master/packages/alias) before continuing.
```js
const isDev = /*custom logic*/ || !!process.env.ROLLUP_WATCH;
export default {
// ...,
plugins: [
// ...
require('/plugin-alias')({
entries: {
rosetta: isDev ? 'rosetta/debug' : 'rosetta'
}
})
]
}
```
***webpack***
The ability to add aliases within webpack comes by default.<br>One simply needs to add a [`resolve.alias`](https://webpack.js.org/configuration/resolve/#resolvealias) value depending on the environment:
```js
const isDev = /*specific to your config*/;
module.exports = {
//...,
resolve: {
alias: {
// ...,
rosetta: isDev ? 'rosetta/debug' : 'rosetta'
}
}
}
```
## Runtime Support
The library makes use of [Object shorthand methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions#Browser_compatibility) and [`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Browser_compatibility).<br>This yields the following support matrix:
| Chrome | Safari | Firefox | Edge | IE | Node.js |
|:---:|:--:|:---:|:---:|:---:|:----:|
| 45+ | 9+ | 34+ | 12+ | :x: | 4.0+ |
If you need to support older platforms, simply attach `rosetta` to your project's Babel (or similar) configuration.
## Examples
* [**Using Next.js**](https://github.com/zeit/next.js/tree/canary/examples/with-i18n-rosetta) — Thank you [@SharpTech](https://github.com/StarpTech)<br>_Official Next.js example using React Hooks and Context to provide SSR, SSG, CSR compatible i18n solutions._
## Credits
Thank you [@7sempra](https://github.com/7sempra) for gifting the `rosetta` name on npm.
## License
MIT © [Luke Edwards](https://lukeed.com)