This commit is contained in:
Christian Kaisermann 2019-05-16 18:59:23 -03:00
parent cec700af53
commit 4323f56ff3
18 changed files with 13078 additions and 5034 deletions

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
```

2879
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": "../"
}
}

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: '{n,plural,one{gato}other{gatos}}'
},
})

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

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

2055
example/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

12522
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
"version": "0.0.5",
"license": "MIT",
"main": "dist/i18n.js",
"module": "dist/i18n.m.js",
"module": "src/index.js",
"types": "src/index.d.ts",
"description": "Internationalization library for Svelte",
"author": "Christian Kaisermann <christian@kaisermann.me>",
@ -50,28 +50,31 @@
"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-format-cache": "^2.1.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,70 @@
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 memoizeConstructor from 'intl-format-cache'
export { capital, title, upper, lower }
const capital = str => str.replace(/(^|\s)\S/, l => l.toUpperCase())
const title = str => str.replace(/(^|\s)\S/g, l => l.toUpperCase())
const upper = str => str.toLocaleUpperCase()
const lower = str => str.toLocaleLowerCase()
export function i18n(store, { dictionary: initialDictionary }) {
const formatter = new Formatter()
let currentLocale
let dictionary = Array.isArray(initialDictionary)
? deepmerge.all(initialDictionary)
: initialDictionary
let currentLocale
let currentDictionary
const getLocalizedMessage = (
path,
interpolations,
locale = currentLocale,
transformers = undefined,
) => {
let message = resolvePath(dictionary[locale], path)
const getMessageFormatter = memoizeConstructor(IntlMessageFormat)
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
function lookupMessage(path, locale) {
// TODO improve perf here
return (
currentDictionary[locale][path] ||
resolvePath(currentDictionary[locale], path)
)
}
function formatMessage(message, interpolations, locale = currentLocale) {
return getMessageFormatter(message, locale).format(interpolations)
}
function getLocalizedMessage(path, interpolations, locale = currentLocale) {
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 +0,0 @@
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'

View File

@ -1,11 +1,9 @@
// TODO: A more serious test
import { i18n } from '../src/index'
import { Store } from 'svelte/store.umd'
import { capital, title, upper, lower, isObject } from '../src/utils'
import { dictionary, locale } from '../src/index'
import { capital, title, upper, lower } from '../src/utils'
let store = new Store()
const locales = {
dictionary.set({
'pt-br': {
test: 'teste',
phrase: 'adoro banana',
@ -29,17 +27,11 @@ const locales = {
b: 'b',
},
},
}
i18n(store, { dictionary: [locales] })
describe('Utilities', () => {
it('should check if a variable is an object', () => {
expect(isObject({})).toBe(true)
expect(isObject(1)).toBe(false)
})
})
locale.set('pt-br')
describe('Localization', () => {
beforeEach(() => {
console.error = jest.fn()