Support svelte v3 (#6)

This commit is contained in:
Christian Kaisermann 2019-05-18 22:59:26 -03:00 committed by GitHub
parent cec700af53
commit 0ae0cc8148
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 11335 additions and 5274 deletions

View File

@ -3,4 +3,5 @@
/yarn.lock /yarn.lock
/package-lock.json /package-lock.json
/src /src
/coverage /coverage
/example

280
README.md
View File

@ -4,132 +4,224 @@
## Usage ## Usage
### On the `store` `svelte-i18n` utilizes svelte `stores` for keeping track of the current locale, dictionary of messages and the main format function. This way, we keep everything neat, in sync and easy to use on your svelte files.
---
### Locale
The `locale` store defines what is the current locale.
```js ```js
import { i18n } from 'svelte-i18n' import { locale, dictionary } from 'svelte-i18n'
import { Store } from 'svelte/store'
/** i18n(svelteStore, { dictionary }) */ // Set the current locale to en-US
let store = new Store() locale.set('en-US')
store = i18n(store, { // This is a store, so we can subscribe to its changes
dictionary: { locale.subscribe(() => {
'pt-BR': { console.log('locale change')
message: 'Mensagem', })
greeting: 'Olá {name}, como vai?', ```
greetingIndex: 'Olá {0}, como vai?',
meter: 'metros | metro | metros', ---
book: 'livro | livros',
messages: { ### The dictionary
alert: 'Alerta',
error: 'Erro', The `dictionary` store defines the dictionary of messages of all locales.
},
```js
import { locale, dictionary } from 'svelte-i18n'
// Define a locale dictionary
dictionary.set({
pt: {
message: 'Mensagem',
'switch.lang': 'Trocar idioma',
greeting: {
ask: 'Por favor, digite seu nome',
message: 'Olá {name}, como vai?',
}, },
'en-US': { photos:
message: 'Message', 'Você {n, plural, =0 {não tem fotos.} =1 {tem uma foto.} other {tem # fotos.}}',
greeting: 'Hello {name}, how are you?', cats: 'Tenho {n, number} {n,plural,=0{gatos}one{gato}other{gatos}}',
greetingIndex: 'Hello {0}, how are you?', },
meter: 'meters | meter | meters', en: {
book: 'book | books', message: 'Message',
messages: { 'switch.lang': 'Switch language',
alert: 'Alert', greeting: {
error: 'Error', ask: 'Please type your name',
}, message: 'Hello {name}, how are you?',
}, },
photos:
'You have {n, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}',
cats: 'I have {n, number} {n,plural,one{cat}other{cats}}',
}, },
}) })
/** // It's also possible to merge the current dictionary
* Extend the initial dictionary. // with other objets
* Dictionaries are deeply merged. dictionary.update(dict => {
* */ dict.fr = {
store.i18n.extendDictionary({ // ...french messages
'pt-BR': { }
messages: { return dict
warn: 'Aviso',
success: 'Sucesso',
},
},
'en-US': {
messages: {
warn: 'Warn',
success: 'Success',
},
},
}) })
/** Set the initial locale */
store.i18n.setLocale('en-US')
``` ```
### On `templates` Each language message dictionary can be as deep as you want. Messages can also be looked up by a string represetation of it's path on the dictionary (i.e `greeting.message`).
#### Basic usage ---
### Formatting
The `_`/`format` store is the actual formatter method. To use it, it's simple as any other svelte store.
```html
<script>
// locale is en
import { _ } from 'svelte-i18n'
</script>
<input placeholder="{$_('greeting.ask')}" />
```
`svelte-i18n` uses `formatjs` behind the scenes, which means it supports the [ICU message format](http://userguide.icu-project.org/formatparse/messages) for interpolation, pluralization and much more.
```html ```html
<div> <div>
{$_('message')}: {$_('messages.success')} {$_('greeting.message', { name: 'John' })}
<!-- Message: SUCCESS--> <!-- Hello John, how are you? -->
{$_('photos', { n: 0 })}
<!-- You have no photos. -->
{$_('photos', { n: 12 })}
<!-- You have 12 photos. -->
</div> </div>
``` ```
#### Current locale ### Formatting methods
The current locale is available via `this.store.get().locale`. #### `_` / `format`
#### Interpolation `function(messageId: string, locale:? string): string`
`function(messageId: string, interpolations?: object, locale:? string): string`
Main formatting method that formats a localized message by its id.
```html ```html
<div> <script>
<!-- Named interpolation --> import { _ } from 'svelte-i18n'
{$_('greeting', { name: 'John' })} </script>
<!-- Hello John, how are you?-->
<!-- List interpolation --> <div>{$_('greeting.ask')}</div>
{$_('greetingIndex', ['John'])} <!-- Please type your name -->
<!-- Hello John, how are you?-->
</div>
``` ```
#### Pluralization #### `_.upper`
Transforms a localized message into uppercase.
```html ```html
<div> <script>
0 {$_.plural('meter', 0)} import { _ } from 'svelte-i18n'
<!-- 0 meters --> </script>
1 {$_.plural('meter', 1)} <div>{$_.upper('greeting.ask')}</div>
<!-- 1 meter --> <!-- PLEASE TYPE YOUR NAME -->
100 {$_.plural('meter', 100)}
<!-- 100 meters -->
0 {$_.plural('book', 0)}
<!-- 0 books -->
1 {$_.plural('book', 1)}
<!-- 1 book -->
10 {$_.plural('book', 10)}
<!-- 10 books -->
</div>
``` ```
#### Utilities #### `_.lower`
Transforms a localized message into lowercase.
```html ```html
<div> <script>
{$_.upper('message')} import { _ } from 'svelte-i18n'
<!-- MESSAGE --> </script>
{$_.lower('message')} <div>{$_.lower('greeting.ask')}</div>
<!-- message --> <!-- PLEASE TYPE YOUR NAME -->
```
{$_.capital('message')}
<!-- Message --> #### `_.capital`
{$_.title('greeting', { name: 'John' })} Capitalize a localized message.
<!-- Hello John, How Are You?-->
</div> ```html
<script>
import { _ } from 'svelte-i18n'
</script>
<div>{$_.capital('greeting.ask')}</div>
<!-- Please type your name -->
```
#### `_.title`
Transform the message into title case.
```html
<script>
import { _ } from 'svelte-i18n'
</script>
<div>{$_.capital('greeting.ask')}</div>
<!-- Please Type Your Name -->
```
#### `_.time`
`function(time: Date, format?: string, locale?: string)`
Formats a date object into a time string with the specified format (`short`, `medium`, `long`, `full`). Please refer to the [ICU message format](http://userguide.icu-project.org/formatparse/messages) documentation for all available. formats
```html
<script>
import { _ } from 'svelte-i18n'
</script>
<div>{$_.time(new Date(2019, 3, 24, 23, 45))}</div>
<!-- 11:45 PM -->
<div>{$_.time(new Date(2019, 3, 24, 23, 45), 'medium')}</div>
<!-- 11:45:00 PM -->
```
#### `_.date`
`function(date: Date, format?: string, locale?: string)`
Formats a date object into a string with the specified format (`short`, `medium`, `long`, `full`). Please refer to the [ICU message format](http://userguide.icu-project.org/formatparse/messages) documentation for all available. formats
```html
<script>
import { _ } from 'svelte-i18n'
</script>
<div>{$_.date(new Date(2019, 3, 24, 23, 45))}</div>
<!-- 4/24/19 -->
<div>{$_.date(new Date(2019, 3, 24, 23, 45), 'medium')}</div>
<!-- Apr 24, 2019 -->
```
#### `_.number`
`function(number: Number, locale?: string)`
Formats a number with the specified locale
```html
<script>
import { _ } from 'svelte-i18n'
</script>
<div>{$_.number(100000000)}</div>
<!-- 100,000,000 -->
<div>{$_.number(100000000, 'pt')}</div>
<!-- 100.000.000 -->
``` ```

3
example/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.DS_Store
node_modules
public/bundle.*

68
example/README.md Normal file
View File

@ -0,0 +1,68 @@
*Psst — looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
---
# svelte app
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
```bash
npx degit sveltejs/template svelte-app
cd svelte-app
```
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
## Get started
Install the dependencies...
```bash
cd svelte-app
npm install
```
...then start [Rollup](https://rollupjs.org):
```bash
npm run dev
```
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
## Deploying to the web
### With [now](https://zeit.co/now)
Install `now` if you haven't already:
```bash
npm install -g now
```
Then, from within your project folder:
```bash
now
```
As an alternative, use the [Now desktop client](https://zeit.co/download) and simply drag the unzipped project folder to the taskbar icon.
### With [surge](https://surge.sh/)
Install `surge` if you haven't already:
```bash
npm install -g surge
```
Then, from within your project folder:
```bash
npm run build
surge public
```

2924
example/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
example/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "svelte-app",
"version": "1.0.0",
"scripts": {
"build": "rollup -c",
"autobuild": "rollup -c -w",
"dev": "run-p start:dev autobuild",
"start": "sirv public",
"start:dev": "sirv public --dev"
},
"devDependencies": {
"npm-run-all": "^4.1.5",
"rollup": "^1.10.1",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-livereload": "^1.0.0",
"rollup-plugin-node-resolve": "^4.2.3",
"rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^4.0.4",
"sirv-cli": "^0.4.0",
"svelte": "^3.0.0"
},
"dependencies": {
"svelte-i18n": "^1.0.2-beta"
}
}

BIN
example/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

61
example/public/global.css Normal file
View File

@ -0,0 +1,61 @@
html, body {
position: relative;
width: 100%;
height: 100%;
}
body {
color: #333;
margin: 0;
padding: 8px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
a {
color: rgb(0,100,200);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0,80,160);
}
label {
display: block;
}
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
input[type="range"] {
height: 0;
}
button {
background-color: #f4f4f4;
outline: none;
}
button:active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}

21
example/public/index.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width" />
<title>Svelte app</title>
<link rel="icon" type="image/png" href="favicon.png" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/tachyons/4.11.1/tachyons.css"
/>
<link rel="stylesheet" href="global.css" />
<link rel="stylesheet" href="bundle.css" />
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>

44
example/rollup.config.js Normal file
View File

@ -0,0 +1,44 @@
import svelte from 'rollup-plugin-svelte'
import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import livereload from 'rollup-plugin-livereload'
import { terser } from 'rollup-plugin-terser'
const production = !process.env.ROLLUP_WATCH
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/bundle.js',
},
plugins: [
svelte({
// enable run-time checks when not in production
dev: !production,
// we'll extract any component CSS out into
// a separate file — better for performance
css: css => {
css.write('public/bundle.css')
},
}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration —
// consult the documentation for details:
// https://github.com/rollup/rollup-plugin-commonjs
resolve(),
commonjs(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser(),
],
}

45
example/src/App.svelte Normal file
View File

@ -0,0 +1,45 @@
<script>
import { locale, _ } from 'svelte-i18n'
let name = ''
let pluralN = 2
let catsN = 992301
let date = new Date()
$: oppositeLocale = $locale === 'pt' ? 'en' : 'pt'
setInterval(() => {
date = new Date()
}, 1000)
</script>
<input
class="w-100"
type="text"
placeholder={$_('greeting.ask')}
bind:value={name} />
<br />
<h1>{$_.title('greeting.message', { name })}</h1>
<br />
<input type="range" min="0" max="5" step="1" bind:value={pluralN} />
<h2>Plural: {$_('photos', { n: pluralN })}</h2>
<br />
<input type="range" min="100" max="100000000" step="10000" bind:value={catsN} />
<h2>Number: {$_('cats', { n: catsN })}</h2>
<br />
<h2>Number util: {$_.number(catsN)}</h2>
<br />
<h2>Date util: {$_.date(date, 'short')}</h2>
<br />
<h2>Time util: {$_.time(date, 'medium')}</h2>
<br />
<button on:click={() => locale.set(oppositeLocale)}>
{$_('switch.lang', null, oppositeLocale)}
</button>

31
example/src/i18n.js Normal file
View File

@ -0,0 +1,31 @@
import { locale, dictionary } from 'svelte-i18n'
// setting the locale
locale.set('pt')
// subscribe to locale changes
locale.subscribe(() => {
console.log('locale change')
})
// defining a locale dictionary
dictionary.set({
pt: {
'switch.lang': 'Trocar idioma',
greeting: {
ask: 'Por favor, digite seu nome',
message: 'Olá {name}, como vai?',
},
photos: 'Você {n, plural, =0 {não tem fotos.} =1 {tem uma foto.} other {tem # fotos.}}',
cats: 'Tenho {n, number} {n,plural,=0{gatos}one{gato}other{gatos}}',
},
en: {
'switch.lang': 'Switch language',
greeting: {
ask: 'Please type your name',
message: 'Hello {name}, how are you?',
},
photos: 'You have {n, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}',
cats: 'I have {n, number} {n,plural,one{cat}other{cats}}'
},
})

9
example/src/main.js Normal file
View File

@ -0,0 +1,9 @@
import './i18n.js'
import App from './App.svelte'
const app = new App({
target: document.body,
props: { name: 'world' },
})
export default app

12517
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
{ {
"name": "svelte-i18n", "name": "svelte-i18n",
"version": "0.0.5", "version": "1.0.2",
"license": "MIT", "license": "MIT",
"main": "dist/i18n.js", "main": "dist/i18n.js",
"module": "dist/i18n.m.js", "module": "dist/i18n.mjs",
"types": "src/index.d.ts", "types": "src/index.d.ts",
"description": "Internationalization library for Svelte", "description": "Internationalization library for Svelte",
"author": "Christian Kaisermann <christian@kaisermann.me>", "author": "Christian Kaisermann <christian@kaisermann.me>",
@ -16,13 +16,12 @@
"translation" "translation"
], ],
"scripts": { "scripts": {
"build": "microbundle --format=cjs,es", "build": "microbundle --format=cjs,es --no-sourcemap",
"start": "microbundle watch --format=cjs,es", "start": "microbundle watch --format=cjs,es",
"test": "jest --no-cache --verbose", "test": "jest --no-cache --verbose",
"test:watch": "jest --no-cache --verbose --watchAll", "test:watch": "jest --no-cache --verbose --watchAll",
"lint": "eslint \"src/**/*.js\"", "lint": "eslint \"src/**/*.js\"",
"format": "prettier --loglevel silent --write \"src/**/*.js\" && eslint --fix \"src/**/*.js\"", "format": "prettier --loglevel silent --write \"src/**/*.js\" && eslint --fix \"src/**/*.js\""
"prepublishOnly": "npm run format && npm run test && npm run build"
}, },
"jest": { "jest": {
"verbose": true, "verbose": true,
@ -50,28 +49,30 @@
"collectCoverage": true "collectCoverage": true
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.0.0-beta.56", "@babel/core": "^7.4.4",
"@babel/preset-env": "^7.0.0-beta.56", "@babel/preset-env": "^7.4.4",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-jest": "^23.4.2", "babel-jest": "^24.8.0",
"eslint": "^5.3.0", "eslint": "^5.16.0",
"eslint-config-prettier": "^2.9.0", "eslint-config-prettier": "^4.3.0",
"eslint-config-standard": "^11.0.0", "eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.13.0", "eslint-plugin-import": "^2.17.2",
"eslint-plugin-node": "^7.0.1", "eslint-plugin-node": "^9.0.1",
"eslint-plugin-prettier": "^2.6.2", "eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-promise": "^3.8.0", "eslint-plugin-promise": "^4.1.1",
"eslint-plugin-standard": "^3.1.0", "eslint-plugin-standard": "^4.0.0",
"jest": "^23.4.2", "jest": "^24.8.0",
"microbundle": "^0.6.0", "microbundle": "^0.11.0",
"prettier": "^1.14.1", "prettier": "^1.17.1",
"svelte": "^2.9.11" "svelte": "^3.4.1"
}, },
"peerDependencies": { "peerDependencies": {
"svelte": "^2.9.11" "svelte": "^3.4.1"
}, },
"dependencies": { "dependencies": {
"deepmerge": "^2.1.1", "deepmerge": "^3.2.0",
"intl-messageformat": "^2.2.0",
"micro-memoize": "^3.0.1",
"object-resolve-path": "^1.1.1" "object-resolve-path": "^1.1.1"
} }
} }

View File

@ -1,124 +0,0 @@
/**
* Adapted from 'https://github.com/kazupon/vue-i18n/blob/dev/src/format.js'
* Copyright (c) 2016 kazuya kawaguchi
**/
import { isObject } from './utils'
const RE_TOKEN_LIST_VALUE = /^(\d)+/
const RE_TOKEN_NAMED_VALUE = /^(\w)+/
export default class Formatter {
constructor() {
this._caches = Object.create(null)
}
interpolate(message, values) {
if (!values) {
return [message]
}
let tokens = this._caches[message]
if (!tokens) {
tokens = parse(message)
this._caches[message] = tokens
}
return compile(tokens, values)
}
}
/** Parse a identification string into cached Tokens */
export function parse(format) {
const tokens = []
let position = 0
let currentText = ''
while (position < format.length) {
let char = format[position++]
/** If found any character that's not a '{' (does not include '\{'), assume text */
if (char !== '{' || (position > 0 && char[position - 1] === '\\')) {
currentText += char
} else {
/** Beginning of a interpolation */
if (currentText.length) {
tokens.push({ type: 'text', value: currentText })
}
/** Reset the current text string because we're dealing interpolation entry */
currentText = ''
/** Key name */
let namedKey = ''
char = format[position++]
while (char !== '}') {
namedKey += char
char = format[position++]
}
const type = RE_TOKEN_LIST_VALUE.test(namedKey)
? 'list'
: RE_TOKEN_NAMED_VALUE.test(namedKey)
? 'named'
: 'unknown'
tokens.push({ value: namedKey, type })
}
}
/** If there's any text left, push it to the tokens list */
if (currentText) {
tokens.push({ type: 'text', value: currentText })
}
return tokens
}
export function compile(tokens, values) {
const compiled = []
let index = 0
const mode = Array.isArray(values)
? 'list'
: isObject(values)
? 'named'
: 'unknown'
if (mode === 'unknown') {
return compiled
}
while (index < tokens.length) {
const token = tokens[index++]
switch (token.type) {
case 'text':
compiled.push(token.value)
break
case 'list':
compiled.push(values[parseInt(token.value, 10)])
break
case 'named':
if (mode === 'named') {
compiled.push(values[token.value])
} else {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[svelte-i18n] Type of token '${
token.type
}' and format of value '${mode}' don't match!`,
)
}
}
break
case 'unknown':
if (process.env.NODE_ENV !== 'production') {
console.warn(`[svelte-i18n] Detect 'unknown' type of token!`)
}
break
}
}
return compiled
}

View File

@ -1,100 +1,71 @@
import deepmerge from 'deepmerge' import { writable, derived } from 'svelte/store'
import resolvePath from 'object-resolve-path' import resolvePath from 'object-resolve-path'
import { capital, title, upper, lower } from './utils' import IntlMessageFormat from 'intl-messageformat'
import Formatter from './formatter' import memoize from 'micro-memoize'
import { capital, title, upper, lower } from './utils.js'
export { capital, title, upper, lower } let currentLocale
let currentDictionary
export function i18n(store, { dictionary: initialDictionary }) { const getMessageFormatter = memoize(
const formatter = new Formatter() (message, locale, formats) => new IntlMessageFormat(message, locale, formats),
let currentLocale )
let dictionary = Array.isArray(initialDictionary)
? deepmerge.all(initialDictionary)
: initialDictionary
const getLocalizedMessage = ( const lookupMessage = memoize((path, locale) => {
path, return (
interpolations, currentDictionary[locale][path] ||
locale = currentLocale, resolvePath(currentDictionary[locale], path)
transformers = undefined, )
) => { })
let message = resolvePath(dictionary[locale], path)
if (!message) return path const formatMessage = (message, interpolations, locale = currentLocale) => {
return getMessageFormatter(message, locale).format(interpolations)
if (transformers) {
for (let i = 0, len = transformers.length; i < len; i++) {
message = transformers[i](message)
}
}
if (interpolations) {
message = formatter.interpolate(message, interpolations).join('')
}
return message.trim()
}
const utilities = {
capital(path, interpolations, locale) {
return capital(getLocalizedMessage(path, interpolations, locale))
},
title(path, interpolations, locale) {
return title(getLocalizedMessage(path, interpolations, locale))
},
upper(path, interpolations, locale) {
return upper(getLocalizedMessage(path, interpolations, locale))
},
lower(path, interpolations, locale) {
return lower(getLocalizedMessage(path, interpolations, locale))
},
plural(path, counter, interpolations, locale) {
return getLocalizedMessage(path, interpolations, locale, [
message => {
const parts = message.split('|')
/** Check for 'singular|plural' or 'zero|one|multiple' pluralization */
const isSimplePluralization = parts.length === 2
let choice = isSimplePluralization ? 1 : 0
if (typeof counter === 'number') {
choice = Math.min(
Math.abs(counter) - (isSimplePluralization ? 1 : 0),
parts.length - 1,
)
}
return parts[choice]
},
])
},
}
store.on('locale', newLocale => {
if (!Object.keys(dictionary).includes(newLocale)) {
console.error(`[svelte-i18n] Couldn't find the "${newLocale}" locale.`)
return
}
currentLocale = newLocale
const _ = getLocalizedMessage
_.upper = utilities.upper
_.lower = utilities.lower
_.title = utilities.title
_.capital = utilities.capital
_.plural = utilities.plural
store.set({ locale: newLocale, _ })
})
store.i18n = {
setLocale(locale) {
store.fire('locale', locale)
},
extendDictionary(...list) {
dictionary = deepmerge.all([dictionary, ...list])
},
}
return store
} }
const getLocalizedMessage = (path, interpolations, locale = currentLocale) => {
if (typeof interpolations === 'string') {
locale = interpolations
interpolations = undefined
}
const message = lookupMessage(path, locale)
if (!message) return path
if (!interpolations) return message
return getMessageFormatter(message, locale).format(interpolations)
}
getLocalizedMessage.time = (t, format = 'short', locale) =>
formatMessage(`{t,time,${format}}`, { t }, locale)
getLocalizedMessage.date = (d, format = 'short', locale) =>
formatMessage(`{d,date,${format}}`, { d }, locale)
getLocalizedMessage.number = (n, locale) =>
formatMessage('{n,number}', { n }, locale)
getLocalizedMessage.capital = (path, interpolations, locale) =>
capital(getLocalizedMessage(path, interpolations, locale))
getLocalizedMessage.title = (path, interpolations, locale) =>
title(getLocalizedMessage(path, interpolations, locale))
getLocalizedMessage.upper = (path, interpolations, locale) =>
upper(getLocalizedMessage(path, interpolations, locale))
getLocalizedMessage.lower = (path, interpolations, locale) =>
lower(getLocalizedMessage(path, interpolations, locale))
const dictionary = writable({})
dictionary.subscribe(newDictionary => {
currentDictionary = newDictionary
})
const locale = writable({})
locale.subscribe(newLocale => {
currentLocale = newLocale
})
const format = derived(locale, () => getLocalizedMessage)
export { locale, format as _, format, dictionary }

View File

@ -1,6 +1,6 @@
export const capital = str => str.replace(/(^|\s)\S/, l => l.toUpperCase()) export const capital = str => str.replace(/(^|\s)\S/, l => l.toUpperCase())
export const title = str => str.replace(/(^|\s)\S/g, l => l.toUpperCase()) export const title = str => str.replace(/(^|\s)\S/g, l => l.toUpperCase())
export const upper = str => str.toLocaleUpperCase() export const upper = str => str.toLocaleUpperCase()
export const lower = str => str.toLocaleLowerCase() export const lower = str => str.toLocaleLowerCase()
export const isObject = obj => obj !== null && typeof obj === 'object'

View File

@ -1,182 +1,110 @@
// TODO: A more serious test import { dictionary, locale, format } from '../src/index'
import { capital, title, upper, lower } from '../src/utils'
import { i18n } from '../src/index' let _
import { Store } from 'svelte/store.umd' let currentLocale
import { capital, title, upper, lower, isObject } from '../src/utils'
let store = new Store() const dict = {
const locales = { pt: {
'pt-br': { hi: 'olá você',
test: 'teste', 'switch.lang': 'Trocar idioma',
phrase: 'adoro banana', greeting: {
phrases: ['Frase 1', 'Frase 2'], ask: 'Por favor, digite seu nome',
pluralization: 'Zero | Um | Muito!', message: 'Olá {name}, como vai?',
simplePluralization: 'Singular | Plural',
interpolation: {
key: 'Olá, {0}! Como está {1}?',
named: 'Olá, {name}! Como está {time}?',
}, },
interpolationPluralization: 'One thingie | {0} thingies', photos:
wow: { 'Você {n, plural, =0 {não tem fotos.} =1 {tem uma foto.} other {tem # fotos.}}',
much: { cats: 'Tenho {n, number} {n,plural,=0{gatos}one{gato}other{gatos}}',
deep: { },
list: ['Muito', 'muito profundo'], en: {
}, hi: 'hi yo',
}, 'switch.lang': 'Switch language',
}, greeting: {
obj: { ask: 'Please type your name',
a: 'a', message: 'Hello {name}, how are you?',
b: 'b',
}, },
photos:
'You have {n, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}',
cats: 'I have {n, number} {n,plural,one{cat}other{cats}}',
}, },
} }
i18n(store, { dictionary: [locales] }) format.subscribe(formatFn => {
_ = formatFn
})
dictionary.set(dict)
locale.subscribe(l => (currentLocale = l))
locale.set('pt')
describe('Utilities', () => { it('should change locale', () => {
it('should check if a variable is an object', () => { locale.set('pt')
expect(isObject({})).toBe(true) expect(currentLocale).toBe('pt')
expect(isObject(1)).toBe(false) locale.set('en')
}) expect(currentLocale).toBe('en')
}) })
describe('Localization', () => { it('should fallback to message id if id is not found', () => {
beforeEach(() => { expect(_('batatinha')).toBe('batatinha')
console.error = jest.fn()
})
afterEach(() => {
console.error.mockRestore()
})
it('should start with a clean store', () => {
const { _, locale } = store.get()
expect(locale).toBeFalsy()
expect(_).toBeFalsy()
})
it('should change the locale after a "locale" store event', () => {
store.fire('locale', 'pt-br')
const { locale, _ } = store.get()
expect(locale).toBe('pt-br')
expect(_).toBeInstanceOf(Function)
})
it('should have a .i18n.setLocale() method', () => {
expect(store.i18n.setLocale).toBeInstanceOf(Function)
store.i18n.setLocale('pt-br')
const { locale } = store.get()
expect(locale).toBe('pt-br')
})
it('should handle nonexistent locale', () => {
expect(store.i18n.setLocale('foo'))
expect(console.error).toHaveBeenCalledTimes(1)
})
it('should return the message id when no message identified by it was found', () => {
store.i18n.setLocale('pt-br')
const { _ } = store.get()
expect(_('non.existent')).toBe('non.existent')
})
it('should get a message by its id', () => {
const { _ } = store.get()
expect(_('test')).toBe(locales['pt-br'].test)
})
it('should get a deep nested message by its string path', () => {
store.i18n.setLocale('pt-br')
const { _ } = store.get()
expect(_('obj.b')).toBe('b')
})
it('should get a message within an array by its index', () => {
store.i18n.setLocale('pt-br')
const { _ } = store.get()
expect(_('phrases[1]')).toBe(locales['pt-br'].phrases[1])
/** Not found */
expect(_('phrases[2]')).toBe('phrases[2]')
})
it('should interpolate with {numeric} placeholders', () => {
store.i18n.setLocale('pt-br')
const { _ } = store.get()
expect(_('interpolation.key', ['Chris', 'o dia'])).toBe(
'Olá, Chris! Como está o dia?',
)
})
it('should interpolate with {named} placeholders', () => {
store.i18n.setLocale('pt-br')
const { _ } = store.get()
expect(
_('interpolation.named', {
name: 'Chris',
time: 'o dia',
}),
).toBe('Olá, Chris! Como está o dia?')
})
it('should handle pluralization with _.plural()', () => {
store.i18n.setLocale('pt-br')
const { _ } = store.get()
expect(_.plural('simplePluralization')).toBe('Plural')
expect(_.plural('simplePluralization', 1)).toBe('Singular')
expect(_.plural('simplePluralization', 3)).toBe('Plural')
expect(_.plural('simplePluralization', -23)).toBe('Plural')
expect(_.plural('pluralization')).toBe('Zero')
expect(_.plural('pluralization', 0)).toBe('Zero')
expect(_.plural('pluralization', 1)).toBe('Um')
expect(_.plural('pluralization', -1)).toBe('Um')
expect(_.plural('pluralization', -1000)).toBe('Muito!')
expect(_.plural('pluralization', 2)).toBe('Muito!')
expect(_.plural('pluralization', 100)).toBe('Muito!')
expect(_.plural('interpolationPluralization', 1)).toBe('One thingie')
expect(_.plural('interpolationPluralization', 10, [10])).toBe('10 thingies')
})
}) })
describe('Localization utilities', () => { it('should translate to current locale', () => {
locale.set('pt')
expect(_('switch.lang')).toBe('Trocar idioma')
locale.set('en')
expect(_('switch.lang')).toBe('Switch language')
})
it('should translate to passed locale', () => {
expect(_('switch.lang', 'pt')).toBe('Trocar idioma')
expect(_('switch.lang', 'en')).toBe('Switch language')
})
it('should interpolate message with variables', () => {
expect(_('greeting.message', { name: 'Chris' })).toBe(
'Hello Chris, how are you?',
)
})
it('should interpolate message with variables according to passed locale', () => {
expect(_('greeting.message', { name: 'Chris' }, 'pt')).toBe(
'Olá Chris, como vai?',
)
})
describe('utilities', () => {
beforeAll(() => {
locale.set('en')
})
it('should capital a translated message', () => { it('should capital a translated message', () => {
store.i18n.setLocale('pt-br') expect(_.capital('hi')).toBe('Hi yo')
const { _ } = store.get()
expect(capital('Adoro banana')).toBe('Adoro banana')
expect(_.capital('phrase')).toBe('Adoro banana')
}) })
it('should title a translated message', () => { it('should title a translated message', () => {
store.i18n.setLocale('pt-br') expect(_.title('hi')).toBe('Hi Yo')
const { _ } = store.get()
expect(title('Adoro Banana')).toBe('Adoro Banana')
expect(_.title('phrase')).toBe('Adoro Banana')
}) })
it('should lowercase a translated message', () => { it('should lowercase a translated message', () => {
store.i18n.setLocale('pt-br') expect(_.lower('hi')).toBe('hi yo')
const { _ } = store.get()
expect(lower('adoro banana')).toBe('adoro banana')
expect(_.lower('phrase')).toBe('adoro banana')
}) })
it('should uppercase a translated message', () => { it('should uppercase a translated message', () => {
store.i18n.setLocale('pt-br') expect(_.upper('hi')).toBe('HI YO')
const { _ } = store.get()
expect(upper('ADORO BANANA')).toBe('ADORO BANANA')
expect(_.upper('phrase')).toBe('ADORO BANANA')
}) })
const date = new Date(2019, 3, 24, 23, 45)
it('should format a time value', () => {
locale.set('en')
expect(_.time(date)).toBe('11:45 PM')
expect(_.time(date, 'medium')).toBe('11:45:00 PM')
})
it('should format a date value', () => {
expect(_.date(date)).toBe('4/24/19')
expect(_.date(date, 'medium')).toBe('Apr 24, 2019')
})
// number
it('should format a date value', () => {
expect(_.number(123123123)).toBe('123,123,123')
})
}) })