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
/package-lock.json
/src
/coverage
/coverage
/example

280
README.md
View File

@ -4,132 +4,224 @@
## 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
import { i18n } from 'svelte-i18n'
import { Store } from 'svelte/store'
import { locale, dictionary } from 'svelte-i18n'
/** i18n(svelteStore, { dictionary }) */
let store = new Store()
// Set the current locale to en-US
locale.set('en-US')
store = i18n(store, {
dictionary: {
'pt-BR': {
message: 'Mensagem',
greeting: 'Olá {name}, como vai?',
greetingIndex: 'Olá {0}, como vai?',
meter: 'metros | metro | metros',
book: 'livro | livros',
messages: {
alert: 'Alerta',
error: 'Erro',
},
// This is a store, so we can subscribe to its changes
locale.subscribe(() => {
console.log('locale change')
})
```
---
### The dictionary
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': {
message: 'Message',
greeting: 'Hello {name}, how are you?',
greetingIndex: 'Hello {0}, how are you?',
meter: 'meters | meter | meters',
book: 'book | books',
messages: {
alert: 'Alert',
error: 'Error',
},
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: {
message: 'Message',
'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}}',
},
})
/**
* Extend the initial dictionary.
* Dictionaries are deeply merged.
* */
store.i18n.extendDictionary({
'pt-BR': {
messages: {
warn: 'Aviso',
success: 'Sucesso',
},
},
'en-US': {
messages: {
warn: 'Warn',
success: 'Success',
},
},
// It's also possible to merge the current dictionary
// with other objets
dictionary.update(dict => {
dict.fr = {
// ...french messages
}
return dict
})
/** 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
<div>
{$_('message')}: {$_('messages.success')}
<!-- Message: SUCCESS-->
{$_('greeting.message', { name: 'John' })}
<!-- Hello John, how are you? -->
{$_('photos', { n: 0 })}
<!-- You have no photos. -->
{$_('photos', { n: 12 })}
<!-- You have 12 photos. -->
</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
<div>
<!-- Named interpolation -->
{$_('greeting', { name: 'John' })}
<!-- Hello John, how are you?-->
<script>
import { _ } from 'svelte-i18n'
</script>
<!-- List interpolation -->
{$_('greetingIndex', ['John'])}
<!-- Hello John, how are you?-->
</div>
<div>{$_('greeting.ask')}</div>
<!-- Please type your name -->
```
#### Pluralization
#### `_.upper`
Transforms a localized message into uppercase.
```html
<div>
0 {$_.plural('meter', 0)}
<!-- 0 meters -->
<script>
import { _ } from 'svelte-i18n'
</script>
1 {$_.plural('meter', 1)}
<!-- 1 meter -->
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>
<div>{$_.upper('greeting.ask')}</div>
<!-- PLEASE TYPE YOUR NAME -->
```
#### Utilities
#### `_.lower`
Transforms a localized message into lowercase.
```html
<div>
{$_.upper('message')}
<!-- MESSAGE -->
<script>
import { _ } from 'svelte-i18n'
</script>
{$_.lower('message')}
<!-- message -->
{$_.capital('message')}
<!-- Message -->
{$_.title('greeting', { name: 'John' })}
<!-- Hello John, How Are You?-->
</div>
<div>{$_.lower('greeting.ask')}</div>
<!-- PLEASE TYPE YOUR NAME -->
```
#### `_.capital`
Capitalize a localized message.
```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",
"version": "0.0.5",
"version": "1.0.2",
"license": "MIT",
"main": "dist/i18n.js",
"module": "dist/i18n.m.js",
"module": "dist/i18n.mjs",
"types": "src/index.d.ts",
"description": "Internationalization library for Svelte",
"author": "Christian Kaisermann <christian@kaisermann.me>",
@ -16,13 +16,12 @@
"translation"
],
"scripts": {
"build": "microbundle --format=cjs,es",
"build": "microbundle --format=cjs,es --no-sourcemap",
"start": "microbundle watch --format=cjs,es",
"test": "jest --no-cache --verbose",
"test:watch": "jest --no-cache --verbose --watchAll",
"lint": "eslint \"src/**/*.js\"",
"format": "prettier --loglevel silent --write \"src/**/*.js\" && eslint --fix \"src/**/*.js\"",
"prepublishOnly": "npm run format && npm run test && npm run build"
"format": "prettier --loglevel silent --write \"src/**/*.js\" && eslint --fix \"src/**/*.js\""
},
"jest": {
"verbose": true,
@ -50,28 +49,30 @@
"collectCoverage": true
},
"devDependencies": {
"@babel/core": "^7.0.0-beta.56",
"@babel/preset-env": "^7.0.0-beta.56",
"@babel/core": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^23.4.2",
"eslint": "^5.3.0",
"eslint-config-prettier": "^2.9.0",
"eslint-config-standard": "^11.0.0",
"eslint-plugin-import": "^2.13.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-prettier": "^2.6.2",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-standard": "^3.1.0",
"jest": "^23.4.2",
"microbundle": "^0.6.0",
"prettier": "^1.14.1",
"svelte": "^2.9.11"
"babel-jest": "^24.8.0",
"eslint": "^5.16.0",
"eslint-config-prettier": "^4.3.0",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.17.2",
"eslint-plugin-node": "^9.0.1",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-promise": "^4.1.1",
"eslint-plugin-standard": "^4.0.0",
"jest": "^24.8.0",
"microbundle": "^0.11.0",
"prettier": "^1.17.1",
"svelte": "^3.4.1"
},
"peerDependencies": {
"svelte": "^2.9.11"
"svelte": "^3.4.1"
},
"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"
}
}

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 { capital, title, upper, lower } from './utils'
import Formatter from './formatter'
import IntlMessageFormat from 'intl-messageformat'
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 formatter = new Formatter()
let currentLocale
let dictionary = Array.isArray(initialDictionary)
? deepmerge.all(initialDictionary)
: initialDictionary
const getMessageFormatter = memoize(
(message, locale, formats) => new IntlMessageFormat(message, locale, formats),
)
const getLocalizedMessage = (
path,
interpolations,
locale = currentLocale,
transformers = undefined,
) => {
let message = resolvePath(dictionary[locale], path)
const lookupMessage = memoize((path, locale) => {
return (
currentDictionary[locale][path] ||
resolvePath(currentDictionary[locale], path)
)
})
if (!message) return path
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 formatMessage = (message, interpolations, locale = currentLocale) => {
return getMessageFormatter(message, locale).format(interpolations)
}
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 title = str => str.replace(/(^|\s)\S/g, l => l.toUpperCase())
export const upper = str => str.toLocaleUpperCase()
export const lower = str => str.toLocaleLowerCase()
export const isObject = obj => obj !== null && typeof obj === 'object'
export const lower = str => str.toLocaleLowerCase()

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'
import { Store } from 'svelte/store.umd'
import { capital, title, upper, lower, isObject } from '../src/utils'
let _
let currentLocale
let store = new Store()
const locales = {
'pt-br': {
test: 'teste',
phrase: 'adoro banana',
phrases: ['Frase 1', 'Frase 2'],
pluralization: 'Zero | Um | Muito!',
simplePluralization: 'Singular | Plural',
interpolation: {
key: 'Olá, {0}! Como está {1}?',
named: 'Olá, {name}! Como está {time}?',
const dict = {
pt: {
hi: 'olá você',
'switch.lang': 'Trocar idioma',
greeting: {
ask: 'Por favor, digite seu nome',
message: 'Olá {name}, como vai?',
},
interpolationPluralization: 'One thingie | {0} thingies',
wow: {
much: {
deep: {
list: ['Muito', 'muito profundo'],
},
},
},
obj: {
a: 'a',
b: 'b',
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: {
hi: 'hi yo',
'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}}',
},
}
i18n(store, { dictionary: [locales] })
format.subscribe(formatFn => {
_ = formatFn
})
dictionary.set(dict)
locale.subscribe(l => (currentLocale = l))
locale.set('pt')
describe('Utilities', () => {
it('should check if a variable is an object', () => {
expect(isObject({})).toBe(true)
expect(isObject(1)).toBe(false)
})
it('should change locale', () => {
locale.set('pt')
expect(currentLocale).toBe('pt')
locale.set('en')
expect(currentLocale).toBe('en')
})
describe('Localization', () => {
beforeEach(() => {
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')
})
it('should fallback to message id if id is not found', () => {
expect(_('batatinha')).toBe('batatinha')
})
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', () => {
store.i18n.setLocale('pt-br')
const { _ } = store.get()
expect(capital('Adoro banana')).toBe('Adoro banana')
expect(_.capital('phrase')).toBe('Adoro banana')
expect(_.capital('hi')).toBe('Hi yo')
})
it('should title a translated message', () => {
store.i18n.setLocale('pt-br')
const { _ } = store.get()
expect(title('Adoro Banana')).toBe('Adoro Banana')
expect(_.title('phrase')).toBe('Adoro Banana')
expect(_.title('hi')).toBe('Hi Yo')
})
it('should lowercase a translated message', () => {
store.i18n.setLocale('pt-br')
const { _ } = store.get()
expect(lower('adoro banana')).toBe('adoro banana')
expect(_.lower('phrase')).toBe('adoro banana')
expect(_.lower('hi')).toBe('hi yo')
})
it('should uppercase a translated message', () => {
store.i18n.setLocale('pt-br')
const { _ } = store.get()
expect(upper('ADORO BANANA')).toBe('ADORO BANANA')
expect(_.upper('phrase')).toBe('ADORO BANANA')
expect(_.upper('hi')).toBe('HI YO')
})
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')
})
})