Repository: synacor/preact-i18n
Branch: master
Commit: 28ff35cbdbee
Files: 25
Total size: 57.1 KB
Directory structure:
gitextract_qzqk6_j4/
├── .editorconfig
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── karma.conf.js
├── package.json
├── rollup.config.js
├── src/
│ ├── components/
│ │ ├── highlight-i18n.js
│ │ ├── intl-provider.js
│ │ ├── localizer.js
│ │ ├── markup-text.js
│ │ ├── text.js
│ │ └── with-text.js
│ ├── index.js
│ ├── intl.js
│ └── lib/
│ ├── template.js
│ ├── translate-mapping.js
│ ├── translate.js
│ └── util.js
└── test/
├── .eslintrc
├── index.js
└── lib/
├── template.js
├── translate.js
└── util.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,.*rc,*.yml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .gitignore
================================================
/docs
/dist
/test-reports
/node_modules
/npm-debug.log
.DS_Store
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- stable
================================================
FILE: LICENSE
================================================
Copyright (c) 2017, Synacor, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README.md
================================================
# preact-i18n 🌎 [](https://npm.im/preact-i18n) [](https://travis-ci.org/synacor/preact-i18n) [](https://app.fossa.io/projects/git%2Bgithub.com%2Fsynacor%2Fpreact-i18n?ref=badge_shield)
<img src="resources/example.png" width="600" alt="Usage example" />
**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](https://www.npmjs.com/package/eslint-plugin-preact-i18n) to help catch bugs early
* * *
- [Installation](#installation)
- [Getting Started](#getting-started)
- [Fallback Text](#fallback-text)
- [Pluralization and Templating](#pluralization-and-templating)
- [ESLint Plugin](#eslint-plugin)
- [API](#api)
<!-- /MDTOC -->
## 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
```sh
npm install --save preact-i18n
# For TypeScript Definitions
npm install --save-dev @types/preact-i18n
```
## Getting Started
1. Create a definition. Typically JSON files, we'll call ours `fr.json`:
```json
{
"news": {
"title": "Nouvelles du Monde",
"totalStories": {
"none": "Aucun article",
"one": "Un article",
"many": "{{count}} articles"
}
}
}
```
2. Expose the definition to your whole app via `<IntlProvider>`:
```js
import { IntlProvider } from 'preact-i18n';
import definition from './fr.json';
render(
<IntlProvider definition={definition}>
<App />
</IntlProvider>
);
```
3. Use `<Text />` to translate string literals:
```js
import { Text } from 'preact-i18n';
// Assume the "stories" prop is a list of news stories.
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>`](#Text), [`withText()`](#withText) and [`<Localizer>`](#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](https://www.npmjs.com/package/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
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
#### Table of Contents
- [IntlProvider](#intlprovider)
- [Parameters](#parameters)
- [Examples](#examples)
- [Localizer](#localizer)
- [Parameters](#parameters-1)
- [Examples](#examples-1)
- [MarkupText](#markuptext)
- [Parameters](#parameters-2)
- [Examples](#examples-2)
- [Text](#text)
- [Parameters](#parameters-3)
- [Examples](#examples-3)
- [withText](#withtext)
- [Parameters](#parameters-4)
- [Examples](#examples-4)
- [intl](#intl)
- [Parameters](#parameters-5)
### 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](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Nest `definition` under a root key, and set the active scope for the tree (essentially prefixing all `<Text />` keys).
- `props.mark` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/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](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/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
```javascript
// generally imported from a JSON file:
let definition = {
foo: 'Le Feux'
};
<IntlProvider scope="weather" definition={definition}>
<Text key="foo">The Foo</Text>
</IntlProvider>
// This will render the text:
"Le Feux"
```
### Localizer
`<Localizer />` is a Compositional Component.
It "renders" out any `<Text />` values in its child's props.
#### Parameters
- `props` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)**
- `props.children` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** Child components with props to localize.
- `context` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)**
- `context.intl` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** [internal] dictionary and scope info
#### Examples
```javascript
<Localizer>
<input placeholder={<Text id="username.placeholder" />} />
</Localizer>
// produces:
<input placeholder="foo" />
```
```javascript
<Localizer>
<abbr title={<Text id="oss-title">Open Source Software</Text>}>
<Text id="oss">OSS</Text>
</abbr>
</Localizer>
// produces:
<abbr title="Open Source Software">OSS</abbr>
```
### MarkupText
`<MarkupText>` is just like [Text](#text) but it can also contain html markup in rendered strings. It wraps its contents in a `<span>` tag.
#### Parameters
- `props` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** props
- `props.id` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Key to look up in intl dictionary, within any parent scopes (`$scope1.$scope2.$id`)
- `props.fields` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/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](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** Integer "count", used to select plural forms
- `context` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)**
- `context.intl` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** [internal] dictionary and scope info
#### Examples
```javascript
// If there is no dictionary in context..
<MarkupText id="foo"><b>The Foo</b></MarkupText>
// ..produces the vnode:
<span><b>The Foo</b></span>
```
```javascript
// Given a dictionary and some fields..
<IntlProvider definition={{ foo:'Le Feux <b>{{bar}}</b>' }}>
<MarkupText id="foo" fields={{ bar: 'BEAR' }}>The Foo</MarkupText>
</IntlProvider>
// ..produces the vnode:
<span>Le Feux <b>BEAR</b></span>
```
```javascript
// Within a scope, both `id` and the definition are namespaced..
<IntlProvider scope="weather" definition={{ foo:'Le <a href="http://foo.com">Feux</a>' }}>
<MarkupText id="foo">The Foo</MarkupText>
</IntlProvider>
// ..produces the vnode:
<span>Le <a href="http://foo.com">Feux</a></span>
```
```javascript
// renders nothing if there is no key match and no fallback
<div><MarkupText /></div>
// ..produces the vnode:
<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](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** props
- `props.id` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Key to look up in intl dictionary, within any parent scopes (`$scope1.$scope2.$id`)
- `props.plural` **[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** Integer "count", used to select plural forms
- `props.fields` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/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](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)**
- `context.intl` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** [internal] dictionary and scope info
#### Examples
```javascript
// If there is no dictionary in context..
<Text id="foo">The Foo</Text>
// ..produces the text:
"The Foo"
```
```javascript
// Given a dictionary and some fields..
<IntlProvider definition={{ foo:'Le Feux {{bar}}' }}>
<Text id="foo" fields={{ bar: 'BEAR' }}>The Foo</Text>
</IntlProvider>
// ..produces the text:
"Le Feux BEAR"
```
```javascript
// Within a scope, both `id` and the definition are namespaced..
<IntlProvider scope="weather" definition={{ foo:'Le Feux' }}>
<Text id="foo">The Foo</Text>
</IntlProvider>
// ..produces the text:
"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
- `mapping` **([Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object) \| [Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function) \| [String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String))** Maps prop names to intl keys (or `<Text>` nodes).
#### Examples
```javascript
@withText({
placeholder: 'user.placeholder'
})
class Foo {
// now the `placeholder` prop is our localized String:
render({ placeholder }) {
return <input placeholder={placeholder} />
}
}
```
```javascript
@withText({
placeholder: <Text id="user.placeholder">fallback text</Text>
})
class Foo {
render({ placeholder }) {
return <input placeholder={placeholder} />
}
}
```
```javascript
@withText('user.placeholder')
class Foo {
// for Strings/Arrays, the last path segment becomes the prop name:
render({ placeholder }) {
return <input placeholder={placeholder} />
}
}
```
Works with functional components, too
```javascript
const Foo = withText('user.placeholder')( props =>
<input placeholder={props.placeholder} />
)
```
getWrappedComponent() returns wrapped child Component
```javascript
const Foo = () => <div/>;
const WrappedFoo = withText('user.placeholer')(Foo);
WrappedFoo.getWrappedComponent() === Foo; // true
```
### 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](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/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
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fsynacor%2Fpreact-i18n?ref=badge_large)
================================================
FILE: karma.conf.js
================================================
/* eslint-disable */
var path = require('path');
var pkg = require('./package.json');
var REPORTER = process.env.REPORTER || (process.env.ENVIRONMENT==='ci' && 'junit') || '';
module.exports = function(config) {
config.set({
browsers: ['ChromeHeadless'],
frameworks: ['mocha', 'chai-sinon'],
reporters: ['mocha'].concat(REPORTER.split(/[, ]/)).filter(dedupe),
junitReporter: {
outputDir: 'test-reports', // results will be saved as $outputDir/$browserName.xml
suite: require('./package.json').name
},
mochaReporter: { showDiff: true },
files: [
'test/**/*.js'
],
preprocessors: {
'{src,test}/**/*': ['webpack', 'sourcemap']
},
webpack: {
mode: 'development',
devtool: 'inline-source-map',
resolve: {
alias: {
'preact-i18n': path.resolve(__dirname, process.env.TEST_PRODUCTION ? pkg.main : 'src')
}
},
module: {
rules: [{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [ '@babel/env' ],
plugins: [
['@babel/plugin-transform-react-jsx', { pragma: 'h' }]
]
}
}]
},
},
webpackServer: { stats: 'errors-only' }
});
};
// filters out empties && dupes
function dedupe(v, i, arr) { return v && arr.indexOf(v)===i; }
================================================
FILE: package.json
================================================
{
"name": "preact-i18n",
"amdName": "preactLocalize",
"version": "1.5.0",
"description": "Preact internationalization, done simply.",
"source": "src/index.js",
"module": "dist/preact-i18n.esm.js",
"main": "dist/preact-i18n.js",
"umd:main": "dist/preact-i18n.umd.js",
"scripts": {
"test": "npm-run-all --silent -p lint build test:unit -s test:prod",
"lint": "eslint src test",
"test:watch": "npm run test:unit -- --no-single-run",
"test:unit": "karma start karma.conf.js --single-run",
"test:prod": "cross-env TEST_PRODUCTION=true karma start karma.conf.js --single-run",
"build": "npm-run-all --silent clean -p rollup:* -p minify:* -s docs size",
"clean": "rimraf dist && mkdirp dist",
"rollup:cjs": "cross-var rollup -c rollup.config.js -m -f cjs -n $npm_package_amdName -i $npm_package_source -o $npm_package_main",
"rollup:umd": "cross-var rollup -c rollup.config.js -m -f umd -n $npm_package_amdName -i $npm_package_source -o $npm_package_umd_main",
"rollup:esm": "cross-var rollup -c rollup.config.js -m -f es --environment FORMAT:es -n $npm_package_amdName -i $npm_package_source -o $npm_package_module",
"minify:cjs": "cross-var uglifyjs $npm_package_main -c -m toplevel -o $npm_package_main --source-map content=$npm_package_main.map",
"minify:umd": "cross-var uglifyjs $npm_package_umd_main -c -m -o $npm_package_umd_main --source-map content=$npm_package_umd_main.map",
"docs": "documentation readme -q --section API src",
"size": "cross-var echo \"Gzipped Size: $(strip-json-comments --no-whitespace $npm_package_main | gzip-size --raw)\"",
"prepublishOnly": "cross-var npm run build -s && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags"
},
"repository": "synacor/preact-i18n",
"keywords": [
"intl",
"internationalization",
"localization"
],
"homepage": "https://github.com/synacor/preact-i18n",
"authors": [
"Jason Miller <jasonmiller@synacor.com>",
"Bill Neff <billneff79@gmail.com>"
],
"license": "BSD-3-Clause",
"files": [
"src",
"dist"
],
"eslintConfig": {
"extends": "eslint-config-synacor",
"rules": {
"guard-for-in": 0
}
},
"devDependencies": {
"@babel/core": "^7.4.0",
"@babel/plugin-transform-react-jsx": "^7.3.0",
"@babel/preset-env": "^7.4.2",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.5",
"buble": "^0.19.7",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"cross-env": "^5.2.0",
"cross-var": "^1.1.0",
"documentation": "^9.3.1",
"documentation-theme-default": "^3.0.0",
"eslint": "^5.15.3",
"eslint-config-synacor": "^3.0.3",
"gzip-size-cli": "^3.0.0",
"karma": "^4.0.1",
"karma-chai": "^0.1.0",
"karma-chai-as-promised": "^0.1.2",
"karma-chai-sinon": "^0.1.5",
"karma-chrome-launcher": "^2.2.0",
"karma-cli": "^2.0.0",
"karma-junit-reporter": "^1.2.0",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^3.0.5",
"mkdirp": "^0.5.1",
"mocha": "^6.0.2",
"npm-run-all": "^4.1.5",
"preact": "^8.2.5",
"preact-jsx-chai": "^2.2.1",
"rimraf": "^2.6.3",
"rollup": "^1.7.0",
"rollup-plugin-buble": "^0.19.6",
"rollup-plugin-memory": "^3.0.0",
"sinon": "^7.3.0",
"sinon-chai": "^3.3.0",
"strip-json-comments-cli": "^1.0.1",
"uglify-js": "^3.5.1",
"webpack": "^4.29.6"
},
"dependencies": {
"dlv": "^1.1.3"
},
"peerDependencies": {
"preact": "<10"
}
}
================================================
FILE: rollup.config.js
================================================
/*eslint-disable*/
import memory from 'rollup-plugin-memory';
import buble from 'rollup-plugin-buble';
export default function(config) {
let format = config.format;
return {
external: [
'preact',
'dlv'
],
output: {
strict: false,
exports: format==='es' ? null : 'named',
globals: {
preact: 'preact',
dlv: 'dlv'
}
},
plugins: [
format!=='es' && memory({
path: 'src/entry.js',
contents: "export { default } from './index';"
}),
buble({
jsx: 'h'
})
].filter(Boolean)
};
}
================================================
FILE: src/components/highlight-i18n.js
================================================
import { h } from 'preact';
import delve from 'dlv';
/** Highlight/colorize the i18n'd node if `mark` is set on `intl` in context. If not, just returns `value`
*
* @private
* @param {String|VNode} value The l10n'd text/vnode to highlight or pass through
* @param {string} id The key used to lookup the value in the intl dictionary
*/
export function HighlightI18N({ value, id }, { intl }) {
if (intl && intl.mark) {
const dictionaryKey = `dictionary${intl && intl.scope ? `.${intl.scope}` : ''}.${id}`;
return h('mark', {
style: 'background: ' + (
value
? delve(intl, dictionaryKey)
? 'rgba(119,231,117,.5)' // Green = this string is fully internationalized
: 'rgba(229,226,41,.5)' // Yellow = this string does not have a value in the dictionary, but it has a fallback value
: 'rgba(228,147,51,.5)' // Red = this string has no value and no fallback
),
title: id
}, value);
}
return value ;
}
================================================
FILE: src/components/intl-provider.js
================================================
import { h, Component } from 'preact';
import { assign, deepAssign } from '../lib/util';
const URL_FLAG = /[?&#]intl=show/;
/** `<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.
*
* @name IntlProvider
* @param props
* @param {String} [props.scope] Nest `definition` under a root key, and set the active scope for the tree (essentially prefixing all `<Text />` keys).
* @param {Boolean} [props.mark=false] If `true`, all `<Text>` elements will be shown with a red/green background indicating whether they have valid Intl keys.
* @param {Object} [props.definition={}] Merge the given definition into the current intl definition, giving the *current* definition precedence (i.e., only adding keys, acting as defaults)
*
* @example
* // generally imported from a JSON file:
* let definition = {
* foo: 'Le Feux'
* };
*
* <IntlProvider scope="weather" definition={definition}>
* <Text key="foo">The Foo</Text>
* </IntlProvider>
*
* // This will render the text:
* "Le Feux"
*/
export class IntlProvider extends Component {
getChildContext() {
let { scope, definition, mark } = this.props,
intl = assign({}, this.context.intl || {});
// set active scope for the tree if given
if (scope) intl.scope = scope;
// merge definition into current with lower precedence
if (definition) {
intl.dictionary = deepAssign(intl.dictionary || {}, definition);
}
if (mark || (typeof location!=='undefined' && String(location).match(URL_FLAG))) {
intl.mark = true;
}
return { intl };
}
render({ children }) {
return children && children[0] || null;
}
}
================================================
FILE: src/components/localizer.js
================================================
import { cloneElement } from 'preact';
import translateMapping from '../lib/translate-mapping';
/** `<Localizer />` is a Compositional Component.
* It "renders" out any `<Text />` values in its child's props.
*
* @name Localizer
* @param {Object} props
* @param {Object} props.children Child components with props to localize.
* @param {Object} context
* @param {Object} context.intl [internal] dictionary and scope info
* @example
* <Localizer>
* <input placeholder={<Text id="username.placeholder" />} />
* </Localizer>
* // produces:
* <input placeholder="foo" />
*
* @example
* <Localizer>
* <abbr title={<Text id="oss-title">Open Source Software</Text>}>
* <Text id="oss">OSS</Text>
* </abbr>
* </Localizer>
* // produces:
* <abbr title="Open Source Software">OSS</abbr>
*/
export function Localizer({ children }, { intl }) {
let child = children && children[0];
return child && cloneElement(child, translateMapping(child.attributes, intl, true));
}
================================================
FILE: src/components/markup-text.js
================================================
import { h } from 'preact';
import { Text } from './text';
import { Localizer } from './localizer';
import { HighlightI18N } from './highlight-i18n';
/* eslint-disable react/no-danger */
/** `<MarkupText>` is just like {@link Text} but it can also contain html markup in rendered strings. It wraps its contents in a `<span>` tag.
*
* @param {Object} props props
* @param {String} props.id Key to look up in intl dictionary, within any parent scopes (`$scope1.$scope2.$id`)
* @param {Object} [props.fields={}] 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
* @param {Number} [props.plural] Integer "count", used to select plural forms
* @param {Object} context
* @param {Object} context.intl [internal] dictionary and scope info
*
* @example
* // If there is no dictionary in context..
* <MarkupText id="foo"><b>The Foo</b></MarkupText>
* // ..produces the vnode:
* <span><b>The Foo</b></span>
*
* @example
* // Given a dictionary and some fields..
* <IntlProvider definition={{ foo:'Le Feux <b>{{bar}}</b>' }}>
* <MarkupText id="foo" fields={{ bar: 'BEAR' }}>The Foo</MarkupText>
* </IntlProvider>
* // ..produces the vnode:
* <span>Le Feux <b>BEAR</b></span>
*
* @example
* // Within a scope, both `id` and the definition are namespaced..
* <IntlProvider scope="weather" definition={{ foo:'Le <a href="http://foo.com">Feux</a>' }}>
* <MarkupText id="foo">The Foo</MarkupText>
* </IntlProvider>
* // ..produces the vnode:
* <span>Le <a href="http://foo.com">Feux</a></span>
*
* @example
* // renders nothing if there is no key match and no fallback
* <div><MarkupText /></div>
* // ..produces the vnode:
* <div/>
*/
export function MarkupText(props) {
return (
<Localizer>
<Html html={<Text {...props} />} id={props.id} />
</Localizer>
);
}
function Html({ html, id }) {
let value = !html ? html : typeof html === 'string' ? <span dangerouslySetInnerHTML={{ __html: html }} /> : <span>{html}</span> ;
return <HighlightI18N id={id} value={value} />;
}
================================================
FILE: src/components/text.js
================================================
import { h } from 'preact';
import translate from '../lib/translate';
import { HighlightI18N } from './highlight-i18n';
/** `<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.
*
* @param {Object} props props
* @param {String} props.id Key to look up in intl dictionary, within any parent scopes (`$scope1.$scope2.$id`)
* @param {Object} [props.fields={}] 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
* @param {Number} [props.plural] Integer "count", used to select plural forms
* @param {Object} context
* @param {Object} context.intl [internal] dictionary and scope info
*
* @example
* // If there is no dictionary in context..
* <Text id="foo">The Foo</Text>
* // ..produces the text:
* "The Foo"
*
* @example
* // Given a dictionary and some fields..
* <IntlProvider definition={{ foo:'Le Feux {{bar}}' }}>
* <Text id="foo" fields={{ bar: 'BEAR' }}>The Foo</Text>
* </IntlProvider>
* // ..produces the text:
* "Le Feux BEAR"
*
* @example
* // Within a scope, both `id` and the definition are namespaced..
* <IntlProvider scope="weather" definition={{ foo:'Le Feux' }}>
* <Text id="foo">The Foo</Text>
* </IntlProvider>
* // ..produces the text:
* "Le Feux"
*/
export function Text({ id, children, plural, fields }, { intl }) {
let fallback = children && children[0];
let value = translate(
id,
intl && intl.scope,
intl && intl.dictionary,
fields,
plural,
fallback
);
return <HighlightI18N id={id} value={value} />;
}
================================================
FILE: src/components/with-text.js
================================================
import { h } from 'preact';
import translateMapping from '../lib/translate-mapping';
import { assign } from '../lib/util';
/** `@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.
*
* @param {Object|Function|String} mapping Maps prop names to intl keys (or `<Text>` nodes).
*
* @example @withText({
* placeholder: 'user.placeholder'
* })
* class Foo {
* // now the `placeholder` prop is our localized String:
* render({ placeholder }) {
* return <input placeholder={placeholder} />
* }
* }
*
* @example @withText({
* placeholder: <Text id="user.placeholder">fallback text</Text>
* })
* class Foo {
* render({ placeholder }) {
* return <input placeholder={placeholder} />
* }
* }
*
* @example @withText('user.placeholder')
* class Foo {
* // for Strings/Arrays, the last path segment becomes the prop name:
* render({ placeholder }) {
* return <input placeholder={placeholder} />
* }
* }
*
* @example <caption>Works with functional components, too</caption>
* const Foo = withText('user.placeholder')( props =>
* <input placeholder={props.placeholder} />
* )
*
* @example <caption>getWrappedComponent() returns wrapped child Component</caption>
* const Foo = () => <div/>;
* const WrappedFoo = withText('user.placeholer')(Foo);
* WrappedFoo.getWrappedComponent() === Foo; // true
*/
export function withText(mapping) {
return function withTextWrapper(Child) {
function WithTextWrapper(props, context) {
let map = typeof mapping==='function' ? mapping(props, context) : mapping;
let translations = translateMapping(map, context.intl);
return h(Child, assign(assign({}, props), translations));
}
WithTextWrapper.getWrappedComponent = Child && Child.getWrappedComponent || (() => Child);
return WithTextWrapper;
};
}
================================================
FILE: src/index.js
================================================
import { intl } from './intl';
import { IntlProvider } from './components/intl-provider';
import { Text } from './components/text';
import { MarkupText } from './components/markup-text';
import { Localizer } from './components/localizer';
import { withText } from './components/with-text';
export { intl, IntlProvider, Text, MarkupText, Localizer, withText };
intl.intl = intl;
intl.IntlProvider = IntlProvider;
intl.Text = Text;
intl.MarkupText = MarkupText;
intl.Localizer = Localizer;
intl.withText = withText;
export default intl;
================================================
FILE: src/intl.js
================================================
import { h } from 'preact';
import { IntlProvider } from './components/intl-provider';
/**
* 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:
* @intl(options)
* class ComponentToWrap extends Component {}
*
* @param {Component or Object} args[0] If there are two arguments, the first argument is the Component to wrap in `<IntlProvider/>`. If there is just one argument, this is the options object to pass as `props` to `<IntlProvider/>`. See the definition of the options param below for details.
* @param {Object} options If there are two arguments, the second argument is Passed as `props` to `<IntlProvider />`
* @param [options.scope] Nest `definition` under a root key, and set the active scope for the tree (essentially prefixing all `<Text />` keys).
* @param [options.definition={}] Merge the given definition into the current intl definition, giving the *current* definition precedence (i.e., only adding keys, acting as defaults)
*/
export function intl(Child, options) {
if (arguments.length<2) {
options = Child;
return Child => intl(Child, options);
}
function IntlProviderWrapper(props) {
return h(
IntlProvider,
options || {},
h(Child, props)
);
}
IntlProviderWrapper.getWrappedComponent = Child && Child.getWrappedComponent || (() => Child);
return IntlProviderWrapper;
}
================================================
FILE: src/lib/template.js
================================================
import { Text } from '../components/text';
import translate from './translate';
const EMPTY = {};
/** Populate {{template.fields}} within a given string.
*
* @private
* @param {String} template The string containing fields to be resolved
* @param {Object} [fields={}] Optionally nested object of fields, referencable from `template`.
* @example
* template('foo{{bar}}', { bar:'baz' }) === 'foobaz'
*/
export default function template(template, fields, scope, dictionary) {
return template && template.replace(/\{\{([\w.-]+)\}\}/g, replacer.bind(null, fields || EMPTY, scope, dictionary));
}
/** Replacement callback for injecting fields into a String
* @private
*/
function replacer(currentFields, scope, dictionary, s, field) {
let parts = field.split('.'),
v = currentFields;
for (let i=0; i<parts.length; i++) {
v = v[parts[i]];
if (v == null) return ''; // eslint-disable-line eqeqeq
//allow field values to be <Text /> nodes
if (v && v.nodeName === Text) {
return translate(v.attributes.id, scope, dictionary, v.attributes.fields, v.attributes.plural, v.attributes.fallback);
}
}
// allow for recursive {{config.xx}} references:
if (typeof v==='string' && v.match(/\{\{/)) {
v = template(v, currentFields);
}
return v;
}
================================================
FILE: src/lib/translate-mapping.js
================================================
import { assign, select } from './util';
import translate from './translate';
import { Text } from '../components/text';
/** Translates the property values in an Object, returning a copy.
* **Note:** By default, `String` keys will be treated as Intl ID's.
* Pass `true` to return an Object containing *only* translated
* values where the prop is a <Text /> node.
*
* @private
* @param {Object} props An object with values to translate
* @param {Object} intl An intl context object (eg: `context.intl`)
* @param {Boolean} [onlyTextNodes=false] Only process `<Text />` values
* @returns {Object} translatedProps
*/
export default function translateMapping(props, intl, onlyTextNodes) {
let out = {};
intl = intl || {};
props = select(props);
for (let name in props) {
if (props.hasOwnProperty(name) && props[name]) {
let def = props[name];
// if onlyTextNodes=true, skip any props that aren't <Text /> vnodes
if (!onlyTextNodes && typeof def==='string') {
out[name] = translate(def, intl.scope, intl.dictionary);
}
else if (def.nodeName===Text) {
// it's a <Text />, just grab its props:
let c = def.children;
def = assign({
//no fallback if there are no children. Return first child if there is only 1, or array of children if there are more than one
fallback: c.length && (c.length === 1 ? c[0] : c)
}, def.attributes);
out[name] = translate(def.id, intl.scope, intl.dictionary, def.fields, def.plural, def.fallback);
}
}
}
return out;
}
================================================
FILE: src/lib/translate.js
================================================
import delve from 'dlv';
import { defined } from './util';
import template from './template';
/** Attempts to look up a translated value from a given dictionary.
* Also supports json templating using the format: {{variable}}
* Falls back to default text.
*
* @private
* @param {String} id Intl field name/id (subject to scope)
* @param {String} [scope=''] Scope, which prefixes `id` with `${scope}.`
* @param {Object} dictionary A nested object containing translations
* @param {Object} [fields={}] Template fields for use by translated strings
* @param {Number} [plural] Indicates a quantity, used to trigger pluralization
* @param {String|Array} [fallback] Text to return if no translation is found
* @returns {String} translated
*/
export default function translate(id, scope, dictionary, fields, plural, fallback) {
if (scope) id = scope + '.' + id;
let value = dictionary && delve(dictionary, id);
// plural forms:
// key: ['plural', 'singular']
// key: { none, one, many }
// key: { zero, one, other }
// key: { singular, plural }
if ((plural || plural===0) && value && typeof value==='object') {
if (value.splice) {
value = value[plural] || value[0];
}
else if (plural===0 && defined(value.none || value.zero)) {
value = value.none || value.zero;
}
else if (plural===1 && defined(value.one || value.singular)) {
value = value.one || value.singular;
}
else {
value = value.some || value.many || value.plural || value.other || value;
}
}
return value && template(value, fields, scope, dictionary) || fallback || null;
}
================================================
FILE: src/lib/util.js
================================================
/** Check if an object is not null or undefined
* @private
*/
export function defined(obj) {
return obj!==null && obj!==undefined;
}
/** A simpler Object.assign
* @private
*/
export function assign(obj, props) {
for (let i in props) {
obj[i] = props[i];
}
return obj;
}
/** Recursively copy keys from `source` to `target`, skipping truthy values already in `target`.
* @private
*/
export function deepAssign(target, source) {
let out = assign({}, target);
for (let i in source) {
if (source.hasOwnProperty(i)) {
if (target[i] && source[i] && typeof target[i]==='object' && typeof source[i]==='object') {
out[i] = deepAssign(target[i], source[i]);
}
else {
out[i] = target[i] || source[i];
}
}
}
return out;
}
/** select('foo,bar') creates a mapping: `{ foo, bar }`
* @private
*/
export function select(properties) {
properties = properties || {};
if (typeof properties==='string') {
properties = properties.split(',');
}
if ('join' in properties) {
let selected = {};
for (let i=0; i<properties.length; i++) {
let val = properties[i].trim();
if (val) selected[val.split('.').pop()] = val;
}
return selected;
}
return properties;
}
================================================
FILE: test/.eslintrc
================================================
{
"extends": "eslint-config-synacor/test-rules"
}
================================================
FILE: test/index.js
================================================
import { h, render } from 'preact';
import 'preact-jsx-chai';
import wrap, { intl, IntlProvider, Text, MarkupText, Localizer, withText } from 'preact-i18n';
/* eslint-disable react/no-danger */
function Empty() {}
describe('intl', () => {
let scope = 'test-scope',
dictionary = { foo: 'bar', baz: 'bat' },
options = { scope, definition: dictionary };
before( () => rndr() );
let scratch = document.createElement('div'),
root;
function rndr(jsx) {
root = render(<Empty />, scratch, root);
if (jsx) root = render(jsx, scratch, root);
return root;
}
it('should export things', () => {
expect(intl).to.be.a('function');
expect(wrap).to.be.a('function');
expect(IntlProvider).to.be.a('function');
expect(Text).to.be.a('function');
});
it('should work as a decorator @intl when given one argumnt', () => {
let TestClass = () => <div />;
let IntlTestClass = intl(options)(TestClass);
expect(<IntlTestClass />).to.equal(
<IntlProvider scope={scope} definition={dictionary}>
<TestClass />
</IntlProvider>
);
});
it('should work as a function when given two arguments', () => {
let TestClass = () => <div />;
let IntlTestClass = intl(TestClass, options);
expect(<IntlTestClass />).to.equal(
<IntlProvider scope={scope} definition={dictionary}>
<TestClass />
</IntlProvider>
);
});
describe('getWrappedComponent()', () => {
it('should be a function', () => {
let Wrapped = intl(options)(sinon.spy());
expect(Wrapped.getWrappedComponent).to.be.a('function');
});
it('should return the Child component that it is wrapping', () => {
let Foo = sinon.spy();
let Wrapped = intl(options)(Foo);
expect(Wrapped.getWrappedComponent()).to.equal(Foo);
});
it('should recursively call getWrappedComponent() on Child components to return the first non-decorator Child', () => {
let Foo = sinon.spy();
//Wrap Foo in two layers of configuration to make sure Foo is returned by the top level call to getWrappedComponent
let Wrapped = intl(options)(intl(options)(Foo));
expect(Wrapped.getWrappedComponent()).to.equal(Foo);
});
});
describe('<IntlProvider>', () => {
it('should provide context', () => {
let Spy = sinon.stub().returns(null);
rndr(
<IntlProvider definition={dictionary}>
<Spy />
</IntlProvider>
);
expect(Spy).to.have.been.calledOnce.and.calledWithMatch({}, { intl: { dictionary } });
Spy.reset();
rndr(
<IntlProvider scope="foo" definition={{ foo: dictionary }}>
<Spy />
</IntlProvider>
);
expect(Spy).to.have.been.calledOnce.and.calledWithMatch({}, {
intl: {
scope: 'foo',
dictionary: { foo: dictionary }
}
});
});
describe('mark', () => {
it('should be off by default', () => {
const Child = sinon.spy( () => <div /> );
rndr(
<IntlProvider>
<Child />
</IntlProvider>
);
expect(Child).to.have.been.calledWithMatch({ }, { intl: { } });
});
it('should be triggered by <IntlProvider mark>', () => {
const Child = sinon.spy( () => <div /> );
rndr(
<IntlProvider mark>
<Child />
</IntlProvider>
);
expect(Child).to.have.been.calledWithMatch({ }, { intl: { mark: true } });
});
it('should be triggered by URL flag', () => {
let url = location.pathname;
function test(urlSuffix) {
history.replaceState(null, null, url+urlSuffix);
const Child = sinon.spy( () => <div /> );
rndr(
<IntlProvider>
<Child />
</IntlProvider>
);
expect(Child).to.have.been.calledWithMatch({ }, { intl: { mark: true } });
}
test('?intl=show');
test('#intl=show');
test('?foo&intl=show');
test('?foo=bar=&intl=show&baz=bat');
history.replaceState(null, null, url);
});
});
});
describe('<Text>', () => {
it('should fall back if not wrapped in a Provider', () => {
rndr(
<div>
<Text>FOO</Text>
</div>
);
expect(root.innerHTML).to.equal('FOO');
});
it('should render text without scope', () => {
rndr(
<div>
<IntlProvider definition={{ foo: 'FOO!' }}>
<div>
<Text id="foo" />
</div>
</IntlProvider>
</div>
);
expect(root.innerHTML).to.equal('<div>FOO!</div>');
});
it('should render text with scope', () => {
rndr(
<div>
<IntlProvider scope="foo" definition={{ foo: { bar: 'BAR!' } }}>
<div>
<Text id="bar" />
</div>
</IntlProvider>
</div>
);
expect(root.innerHTML, '').to.equal('<div>BAR!</div>');
});
it('should render html markup as string data, not markup', () => {
rndr(
<div>
<IntlProvider definition={{ foo: '<b>FOO</b>' }} >
<div>
<Text id="foo" />
</div>
</IntlProvider>
</div>
);
expect(root.innerHTML, '').to.equal('<div><b>FOO</b></div>');
});
it('should render default when requested id is not present', () => {
rndr(
<div>
<IntlProvider scope="foo" definition={{ foo: { bar: 'BAR!' } }}>
<div>
<Text id="asdf">DEFAULT</Text>
</div>
</IntlProvider>
</div>
);
expect(root.innerHTML, '').to.equal('<div>DEFAULT</div>');
});
describe('mark', () => {
it('should render translations with a green wrapping <mark>', () => {
expect(
<IntlProvider mark definition={{ bar: 'BAR!' }}>
<Text id="bar" />
</IntlProvider>
).to.eql(
<mark style="background: rgba(119,231,117,.5)" title="bar">BAR!</mark>
);
});
it('should render translations relying on a fallback with a yellow wrapping <mark>', () => {
expect(
<IntlProvider mark definition={{ bar: 'BAR!' }}>
<Text id="foo">Fooey</Text>
</IntlProvider>
).to.eql(
<mark style="background: rgba(229,226,41,.5)" title="foo">Fooey</mark>
);
});
it('should render missing translations with an orange wrapping <mark>', () => {
expect(
<IntlProvider mark definition={{ bar: 'BAR!' }}>
<Text id="foo" />
</IntlProvider>
).to.eql(
<mark style="background: rgba(228,147,51,.5)" title="foo" />
);
});
});
});
describe('<MarkupText>', () => {
it('should render nothing if no key or fallback is found', () => {
rndr(
<div>
<MarkupText />
</div>
);
expect(root.innerHTML).to.equal('');
});
it('should fall back if not wrapped in a Provider', () => {
rndr(
<div>
<MarkupText><b>FOO</b></MarkupText>
</div>
);
expect(root.innerHTML).to.equal('<span><b>FOO</b></span>');
});
it('should render multi-child fallback', () => {
rndr(
<div>
<MarkupText>This <b>is the fallback</b> with multiple children</MarkupText>
</div>
);
expect(root.innerHTML).to.equal('<span>This <b>is the fallback</b> with multiple children</span>');
});
it('should render text without scope', () => {
rndr(
<div>
<IntlProvider definition={{ foo: 'FOO!' }}>
<div>
<MarkupText id="foo" />
</div>
</IntlProvider>
</div>
);
expect(root.innerHTML).to.equal('<div><span>FOO!</span></div>');
});
it('should render text with scope', () => {
rndr(
<div>
<IntlProvider scope="foo" definition={{ foo: { bar: 'BAR!' } }}>
<div>
<MarkupText id="bar" />
</div>
</IntlProvider>
</div>
);
expect(root.innerHTML, '').to.equal('<div><span>BAR!</span></div>');
});
it('should render default when requested id is not present', () => {
rndr(
<div>
<IntlProvider scope="foo" definition={{ foo: { bar: 'BAR!' } }}>
<div>
<MarkupText id="asdf"><b>DEFAULT</b></MarkupText>
</div>
</IntlProvider>
</div>
);
expect(root.innerHTML, '').to.equal('<div><span><b>DEFAULT</b></span></div>');
});
it('should render html markup as markup', () => {
rndr(
<div>
<IntlProvider definition={{ foo: '<b>FOO</b>' }} >
<div>
<MarkupText id="foo" />
</div>
</IntlProvider>
</div>
);
expect(root.innerHTML, '').to.equal('<div><span><b>FOO</b></span></div>');
});
describe('mark', () => {
it('should render translations with a green wrapping <mark>', () => {
expect(
<IntlProvider mark definition={{ bar: '<b>BAR!</b>' }}>
<MarkupText id="bar" />
</IntlProvider>
).to.eql(
<mark style="background: rgba(119,231,117,.5)" title="bar">
<span dangerouslySetInnerHTML={{ __html: '<b>BAR!</b>' }} />
</mark>);
});
it('should render translations relying on a fallback with a yellow wrapping <mark>', () => {
expect(
<IntlProvider mark definition={{ bar: 'BAR!' }}>
<MarkupText id="foo"><b>Fooey</b></MarkupText>
</IntlProvider>
).to.eql(
<mark style="background: rgba(229,226,41,.5)" title="foo">
<span><b>Fooey</b></span>
</mark>
);
});
it('should render missing translations with an orange wrapping <mark>', () => {
expect(
<IntlProvider mark definition={{ bar: 'BAR!' }}>
<MarkupText id="foo" />
</IntlProvider>
).to.eql(
<mark style="background: rgba(228,147,51,.5)" title="foo" />
);
});
});
});
describe('withText()', () => {
it('should provide strings to child as props', () => {
const Child = sinon.stub().returns(<div />);
const Wrapped = withText({
propName: 'foo'
})(Child);
rndr(
<IntlProvider definition={{ foo: 'FOO!' }}>
<Wrapped />
</IntlProvider>
);
expect(Child).to.have.been.calledOnce.and.calledWithMatch({ propName: 'FOO!' });
});
it('should accept a CSV of keys', () => {
const Child = sinon.stub().returns(<div />);
const Wrapped = withText('foo,bar,baz')(Child);
rndr(
<IntlProvider definition={{ foo: '1', bar: '2', baz: '3' }}>
<Wrapped />
</IntlProvider>
);
expect(Child).to.have.been.calledOnce.and.calledWithMatch({ foo: '1', bar: '2', baz: '3' });
});
it('should accept an Array of keys', () => {
const Child = sinon.stub().returns(<div />);
const Wrapped = withText(['foo','bar','baz'])(Child);
rndr(
<IntlProvider definition={{ foo: '1', bar: '2', baz: '3' }}>
<Wrapped />
</IntlProvider>
);
expect(Child).to.have.been.calledOnce.and.calledWithMatch({ foo: '1', bar: '2', baz: '3' });
});
it('should support <Text> as a value', () => {
const Child = sinon.stub().returns(<div />);
const Wrapped = withText({
normal: <Text id="foo" />,
missing: <Text id="bar" />,
withchild: <Text id="baz">child</Text>,
withfallback: <Text id="bat" fallback="fallback" />
})(Child);
rndr(
<IntlProvider definition={{ foo: 'FOO!' }}>
<Wrapped />
</IntlProvider>
);
expect(Child).to.have.been.calledOnce.and.calledWithMatch({
normal: 'FOO!',
missing: null,
withchild: 'child',
withfallback: 'fallback'
});
});
describe('getWrappedComponent()', () => {
it('should be a function', () => {
const Wrapped = withText(['foo','bar','baz'])(sinon.spy());
expect(Wrapped.getWrappedComponent).to.be.a('function');
});
it('should return the Child component that it is wrapping', () => {
let Foo = sinon.spy();
let Wrapped = withText(['foo','bar','baz'])(Foo);
expect(Wrapped.getWrappedComponent()).to.equal(Foo);
});
it('should recursively call getWrappedComponent() on Child components to return the first non-decorator Child', () => {
let Foo = sinon.spy();
//Wrap Foo in two layers of configuration to make sure Foo is returned by the top level call to getWrappedComponent
let Wrapped = withText(['foo','bar','baz'])(withText(['buzz'])(Foo));
expect(Wrapped.getWrappedComponent()).to.equal(Foo);
});
});
});
describe('<Localizer>', () => {
it('should translate any <Text> props on its child', () => {
rndr(
<IntlProvider definition={{ input: { pl: 'type a name' } }}>
<div>
<Localizer>
<input
placeholder={<Text id="input.pl" />}
title={<Text id="input.title">blah</Text>}
type="email"
minlength={0}
maxlength={1}
required
/>
</Localizer>
</div>
</IntlProvider>
);
expect(root).to.have.property('innerHTML', `<input placeholder="type a name" title="blah" type="email" minlength="0" maxlength="1" required="">`);
});
});
});
================================================
FILE: test/lib/template.js
================================================
import template from '../../src/lib/template';
describe('template()', () => {
it('should leave fieldless unmodified', () => {
expect(template('foo')).to.equal('foo');
expect(template('{foo}')).to.equal('{foo}');
expect(template('{{foo\\}}')).to.equal('{{foo\\}}');
expect(template('a{{$$}}')).to.equal('a{{$$}}');
});
it('should inject top-level fields', () => {
const FIELDS = {
foo: 'FOO',
bar: 'baz'
};
expect(template('{{foo}}', FIELDS)).to.equal('FOO');
expect(template('{{foo}}{{bar}}', FIELDS)).to.equal('FOObaz');
expect(template('a {{foo}} b {{bar}} c', FIELDS)).to.equal('a FOO b baz c');
});
it('should inject nested fields', () => {
const FIELDS = {
foo: {
bar: {
baz: 'bat'
}
},
arr: [
'a',
{ b: 1 }
]
};
expect(template('{{foo.bar.baz}}', FIELDS)).to.equal('bat');
expect(template('{{foo.bar}}', FIELDS)).to.equal('[object Object]');
expect(template('{{arr.0}}', FIELDS)).to.equal('a');
expect(template('{{arr.1.b}}', FIELDS)).to.equal('1');
});
it('should support recursive field injection', () => {
const FIELDS = {
first: '1{{second}}2',
second: '3{{third}}4',
third: 'THIRD'
};
expect(template('{{first}}', FIELDS)).to.equal('13THIRD42');
});
it('should replace empty fields with the empty string', () => {
const FIELDS = {
baz: 'baz'
};
expect(template('{{foo}}', FIELDS)).to.equal('');
expect(template('{{foo.bar}}', FIELDS)).to.equal('');
expect(template('Fooey {{foo.bar}}', FIELDS)).to.equal('Fooey ');
expect(template('Fooey {{foo.bar}} {{baz}}', FIELDS)).to.equal('Fooey baz');
});
it('should replace fields with falsey values', () => {
const FIELDS = {
foo: 0,
bar: false
};
expect(template('{{foo}}', FIELDS)).to.equal('0');
expect(template('{{bar}}', FIELDS)).to.equal('false');
});
});
================================================
FILE: test/lib/translate.js
================================================
import { h } from 'preact';
import translate from '../../src/lib/translate';
import { Text } from '../../src/components/text';
describe('translate', () => {
it('should return the value from the dictionary if a dot-notated match on id is found', () => {
expect(translate('foo.bar', undefined, { foo: { bar: 'hello' } })).to.equal('hello');
});
it('should prefix scope to id to resolve dot-notated id in dictionary', () => {
expect(translate('foo.bar', 'myScope', { myScope: { foo: { bar: 'hello' } } })).to.equal('hello');
expect(translate('foo.bar', 'myScope', { foo: { bar: 'hello' } })).to.be.null;
});
it('should return null if no id match is found in the dictionary and not fallback is provided', () => {
expect(translate('foo.bar', undefined, {})).to.be.null;
});
it('should return the fallback when a value from the dictionary by a dot-notated id match is not found', () => {
expect(translate('foo.bar', undefined, {}, undefined, undefined, 'testFallback')).to.equal('testFallback');
});
it('should replace dot-notated templated strings when given a fields attribute', () => {
expect(translate('foo.bar', undefined, { foo: { bar: 'hello{{c.d}}' } }, { c: { d: 'World' } })).to.equal('helloWorld');
});
it('should translate <Text /> components that exist in field values when working on templated strings', () => {
expect(translate('foo.bar', undefined, { foo: { bar: 'hello{{c.d}}' }, e: 'World' }, { c: { d: <Text id="e" /> } })).to.equal('helloWorld');
});
it('should pluralise for none/one/many pluralisation keys', () => {
expect(translate('foo.bar', undefined, { foo: { bar: { none: 'none', one: 'one', many: 'many' } } }, undefined, 0)).to.equal('none');
expect(translate('foo.bar', undefined, { foo: { bar: { none: 'none', one: 'one', many: 'many' } } }, undefined, 1)).to.equal('one');
expect(translate('foo.bar', undefined, { foo: { bar: { none: 'none', one: 'one', many: 'many' } } }, undefined, 2)).to.equal('many');
expect(translate('foo.bar', undefined, { foo: { bar: { none: 'none', one: 'one', many: 'many' } } }, undefined, 100)).to.equal('many');
});
it('should pluralise for zero/one/other pluralisation keys', () => {
expect(translate('foo.bar', undefined, { foo: { bar: { zero: 'zero', one: 'one', other: 'other' } } }, undefined, 0)).to.equal('zero');
expect(translate('foo.bar', undefined, { foo: { bar: { zero: 'zero', one: 'one', other: 'other' } } }, undefined, 1)).to.equal('one');
expect(translate('foo.bar', undefined, { foo: { bar: { zero: 'zero', one: 'one', other: 'other' } } }, undefined, 2)).to.equal('other');
expect(translate('foo.bar', undefined, { foo: { bar: { zero: 'zero', one: 'one', other: 'other' } } }, undefined, 100)).to.equal('other');
});
it('should pluralise for singular/plural pluralisation keys', () => {
// assume 0 is a plural form if using the singular/plural pluralisation convention
expect(translate('foo.bar', undefined, { foo: { bar: { singular: 'singular', plural: 'plural' } } }, undefined, 0)).to.equal('plural');
expect(translate('foo.bar', undefined, { foo: { bar: { singular: 'singular', plural: 'plural' } } }, undefined, 1)).to.equal('singular');
expect(translate('foo.bar', undefined, { foo: { bar: { singular: 'singular', plural: 'plural' } } }, undefined, 2)).to.equal('plural');
expect(translate('foo.bar', undefined, { foo: { bar: { singular: 'singular', plural: 'plural' } } }, undefined, 100)).to.equal('plural');
});
});
================================================
FILE: test/lib/util.js
================================================
import { select } from '../../src/lib/util';
describe('util', () => {
describe('select()', () => {
it('should return objects unmodified', () => {
let obj = { foo: 'foo', bar: 'bar' };
expect(select(obj)).to.equal(obj);
});
it('should create an object mapping from an Array', () => {
expect(select(['foo'])).to.eql({ foo: 'foo' });
expect(select(['foo', 'bar'])).to.eql({ foo: 'foo', bar: 'bar' });
expect(select(['foo.a', 'bar.b'])).to.eql({ a: 'foo.a', b: 'bar.b' });
});
it('should return create an object mapping from a CSV String', () => {
expect(select('a')).to.eql({ a: 'a' });
expect(select('foo,bar')).to.eql({ foo: 'foo', bar: 'bar' });
expect(select('foo , bar')).to.eql({ foo: 'foo', bar: 'bar' });
});
});
});
gitextract_qzqk6_j4/
├── .editorconfig
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── karma.conf.js
├── package.json
├── rollup.config.js
├── src/
│ ├── components/
│ │ ├── highlight-i18n.js
│ │ ├── intl-provider.js
│ │ ├── localizer.js
│ │ ├── markup-text.js
│ │ ├── text.js
│ │ └── with-text.js
│ ├── index.js
│ ├── intl.js
│ └── lib/
│ ├── template.js
│ ├── translate-mapping.js
│ ├── translate.js
│ └── util.js
└── test/
├── .eslintrc
├── index.js
└── lib/
├── template.js
├── translate.js
└── util.js
SYMBOL INDEX (24 symbols across 13 files)
FILE: karma.conf.js
function dedupe (line 52) | function dedupe(v, i, arr) { return v && arr.indexOf(v)===i; }
FILE: src/components/highlight-i18n.js
function HighlightI18N (line 11) | function HighlightI18N({ value, id }, { intl }) {
FILE: src/components/intl-provider.js
constant URL_FLAG (line 5) | const URL_FLAG = /[?&#]intl=show/;
class IntlProvider (line 35) | class IntlProvider extends Component {
method getChildContext (line 36) | getChildContext() {
method render (line 55) | render({ children }) {
FILE: src/components/localizer.js
function Localizer (line 28) | function Localizer({ children }, { intl }) {
FILE: src/components/markup-text.js
function MarkupText (line 45) | function MarkupText(props) {
function Html (line 53) | function Html({ html, id }) {
FILE: src/components/text.js
function Text (line 42) | function Text({ id, children, plural, fields }, { intl }) {
FILE: src/components/with-text.js
function withText (line 49) | function withText(mapping) {
FILE: src/intl.js
function intl (line 20) | function intl(Child, options) {
FILE: src/lib/template.js
constant EMPTY (line 4) | const EMPTY = {};
function template (line 14) | function template(template, fields, scope, dictionary) {
function replacer (line 21) | function replacer(currentFields, scope, dictionary, s, field) {
FILE: src/lib/translate-mapping.js
function translateMapping (line 16) | function translateMapping(props, intl, onlyTextNodes) {
FILE: src/lib/translate.js
function translate (line 18) | function translate(id, scope, dictionary, fields, plural, fallback) {
FILE: src/lib/util.js
function defined (line 4) | function defined(obj) {
function assign (line 12) | function assign(obj, props) {
function deepAssign (line 23) | function deepAssign(target, source) {
function select (line 41) | function select(properties) {
FILE: test/index.js
function Empty (line 6) | function Empty() {}
function rndr (line 17) | function rndr(jsx) {
function test (line 129) | function test(urlSuffix) {
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (65K chars).
[
{
"path": ".editorconfig",
"chars": 236,
"preview": "root = true\n\n[*]\nindent_style = tab\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newlin"
},
{
"path": ".gitignore",
"chars": 65,
"preview": "/docs\n/dist\n/test-reports\n/node_modules\n/npm-debug.log\n.DS_Store\n"
},
{
"path": ".travis.yml",
"chars": 39,
"preview": "language: node_js\nnode_js:\n - stable\n\n"
},
{
"path": "LICENSE",
"chars": 1484,
"preview": "Copyright (c) 2017, Synacor, Inc.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or witho"
},
{
"path": "README.md",
"chars": 15143,
"preview": "# preact-i18n 🌎 [](https://npm.im/preact-i18n) [![travis]"
},
{
"path": "karma.conf.js",
"chars": 1280,
"preview": "/* eslint-disable */\n\nvar path = require('path');\n\nvar pkg = require('./package.json');\n\nvar REPORTER = process.env.REPO"
},
{
"path": "package.json",
"chars": 3615,
"preview": "{\n \"name\": \"preact-i18n\",\n \"amdName\": \"preactLocalize\",\n \"version\": \"1.5.0\",\n \"description\": \"Preact internationaliz"
},
{
"path": "rollup.config.js",
"chars": 535,
"preview": "/*eslint-disable*/\nimport memory from 'rollup-plugin-memory';\nimport buble from 'rollup-plugin-buble';\n\nexport default f"
},
{
"path": "src/components/highlight-i18n.js",
"chars": 966,
"preview": "import { h } from 'preact';\nimport delve from 'dlv';\n\n\n/** Highlight/colorize the i18n'd node if `mark` is set on `intl`"
},
{
"path": "src/components/intl-provider.js",
"chars": 1974,
"preview": "import { h, Component } from 'preact';\nimport { assign, deepAssign } from '../lib/util';\n\n\nconst URL_FLAG = /[?&#]intl=s"
},
{
"path": "src/components/localizer.js",
"chars": 987,
"preview": "import { cloneElement } from 'preact';\nimport translateMapping from '../lib/translate-mapping';\n\n/** `<Localizer />` is "
},
{
"path": "src/components/markup-text.js",
"chars": 2155,
"preview": "import { h } from 'preact';\nimport { Text } from './text';\nimport { Localizer } from './localizer';\nimport { HighlightI1"
},
{
"path": "src/components/text.js",
"chars": 1868,
"preview": "import { h } from 'preact';\nimport translate from '../lib/translate';\nimport { HighlightI18N } from './highlight-i18n';\n"
},
{
"path": "src/components/with-text.js",
"chars": 1930,
"preview": "import { h } from 'preact';\nimport translateMapping from '../lib/translate-mapping';\nimport { assign } from '../lib/util"
},
{
"path": "src/index.js",
"chars": 537,
"preview": "import { intl } from './intl';\nimport { IntlProvider } from './components/intl-provider';\nimport { Text } from './compon"
},
{
"path": "src/intl.js",
"chars": 1562,
"preview": "import { h } from 'preact';\nimport { IntlProvider } from './components/intl-provider';\n\n\n/**\n * Higher-order function th"
},
{
"path": "src/lib/template.js",
"chars": 1265,
"preview": "import { Text } from '../components/text';\nimport translate from './translate';\n\nconst EMPTY = {};\n\n/** Populate {{templ"
},
{
"path": "src/lib/translate-mapping.js",
"chars": 1513,
"preview": "import { assign, select } from './util';\nimport translate from './translate';\nimport { Text } from '../components/text';"
},
{
"path": "src/lib/translate.js",
"chars": 1587,
"preview": "import delve from 'dlv';\nimport { defined } from './util';\nimport template from './template';\n\n/** Attempts to look up a"
},
{
"path": "src/lib/util.js",
"chars": 1201,
"preview": "/** Check if an object is not null or undefined\n *\t@private\n */\nexport function defined(obj) {\n\treturn obj!==null && obj"
},
{
"path": "test/.eslintrc",
"chars": 52,
"preview": "{\n \"extends\": \"eslint-config-synacor/test-rules\"\n}\n"
},
{
"path": "test/index.js",
"chars": 12392,
"preview": "import { h, render } from 'preact';\nimport 'preact-jsx-chai';\nimport wrap, { intl, IntlProvider, Text, MarkupText, Local"
},
{
"path": "test/lib/template.js",
"chars": 1853,
"preview": "import template from '../../src/lib/template';\n\ndescribe('template()', () => {\n\tit('should leave fieldless unmodified', "
},
{
"path": "test/lib/translate.js",
"chars": 3479,
"preview": "import { h } from 'preact';\nimport translate from '../../src/lib/translate';\nimport { Text } from '../../src/components/"
},
{
"path": "test/lib/util.js",
"chars": 763,
"preview": "import { select } from '../../src/lib/util';\n\ndescribe('util', () => {\n\tdescribe('select()', () => {\n\t\tit('should return"
}
]
About this extraction
This page contains the full source code of the synacor/preact-i18n GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (57.1 KB), approximately 16.7k tokens, and a symbol index with 24 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.