mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-06-30 18:54:49 +02:00
commit
7ff154cc44
|
@ -1 +1,3 @@
|
|||
/test/fixtures
|
||||
dist/
|
||||
example/
|
|
@ -1,10 +1,13 @@
|
|||
{
|
||||
"extends": ["kaisermann"],
|
||||
"extends": ["kaisermann/typescript"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"jest": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/camelcase": "off"
|
||||
}
|
||||
}
|
||||
|
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
|||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [8, 10, 12]
|
||||
node-version: [13.2.0]
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
|
||||
steps:
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@ node_modules
|
|||
*.log
|
||||
dist/
|
||||
coverage/
|
||||
docs/
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"semi": false,
|
||||
"printWidth": 80,
|
||||
"trailingComma": "all",
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"singleQuote": true
|
||||
|
|
71
CHANGELOG.md
Normal file
71
CHANGELOG.md
Normal file
|
@ -0,0 +1,71 @@
|
|||
# [2.1.0](https://github.com/kaisermann/svelte-i18n/compare/v2.1.0-alpha.2...v2.1.0) (2019-11-30)
|
||||
|
||||
|
||||
|
||||
# [2.1.0-alpha.2](https://github.com/kaisermann/svelte-i18n/compare/v2.0.2...v2.1.0-alpha.2) (2019-11-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 allow to wait for initial locale load ([0b7f61c](https://github.com/kaisermann/svelte-i18n/commit/0b7f61c49a1c3206bbb5d9c77dfb5819a85d4bb5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 add warnOnMissingMessages ([efbe793](https://github.com/kaisermann/svelte-i18n/commit/efbe793a0f3656b27d050886d85e06e9327ea681))
|
||||
|
||||
|
||||
|
||||
## [2.0.2](https://github.com/kaisermann/svelte-i18n/compare/v2.0.0...v2.0.2) (2019-11-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 fallback behaviour and simplify API contact points ([6e0df2f](https://github.com/kaisermann/svelte-i18n/commit/6e0df2fb25e1bf9038eb4252ba993541a7fa2b4a))
|
||||
* 🐛 fallback locale being looked twice ([9260b83](https://github.com/kaisermann/svelte-i18n/commit/9260b83756b9bb2811be48817190510fa24cb2ff))
|
||||
|
||||
|
||||
|
||||
## [2.0.1](https://github.com/kaisermann/svelte-i18n/compare/v2.0.0...v2.0.1) (2019-11-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 fallback behaviour and simplify API contact points ([64e69eb](https://github.com/kaisermann/svelte-i18n/commit/64e69eb3c0f62754570429a87450ff53eb29973a))
|
||||
|
||||
|
||||
|
||||
# [2.0.0](https://github.com/kaisermann/svelte-i18n/compare/v1.1.2-beta...v2.0.0) (2019-11-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 consider generic locales when registering loaders ([1b0138c](https://github.com/kaisermann/svelte-i18n/commit/1b0138c3f3458c4d8f0b30b4550652e8e0317fc7))
|
||||
* 🐛 flush use the same promise if it wasn't resolved yet ([66972d4](https://github.com/kaisermann/svelte-i18n/commit/66972d4b1536b53d33c7974eb0fc059c0d0cc46c))
|
||||
* client locale parameters typo ([#11](https://github.com/kaisermann/svelte-i18n/issues/11)) ([d1adf4c](https://github.com/kaisermann/svelte-i18n/commit/d1adf4c00a48ed679ae34a2bffc8ca9d709a2d5c))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 `addMessagesTo` method ([d6b8664](https://github.com/kaisermann/svelte-i18n/commit/d6b8664009d738870aa3f0a4bd80e96abf6e6e59))
|
||||
* 🎸 add $loading indicator store ([bd2b350](https://github.com/kaisermann/svelte-i18n/commit/bd2b3501e9caa2e73f64835fedf93dc8939d41de))
|
||||
* 🎸 add custom formats support ([d483244](https://github.com/kaisermann/svelte-i18n/commit/d483244a9f2bb5ba63ef8be95f0e87030b5cbc7e))
|
||||
* 🎸 add pathname and hostname pattern matching ([b19b690](https://github.com/kaisermann/svelte-i18n/commit/b19b69050e252120016d47540e108f6eea193c37))
|
||||
* 🎸 add preloadLocale method ([0a0e4b3](https://github.com/kaisermann/svelte-i18n/commit/0a0e4b3bab74499d684c86e17c949160762ae19b))
|
||||
* 🎸 add waitInitialLocale helper ([6ee28e7](https://github.com/kaisermann/svelte-i18n/commit/6ee28e7d279c62060e834699714685567b6ab67c))
|
||||
* 🎸 also look for message in generic locale ([e5d7b84](https://github.com/kaisermann/svelte-i18n/commit/e5d7b84241bd7e3fdd833e82dd8a9a8f251f023c)), closes [#19](https://github.com/kaisermann/svelte-i18n/issues/19)
|
||||
* 🎸 export a store listing all locales available ([f58a20b](https://github.com/kaisermann/svelte-i18n/commit/f58a20b21eb58f891b3f9912cb6fff11eb329083))
|
||||
* 🎸 locale change automatically updates the document lang ([64c8e55](https://github.com/kaisermann/svelte-i18n/commit/64c8e55f80636a1185a1797fe486b4189ff56944))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* ⚡️ delay the $loading state change for quick loadings ([6573f51](https://github.com/kaisermann/svelte-i18n/commit/6573f51e9b817db0c77f158945572f4ba14c71fc))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This PR modifies the formatter method arguments.
|
||||
|
||||
|
||||
|
263
README.md
263
README.md
|
@ -1,247 +1,48 @@
|
|||
[![npm version](https://badge.fury.io/js/svelte-i18n.svg)](https://badge.fury.io/js/svelte-i18n) ![](https://github.com/kaisermann/svelte-i18n/workflows/CI/badge.svg)
|
||||
|
||||
# svelte-i18n
|
||||
|
||||
> Internationalization for Svelte.
|
||||
|
||||
**Note:** the `v2` version was unpublished and will be released again in the following week after I rewrite all of its tests. For now, use the `v1.1.2`.
|
||||
`svelte-i18n` helps you localize your app using the reactive tools Svelte provides. By using [stores](https://svelte.dev/docs#svelte_store) to keep track of the current `locale`, `dictionary` of messages and to `format` messages, we keep everything neat, in sync and easy to use on your svelte files.
|
||||
|
||||
## Usage
|
||||
**Requirements**
|
||||
|
||||
`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.
|
||||
- Node: `>= 11.15.0`
|
||||
- Browsers: `Chrome 38+`, `Edge 12+`, `Firefox 13+`, `Opera 25+`, `Safari 8+`.
|
||||
|
||||
---
|
||||
```svelte
|
||||
<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
</script>
|
||||
|
||||
### Locale
|
||||
<h1>{$_('page.home.title')}</h1>
|
||||
|
||||
The `locale` store defines what is the current locale.
|
||||
|
||||
```js
|
||||
import { locale, dictionary, getClientLocale } from 'svelte-i18n'
|
||||
|
||||
// Set the current locale to en-US
|
||||
locale.set('en-US')
|
||||
|
||||
// This is a store, so we can subscribe to its changes
|
||||
locale.subscribe(() => {
|
||||
console.log('locale change')
|
||||
})
|
||||
|
||||
// svelte-i18n exports a method to help getting the current client locale
|
||||
locale.set(
|
||||
getClientLocale({
|
||||
// the fallback locale, if didn't find any
|
||||
fallback: 'en-US',
|
||||
// set to 'true' to check the 'window.navigator.language'
|
||||
navigator: true,
|
||||
// set the key name to look for a locale on 'window.location.search'
|
||||
// 'example.com?locale=en-US'
|
||||
search: 'lang',
|
||||
// set the key name to look for a locale on 'window.location.hash'
|
||||
// 'example.com#locale=en-US'
|
||||
hash: 'locale',
|
||||
}),
|
||||
)
|
||||
<nav>
|
||||
<a>{$_('page.home.nav', { default: 'Home' })}</a>
|
||||
<a>{$_('page.about.nav', { default: 'About' })}</a>
|
||||
<a>{$_('page.contact.nav', { default: 'Contact' })}</a>
|
||||
</nav>
|
||||
```
|
||||
|
||||
If a locale with the format `xx-YY` is not found, `svelte-i18n` looks for the locale `xx` as well.
|
||||
|
||||
---
|
||||
|
||||
### 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?',
|
||||
```jsonc
|
||||
// en.json
|
||||
{
|
||||
"page": {
|
||||
"home": {
|
||||
"title": "Homepage",
|
||||
"nav": "Home"
|
||||
},
|
||||
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}}',
|
||||
"about": {
|
||||
"title": "About",
|
||||
"nav": "About"
|
||||
},
|
||||
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}}',
|
||||
},
|
||||
})
|
||||
|
||||
// It's also possible to merge the current dictionary
|
||||
// with other objets
|
||||
dictionary.update(dict => {
|
||||
dict.fr = {
|
||||
// ...french messages
|
||||
"contact": {
|
||||
"title": "Contact",
|
||||
"nav": "Contact Us"
|
||||
}
|
||||
return dict
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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`).
|
||||
|
||||
---
|
||||
|
||||
### 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>
|
||||
{$_('greeting.message', { name: 'John' })}
|
||||
<!-- Hello John, how are you? -->
|
||||
|
||||
{$_('photos', { n: 0 })}
|
||||
<!-- You have no photos. -->
|
||||
|
||||
{$_('photos', { n: 12 })}
|
||||
<!-- You have 12 photos. -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Formatting methods
|
||||
|
||||
#### `_` / `format`
|
||||
|
||||
`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
|
||||
<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
</script>
|
||||
|
||||
<div>{$_('greeting.ask')}</div>
|
||||
<!-- Please type your name -->
|
||||
```
|
||||
|
||||
#### `_.upper`
|
||||
|
||||
Transforms a localized message into uppercase.
|
||||
|
||||
```html
|
||||
<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
</script>
|
||||
|
||||
<div>{$_.upper('greeting.ask')}</div>
|
||||
<!-- PLEASE TYPE YOUR NAME -->
|
||||
```
|
||||
|
||||
#### `_.lower`
|
||||
|
||||
Transforms a localized message into lowercase.
|
||||
|
||||
```html
|
||||
<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
</script>
|
||||
|
||||
<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 -->
|
||||
```
|
||||
### [Go to documentation](https://github.com/kaisermann/svelte-i18n/wiki)
|
||||
|
|
7
example/.gitignore
vendored
7
example/.gitignore
vendored
|
@ -1,3 +1,6 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
public/bundle.*
|
||||
/node_modules/
|
||||
/src/node_modules/@sapper/
|
||||
yarn-error.log
|
||||
/cypress/screenshots/
|
||||
/__sapper__/
|
||||
|
|
|
@ -1,68 +1,109 @@
|
|||
*Psst — looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
|
||||
# sapper-template
|
||||
|
||||
---
|
||||
The default [Sapper](https://github.com/sveltejs/sapper) template, available for Rollup and webpack.
|
||||
|
||||
# svelte app
|
||||
|
||||
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
|
||||
## Getting started
|
||||
|
||||
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
|
||||
|
||||
### Using `degit`
|
||||
|
||||
[`degit`](https://github.com/Rich-Harris/degit) is a scaffolding tool that lets you create a directory from a branch in a repository. Use either the `rollup` or `webpack` branch in `sapper-template`:
|
||||
|
||||
```bash
|
||||
npx degit sveltejs/template svelte-app
|
||||
cd svelte-app
|
||||
# for Rollup
|
||||
npx degit "sveltejs/sapper-template#rollup" my-app
|
||||
# for webpack
|
||||
npx degit "sveltejs/sapper-template#webpack" my-app
|
||||
```
|
||||
|
||||
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
|
||||
|
||||
### Using GitHub templates
|
||||
|
||||
Alternatively, you can use GitHub's template feature with the [sapper-template-rollup](https://github.com/sveltejs/sapper-template-rollup) or [sapper-template-webpack](https://github.com/sveltejs/sapper-template-webpack) repositories.
|
||||
|
||||
|
||||
## Get started
|
||||
### Running the project
|
||||
|
||||
Install the dependencies...
|
||||
|
||||
```bash
|
||||
cd svelte-app
|
||||
npm install
|
||||
```
|
||||
|
||||
...then start [Rollup](https://rollupjs.org):
|
||||
However you get the code, you can install dependencies and run the project in development mode with:
|
||||
|
||||
```bash
|
||||
cd my-app
|
||||
npm install # or yarn
|
||||
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.
|
||||
Open up [localhost:3000](http://localhost:3000) and start clicking around.
|
||||
|
||||
Consult [sapper.svelte.dev](https://sapper.svelte.dev) for help getting started.
|
||||
|
||||
|
||||
## Deploying to the web
|
||||
## Structure
|
||||
|
||||
### With [now](https://zeit.co/now)
|
||||
Sapper expects to find two directories in the root of your project — `src` and `static`.
|
||||
|
||||
Install `now` if you haven't already:
|
||||
|
||||
### src
|
||||
|
||||
The [src](src) directory contains the entry points for your app — `client.js`, `server.js` and (optionally) a `service-worker.js` — along with a `template.html` file and a `routes` directory.
|
||||
|
||||
|
||||
#### src/routes
|
||||
|
||||
This is the heart of your Sapper app. There are two kinds of routes — *pages*, and *server routes*.
|
||||
|
||||
**Pages** are Svelte components written in `.svelte` files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.)
|
||||
|
||||
**Server routes** are modules written in `.js` files, that export functions corresponding to HTTP methods. Each function receives Express `request` and `response` objects as arguments, plus a `next` function. This is useful for creating a JSON API, for example.
|
||||
|
||||
There are three simple rules for naming the files that define your routes:
|
||||
|
||||
* A file called `src/routes/about.svelte` corresponds to the `/about` route. A file called `src/routes/blog/[slug].svelte` corresponds to the `/blog/:slug` route, in which case `params.slug` is available to the route
|
||||
* The file `src/routes/index.svelte` (or `src/routes/index.js`) corresponds to the root of your app. `src/routes/about/index.svelte` is treated the same as `src/routes/about.svelte`.
|
||||
* Files and directories with a leading underscore do *not* create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called `src/routes/_helpers/datetime.js` and it would *not* create a `/_helpers/datetime` route
|
||||
|
||||
|
||||
### static
|
||||
|
||||
The [static](static) directory contains any static assets that should be available. These are served using [sirv](https://github.com/lukeed/sirv).
|
||||
|
||||
In your [service-worker.js](src/service-worker.js) file, you can import these as `files` from the generated manifest...
|
||||
|
||||
```js
|
||||
import { files } from '@sapper/service-worker';
|
||||
```
|
||||
|
||||
...so that you can cache them (though you can choose not to, for example if you don't want to cache very large files).
|
||||
|
||||
|
||||
## Bundler config
|
||||
|
||||
Sapper uses Rollup or webpack to provide code-splitting and dynamic imports, as well as compiling your Svelte components. With webpack, it also provides hot module reloading. As long as you don't do anything daft, you can edit the configuration files to add whatever plugins you'd like.
|
||||
|
||||
|
||||
## Production mode and deployment
|
||||
|
||||
To start a production version of your app, run `npm run build && npm start`. This will disable live reloading, and activate the appropriate bundler plugins.
|
||||
|
||||
You can deploy your application to any environment that supports Node 8 or above. As an example, to deploy to [Now](https://zeit.co/now), run these commands:
|
||||
|
||||
```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/)
|
||||
## Using external components
|
||||
|
||||
Install `surge` if you haven't already:
|
||||
When using Svelte components installed from npm, such as [@sveltejs/svelte-virtual-list](https://github.com/sveltejs/svelte-virtual-list), Svelte needs the original component source (rather than any precompiled JavaScript that ships with the component). This allows the component to be rendered server-side, and also keeps your client-side app smaller.
|
||||
|
||||
Because of that, it's essential that the bundler doesn't treat the package as an *external dependency*. You can either modify the `external` option under `server` in [rollup.config.js](rollup.config.js) or the `externals` option in [webpack.config.js](webpack.config.js), or simply install the package to `devDependencies` rather than `dependencies`, which will cause it to get bundled (and therefore compiled) with your app:
|
||||
|
||||
```bash
|
||||
npm install -g surge
|
||||
npm install -D @sveltejs/svelte-virtual-list
|
||||
```
|
||||
|
||||
Then, from within your project folder:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
surge public
|
||||
```
|
||||
## Bugs and feedback
|
||||
|
||||
Sapper is in early development, and may have the odd rough edge here and there. Please be vocal over on the [Sapper issue tracker](https://github.com/sveltejs/sapper/issues).
|
||||
|
|
20
example/messages/en.json
Normal file
20
example/messages/en.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"title": {
|
||||
"index": "Sapper project template!"
|
||||
},
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"about": "About",
|
||||
"blog": "Blog"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Great success!",
|
||||
"high_five": "High five",
|
||||
"try_editing": "Try editing this file (src/routes/index.svelte) to test live reloading."
|
||||
},
|
||||
"languages": {
|
||||
"pt_BR": "Portugues",
|
||||
"en": "English",
|
||||
"es_ES": "Spanish"
|
||||
}
|
||||
}
|
20
example/messages/es-ES.json
Normal file
20
example/messages/es-ES.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"title": {
|
||||
"index": " Plantilla de proyecto Sapper!"
|
||||
},
|
||||
"nav": {
|
||||
"home": "Inicio",
|
||||
"about": "Acerca",
|
||||
"blog": "Blog"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Gran éxito!",
|
||||
"high_five": "Cinco altos",
|
||||
"try_editing": " Intente editar este archivo (src/routes/index.svelte) para probar la recarga en vivo."
|
||||
},
|
||||
"languages": {
|
||||
"pt_BR": "Portugués",
|
||||
"en": "Ingles",
|
||||
"es_ES": "Espanol"
|
||||
}
|
||||
}
|
20
example/messages/pt-BR.json
Normal file
20
example/messages/pt-BR.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"title": {
|
||||
"index": "Modelo de projeto em Sapper!"
|
||||
},
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"about": "Sobre",
|
||||
"blog": "Blog"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Suuuucesso!",
|
||||
"high_five": "Toca aqui",
|
||||
"try_editing": "Tente editar este arquivo (src/routes/index.svelte) para testar o recarregamento ao vivo."
|
||||
},
|
||||
"languages": {
|
||||
"pt_BR": "Português",
|
||||
"en": "Inglês",
|
||||
"es_ES": "Espanhol"
|
||||
}
|
||||
}
|
3453
example/package-lock.json
generated
3453
example/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -1,25 +1,33 @@
|
|||
{
|
||||
"name": "svelte-app",
|
||||
"version": "1.0.0",
|
||||
"version": "0.0.1",
|
||||
"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.15.6",
|
||||
"rollup-plugin-commonjs": "^10.0.0",
|
||||
"rollup-plugin-livereload": "^1.0.1",
|
||||
"rollup-plugin-node-resolve": "^5.0.3",
|
||||
"rollup-plugin-svelte": "^5.1.0",
|
||||
"rollup-plugin-terser": "^5.0.0",
|
||||
"sirv-cli": "^0.4.4",
|
||||
"svelte": "^3.5.1"
|
||||
"dev": "sapper dev",
|
||||
"build": "sapper build --legacy",
|
||||
"export": "export NODE_ENV=development; sapper export --legacy",
|
||||
"start": "node __sapper__/build"
|
||||
},
|
||||
"dependencies": {
|
||||
"svelte-i18n": "^1.1.0"
|
||||
"compression": "^1.7.1",
|
||||
"polka": "next",
|
||||
"sirv": "^0.4.0",
|
||||
"svelte-i18n": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@rollup/plugin-json": "^4.0.0",
|
||||
"@rollup/plugin-replace": "^2.2.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rollup": "^1.12.0",
|
||||
"rollup-plugin-babel": "^4.0.2",
|
||||
"rollup-plugin-commonjs": "^10.0.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-svelte": "^5.0.1",
|
||||
"rollup-plugin-terser": "^4.0.4",
|
||||
"sapper": "^0.27.0",
|
||||
"svelte": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
|
2
example/public/bundle.css
Normal file
2
example/public/bundle.css
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
/*# sourceMappingURL=bundle.css.map */
|
3319
example/public/bundle.js
Normal file
3319
example/public/bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -1,61 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<!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>
|
|
@ -1,44 +1,109 @@
|
|||
import svelte from 'rollup-plugin-svelte'
|
||||
import resolve from 'rollup-plugin-node-resolve'
|
||||
import replace from '@rollup/plugin-replace'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
import livereload from 'rollup-plugin-livereload'
|
||||
import svelte from 'rollup-plugin-svelte'
|
||||
import babel from 'rollup-plugin-babel'
|
||||
import json from '@rollup/plugin-json'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import config from 'sapper/config/rollup.js'
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH
|
||||
import pkg from './package.json'
|
||||
|
||||
const mode = process.env.NODE_ENV
|
||||
const dev = mode === 'development'
|
||||
const legacy = !!process.env.SAPPER_LEGACY_BUILD
|
||||
|
||||
const onwarn = (warning, onwarn) =>
|
||||
(warning.code === 'CIRCULAR_DEPENDENCY' &&
|
||||
/[/\\]@sapper[/\\]/.test(warning.message)) ||
|
||||
onwarn(warning)
|
||||
const dedupe = importee =>
|
||||
importee === 'svelte' || importee.startsWith('svelte/')
|
||||
|
||||
export default {
|
||||
input: 'src/main.js',
|
||||
output: {
|
||||
sourcemap: true,
|
||||
format: 'iife',
|
||||
name: 'app',
|
||||
file: 'public/bundle.js',
|
||||
},
|
||||
client: {
|
||||
input: config.client.input(),
|
||||
output: config.client.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode),
|
||||
}),
|
||||
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')
|
||||
},
|
||||
dev,
|
||||
hydratable: true,
|
||||
emitCss: true,
|
||||
}),
|
||||
resolve({
|
||||
browser: true,
|
||||
dedupe,
|
||||
}),
|
||||
commonjs(),
|
||||
json({
|
||||
namedExports: false,
|
||||
compact: !dev,
|
||||
}),
|
||||
legacy &&
|
||||
babel({
|
||||
extensions: ['.js', '.mjs', '.html', '.svelte'],
|
||||
runtimeHelpers: true,
|
||||
exclude: ['node_modules/@babel/**'],
|
||||
presets: [['@babel/preset-env', { targets: '> 0.25%, not dead' }]],
|
||||
plugins: [
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
['@babel/plugin-transform-runtime', { useESModules: true }],
|
||||
],
|
||||
}),
|
||||
|
||||
// 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(),
|
||||
!dev &&
|
||||
terser({
|
||||
module: true,
|
||||
}),
|
||||
],
|
||||
|
||||
onwarn,
|
||||
},
|
||||
server: {
|
||||
input: config.server.input(),
|
||||
output: config.server.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': false,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode),
|
||||
}),
|
||||
svelte({
|
||||
generate: 'ssr',
|
||||
dev,
|
||||
}),
|
||||
resolve({
|
||||
dedupe,
|
||||
}),
|
||||
commonjs(),
|
||||
json({
|
||||
namedExports: false,
|
||||
compact: !dev,
|
||||
}),
|
||||
],
|
||||
external: Object.keys(pkg.dependencies).concat(
|
||||
require('module').builtinModules ||
|
||||
Object.keys(process.binding('natives'))
|
||||
),
|
||||
onwarn,
|
||||
},
|
||||
|
||||
serviceworker: {
|
||||
input: config.serviceworker.input(),
|
||||
output: config.serviceworker.output(),
|
||||
plugins: [
|
||||
resolve(),
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode),
|
||||
}),
|
||||
commonjs(),
|
||||
!dev && terser(),
|
||||
],
|
||||
|
||||
onwarn,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
<script>
|
||||
import { locale, _ } from '../../src/index.js'
|
||||
|
||||
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', { values: { name } })}</h1>
|
||||
|
||||
<br />
|
||||
<input type="range" min="0" max="5" step="1" bind:value={pluralN} />
|
||||
<h2>Plural: {$_('photos', { values: { n: pluralN } })}</h2>
|
||||
|
||||
<br />
|
||||
<input type="range" min="100" max="100000000" step="10000" bind:value={catsN} />
|
||||
<h2>Number: {$_('cats', { values: { n: catsN } })}</h2>
|
||||
|
||||
<br />
|
||||
<h2>Number util: {$_.number(catsN)}</h2>
|
||||
<h2>Number util: {$_.number(10000000, { format: 'compactShort' })}</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', { locale: oppositeLocale })}
|
||||
</button>
|
7
example/src/client.js
Normal file
7
example/src/client.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import * as sapper from '@sapper/app'
|
||||
|
||||
import './i18n.js'
|
||||
|
||||
sapper.start({
|
||||
target: document.querySelector('#sapper'),
|
||||
})
|
86
example/src/components/Nav.svelte
Normal file
86
example/src/components/Nav.svelte
Normal file
|
@ -0,0 +1,86 @@
|
|||
<script>
|
||||
import { _, locale, locales } from 'svelte-i18n'
|
||||
export let segment
|
||||
</script>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
border-bottom: 1px solid rgba(255, 62, 0, 0.1);
|
||||
font-weight: 300;
|
||||
padding: 0 1em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* clearfix */
|
||||
ul::after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
li {
|
||||
display: block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.selected {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.selected::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: calc(100% - 1em);
|
||||
height: 2px;
|
||||
background-color: rgb(255, 62, 0);
|
||||
display: block;
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
a,
|
||||
.a {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
padding: 1em 0.5em;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a class:selected={segment === undefined} href=".">{$_('nav.home')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class:selected={segment === 'about'} href="about">{$_('nav.about')}</a>
|
||||
</li>
|
||||
|
||||
<!-- for the blog link, we're using rel=prefetch so that Sapper prefetches
|
||||
the blog data when we hover over the link or tap it on a touchscreen -->
|
||||
<li>
|
||||
<a rel="prefetch" class:selected={segment === 'blog'} href="blog">
|
||||
{$_('nav.blog')}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="lang">
|
||||
{#each $locales as item}
|
||||
<li>
|
||||
<span
|
||||
class="a"
|
||||
class:selected={$locale.includes(item)}
|
||||
href={`#!${item}`}
|
||||
on:click={() => ($locale = item)}>
|
||||
{item.replace('-', '_')}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
|
@ -1,51 +1,10 @@
|
|||
import {
|
||||
locale,
|
||||
dictionary,
|
||||
getClientLocale,
|
||||
addCustomFormats,
|
||||
} from '../../src/index.js'
|
||||
import { register, init } from 'svelte-i18n'
|
||||
|
||||
addCustomFormats({
|
||||
number: {
|
||||
compact: {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'long',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 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}}',
|
||||
},
|
||||
})
|
||||
|
||||
locale.set(
|
||||
getClientLocale({
|
||||
navigator: true,
|
||||
hash: 'lang',
|
||||
fallback: 'pt',
|
||||
}),
|
||||
)
|
||||
|
||||
locale.subscribe(l => {
|
||||
console.log('locale change', l)
|
||||
register('en', () => import('../messages/en.json'))
|
||||
register('pt-BR', () => import('../messages/pt-BR.json'))
|
||||
register('es-ES', () => import('../messages/es-ES.json'))
|
||||
|
||||
init({
|
||||
fallbackLocale: 'en',
|
||||
initialLocale: { navigator: true },
|
||||
})
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import './i18n.js'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = new App({
|
||||
target: document.body,
|
||||
props: { name: 'world' },
|
||||
})
|
||||
|
||||
export default app
|
40
example/src/routes/_error.svelte
Normal file
40
example/src/routes/_error.svelte
Normal file
|
@ -0,0 +1,40 @@
|
|||
<script>
|
||||
export let status;
|
||||
export let error;
|
||||
|
||||
const dev = process.env.NODE_ENV === 'development';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
h1, p {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.8em;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
h1 {
|
||||
font-size: 4em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>{status}</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>{status}</h1>
|
||||
|
||||
<p>{error.message}</p>
|
||||
|
||||
{#if dev && error.stack}
|
||||
<pre>{error.stack}</pre>
|
||||
{/if}
|
50
example/src/routes/_layout.svelte
Normal file
50
example/src/routes/_layout.svelte
Normal file
|
@ -0,0 +1,50 @@
|
|||
<script context="module">
|
||||
import { isLoading, waitLocale } from 'svelte-i18n'
|
||||
|
||||
export async function preload() {
|
||||
return waitLocale()
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Nav from '../components/Nav.svelte'
|
||||
|
||||
export let segment
|
||||
</script>
|
||||
|
||||
<style>
|
||||
main {
|
||||
position: relative;
|
||||
max-width: 56em;
|
||||
background-color: white;
|
||||
padding: 2em;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
font-size: 4rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if $isLoading}
|
||||
<div class="loading">Loading...</div>
|
||||
{/if}
|
||||
|
||||
<Nav {segment} />
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
7
example/src/routes/about/_locales/default.json
Normal file
7
example/src/routes/about/_locales/default.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"title": {
|
||||
"about": "About"
|
||||
},
|
||||
"about_this_site": "About this site",
|
||||
"about_content": ["This is the 'about' page. There's not much here."]
|
||||
}
|
7
example/src/routes/about/_locales/en.json
Normal file
7
example/src/routes/about/_locales/en.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"title": {
|
||||
"about": "About"
|
||||
},
|
||||
"about_this_site": "About this site",
|
||||
"about_content": ["This is the 'about' page. There's not much here."]
|
||||
}
|
7
example/src/routes/about/_locales/es-ES.json
Normal file
7
example/src/routes/about/_locales/es-ES.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"title": {
|
||||
"about": "Acerca de"
|
||||
},
|
||||
"about_this_site": " Acerca de este sitio",
|
||||
"about_content": ["Esta es la página 'acerca de'. No hay mucho aquí."]
|
||||
}
|
7
example/src/routes/about/_locales/pt-BR.json
Normal file
7
example/src/routes/about/_locales/pt-BR.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"title": {
|
||||
"about": "Sobre"
|
||||
},
|
||||
"about_this_site": "Sobre este site",
|
||||
"about_content": ["Esta é a página 'sobre'. Não há muito aqui."]
|
||||
}
|
23
example/src/routes/about/index.svelte
Normal file
23
example/src/routes/about/index.svelte
Normal file
|
@ -0,0 +1,23 @@
|
|||
<script context="module">
|
||||
import { register, waitLocale } from 'svelte-i18n'
|
||||
|
||||
register('en', () => import('./_locales/en.json'))
|
||||
register('pt-BR', () => import('./_locales/pt-BR.json'))
|
||||
register('es-ES', () => import('./_locales/es-ES.json'))
|
||||
|
||||
export async function preload(page, session) {
|
||||
return waitLocale()
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_('title.about')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>{$_('about_this_site')}</h1>
|
||||
|
||||
<p>{$_('about_content.0')}</p>
|
28
example/src/routes/blog/[slug].json.js
Normal file
28
example/src/routes/blog/[slug].json.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import posts from './_posts.js';
|
||||
|
||||
const lookup = new Map();
|
||||
posts.forEach(post => {
|
||||
lookup.set(post.slug, JSON.stringify(post));
|
||||
});
|
||||
|
||||
export function get(req, res, next) {
|
||||
// the `slug` parameter is available because
|
||||
// this file is called [slug].json.js
|
||||
const { slug } = req.params;
|
||||
|
||||
if (lookup.has(slug)) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(lookup.get(slug));
|
||||
} else {
|
||||
res.writeHead(404, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({
|
||||
message: `Not found`
|
||||
}));
|
||||
}
|
||||
}
|
64
example/src/routes/blog/[slug].svelte
Normal file
64
example/src/routes/blog/[slug].svelte
Normal file
|
@ -0,0 +1,64 @@
|
|||
<script context="module">
|
||||
export async function preload({ params, query }) {
|
||||
// the `slug` parameter is available because
|
||||
// this file is called [slug].svelte
|
||||
const res = await this.fetch(`blog/${params.slug}.json`);
|
||||
const data = await res.json();
|
||||
|
||||
if (res.status === 200) {
|
||||
return { post: data };
|
||||
} else {
|
||||
this.error(res.status, data.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let post;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*
|
||||
By default, CSS is locally scoped to the component,
|
||||
and any unused styles are dead-code-eliminated.
|
||||
In this page, Svelte can't know which elements are
|
||||
going to appear inside the {{{post.html}}} block,
|
||||
so we have to use the :global(...) modifier to target
|
||||
all elements inside .content
|
||||
*/
|
||||
.content :global(h2) {
|
||||
font-size: 1.4em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content :global(pre) {
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: inset 1px 1px 5px rgba(0,0,0,0.05);
|
||||
padding: 0.5em;
|
||||
border-radius: 2px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.content :global(pre) :global(code) {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content :global(ul) {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.content :global(li) {
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>{post.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>{post.title}</h1>
|
||||
|
||||
<div class='content'>
|
||||
{@html post.html}
|
||||
</div>
|
92
example/src/routes/blog/_posts.js
Normal file
92
example/src/routes/blog/_posts.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
// Ordinarily, you'd generate this data from markdown files in your
|
||||
// repo, or fetch them from a database of some kind. But in order to
|
||||
// avoid unnecessary dependencies in the starter template, and in the
|
||||
// service of obviousness, we're just going to leave it here.
|
||||
|
||||
// This file is called `_posts.js` rather than `posts.js`, because
|
||||
// we don't want to create an `/blog/posts` route — the leading
|
||||
// underscore tells Sapper not to do that.
|
||||
|
||||
const posts = [
|
||||
{
|
||||
title: 'What is Sapper?',
|
||||
slug: 'what-is-sapper',
|
||||
html: `
|
||||
<p>First, you have to know what <a href='https://svelte.dev'>Svelte</a> is. Svelte is a UI framework with a bold new idea: rather than providing a library that you write code with (like React or Vue, for example), it's a compiler that turns your components into highly optimized vanilla JavaScript. If you haven't already read the <a href='https://svelte.dev/blog/frameworks-without-the-framework'>introductory blog post</a>, you should!</p>
|
||||
|
||||
<p>Sapper is a Next.js-style framework (<a href='blog/how-is-sapper-different-from-next'>more on that here</a>) built around Svelte. It makes it embarrassingly easy to create extremely high performance web apps. Out of the box, you get:</p>
|
||||
|
||||
<ul>
|
||||
<li>Code-splitting, dynamic imports and hot module replacement, powered by webpack</li>
|
||||
<li>Server-side rendering (SSR) with client-side hydration</li>
|
||||
<li>Service worker for offline support, and all the PWA bells and whistles</li>
|
||||
<li>The nicest development experience you've ever had, or your money back</li>
|
||||
</ul>
|
||||
|
||||
<p>It's implemented as Express middleware. Everything is set up and waiting for you to get started, but you keep complete control over the server, service worker, webpack config and everything else, so it's as flexible as you need it to be.</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'How to use Sapper',
|
||||
slug: 'how-to-use-sapper',
|
||||
html: `
|
||||
<h2>Step one</h2>
|
||||
<p>Create a new project, using <a href='https://github.com/Rich-Harris/degit'>degit</a>:</p>
|
||||
|
||||
<pre><code>npx degit "sveltejs/sapper-template#rollup" my-app
|
||||
cd my-app
|
||||
npm install # or yarn!
|
||||
npm run dev
|
||||
</code></pre>
|
||||
|
||||
<h2>Step two</h2>
|
||||
<p>Go to <a href='http://localhost:3000'>localhost:3000</a>. Open <code>my-app</code> in your editor. Edit the files in the <code>src/routes</code> directory or add new ones.</p>
|
||||
|
||||
<h2>Step three</h2>
|
||||
<p>...</p>
|
||||
|
||||
<h2>Step four</h2>
|
||||
<p>Resist overdone joke formats.</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Why the name?',
|
||||
slug: 'why-the-name',
|
||||
html: `
|
||||
<p>In war, the soldiers who build bridges, repair roads, clear minefields and conduct demolitions — all under combat conditions — are known as <em>sappers</em>.</p>
|
||||
|
||||
<p>For web developers, the stakes are generally lower than those for combat engineers. But we face our own hostile environment: underpowered devices, poor network connections, and the complexity inherent in front-end engineering. Sapper, which is short for <strong>S</strong>velte <strong>app</strong> mak<strong>er</strong>, is your courageous and dutiful ally.</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'How is Sapper different from Next.js?',
|
||||
slug: 'how-is-sapper-different-from-next',
|
||||
html: `
|
||||
<p><a href='https://github.com/zeit/next.js'>Next.js</a> is a React framework from <a href='https://zeit.co'>Zeit</a>, and is the inspiration for Sapper. There are a few notable differences, however:</p>
|
||||
|
||||
<ul>
|
||||
<li>It's powered by <a href='https://svelte.dev'>Svelte</a> instead of React, so it's faster and your apps are smaller</li>
|
||||
<li>Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is <code>src/routes/blog/[slug].html</code></li>
|
||||
<li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> objects as arguments. This makes it very easy to, for example, add a JSON API such as the one <a href='blog/how-is-sapper-different-from-next.json'>powering this very page</a></li>
|
||||
<li>Links are just <code><a></code> elements, rather than framework-specific <code><Link></code> components. That means, for example, that <a href='blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'How can I get involved?',
|
||||
slug: 'how-can-i-get-involved',
|
||||
html: `
|
||||
<p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://svelte.dev/chat'>Discord chatroom</a>. Everyone is welcome, especially you!</p>
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
posts.forEach(post => {
|
||||
post.html = post.html.replace(/^\t{3}/gm, '');
|
||||
});
|
||||
|
||||
export default posts;
|
16
example/src/routes/blog/index.json.js
Normal file
16
example/src/routes/blog/index.json.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import posts from './_posts.js';
|
||||
|
||||
const contents = JSON.stringify(posts.map(post => {
|
||||
return {
|
||||
title: post.title,
|
||||
slug: post.slug
|
||||
};
|
||||
}));
|
||||
|
||||
export function get(req, res) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(contents);
|
||||
}
|
34
example/src/routes/blog/index.svelte
Normal file
34
example/src/routes/blog/index.svelte
Normal file
|
@ -0,0 +1,34 @@
|
|||
<script context="module">
|
||||
export function preload({ params, query }) {
|
||||
return this.fetch(`blog.json`).then(r => r.json()).then(posts => {
|
||||
return { posts };
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let posts;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
ul {
|
||||
margin: 0 0 1em 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>Blog</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>Recent posts</h1>
|
||||
|
||||
<ul>
|
||||
{#each posts as post}
|
||||
<!-- we're using the non-standard `rel=prefetch` attribute to
|
||||
tell Sapper to load the data for the page as soon as
|
||||
the user hovers over the link or taps it, instead of
|
||||
waiting for the 'click' event -->
|
||||
<li><a rel='prefetch' href='blog/{post.slug}'>{post.title}</a></li>
|
||||
{/each}
|
||||
</ul>
|
59
example/src/routes/index.svelte
Normal file
59
example/src/routes/index.svelte
Normal file
|
@ -0,0 +1,59 @@
|
|||
<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
</script>
|
||||
|
||||
<style>
|
||||
h1,
|
||||
figure,
|
||||
p {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.8em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
h1 {
|
||||
font-size: 4em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_('title.index', { default: 'Sapper project template!' })}</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>{$_('messages.success', { default: 'Great success!' })}</h1>
|
||||
|
||||
<figure>
|
||||
<img alt="Borat" src="great-success.png" />
|
||||
<figcaption>{$_('messages.high_five', { default: 'High five' })}</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>
|
||||
<strong>
|
||||
{$_('messages.try_editing', {
|
||||
default:
|
||||
'Try editing this file (src/routes/index.svelte) to test live reloading.',
|
||||
})}
|
||||
</strong>
|
||||
</p>
|
19
example/src/server.js
Normal file
19
example/src/server.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import sirv from 'sirv'
|
||||
import polka from 'polka'
|
||||
import compression from 'compression'
|
||||
import * as sapper from '@sapper/server'
|
||||
|
||||
import './i18n.js'
|
||||
|
||||
const { PORT, NODE_ENV } = process.env
|
||||
const dev = NODE_ENV === 'development'
|
||||
|
||||
polka() // You can also use Express
|
||||
.use(
|
||||
compression({ threshold: 0 }),
|
||||
sirv('static', { dev }),
|
||||
sapper.middleware()
|
||||
)
|
||||
.listen(PORT, err => {
|
||||
if (err) console.log('error', err)
|
||||
})
|
85
example/src/service-worker.js
Normal file
85
example/src/service-worker.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { timestamp, files, shell, routes } from '@sapper/service-worker'
|
||||
|
||||
const ASSETS = `cache${timestamp}`
|
||||
|
||||
// `shell` is an array of all the files generated by the bundler,
|
||||
// `files` is an array of everything in the `static` directory
|
||||
const to_cache = shell.concat(files)
|
||||
const cached = new Set(to_cache)
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(ASSETS)
|
||||
.then(cache => cache.addAll(to_cache))
|
||||
.then(() => {
|
||||
self.skipWaiting()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(async keys => {
|
||||
// delete old caches
|
||||
for (const key of keys) {
|
||||
if (key !== ASSETS) await caches.delete(key)
|
||||
}
|
||||
|
||||
self.clients.claim()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method !== 'GET' || event.request.headers.has('range'))
|
||||
return
|
||||
|
||||
const url = new URL(event.request.url)
|
||||
|
||||
// don't try to handle e.g. data: URIs
|
||||
if (!url.protocol.startsWith('http')) return
|
||||
|
||||
// ignore dev server requests
|
||||
if (
|
||||
url.hostname === self.location.hostname &&
|
||||
url.port !== self.location.port
|
||||
)
|
||||
return
|
||||
|
||||
// always serve static files and bundler-generated assets from cache
|
||||
if (url.host === self.location.host && cached.has(url.pathname)) {
|
||||
event.respondWith(caches.match(event.request))
|
||||
return
|
||||
}
|
||||
|
||||
// for pages, you might want to serve a shell `service-worker-index.html` file,
|
||||
// which Sapper has generated for you. It's not right for every
|
||||
// app, but if it's right for yours then uncomment this section
|
||||
/*
|
||||
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
|
||||
event.respondWith(caches.match('/service-worker-index.html'));
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
if (event.request.cache === 'only-if-cached') return
|
||||
|
||||
// for everything else, try the network first, falling back to
|
||||
// cache if the user is offline. (If the pages never change, you
|
||||
// might prefer a cache-first approach to a network-first one.)
|
||||
event.respondWith(
|
||||
caches.open(`offline${timestamp}`).then(async cache => {
|
||||
try {
|
||||
const response = await fetch(event.request)
|
||||
cache.put(event.request, response.clone())
|
||||
return response
|
||||
} catch (err) {
|
||||
const response = await cache.match(event.request)
|
||||
if (response) return response
|
||||
|
||||
throw err
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
33
example/src/template.html
Normal file
33
example/src/template.html
Normal file
|
@ -0,0 +1,33 @@
|
|||
<!doctype html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1.0'>
|
||||
<meta name='theme-color' content='#333333'>
|
||||
|
||||
%sapper.base%
|
||||
|
||||
<link rel='stylesheet' href='global.css'>
|
||||
<link rel='manifest' href='manifest.json'>
|
||||
<link rel='icon' type='image/png' href='favicon.png'>
|
||||
|
||||
<!-- Sapper generates a <style> tag containing critical CSS
|
||||
for the current page. CSS for the rest of the app is
|
||||
lazily loaded when it precaches secondary pages -->
|
||||
%sapper.styles%
|
||||
|
||||
<!-- This contains the contents of the <svelte:head> component, if
|
||||
the current page has one -->
|
||||
%sapper.head%
|
||||
</head>
|
||||
<body>
|
||||
<!-- The application will be rendered inside this element,
|
||||
because `src/client.js` references it -->
|
||||
<div id='sapper'>%sapper.html%</div>
|
||||
|
||||
<!-- Sapper creates a <script> tag containing `src/client.js`
|
||||
and anything else it needs to hydrate the app and
|
||||
initialise the router -->
|
||||
%sapper.scripts%
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
36
example/static/global.css
Normal file
36
example/static/global.css
Normal file
|
@ -0,0 +1,36 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0 0 0.5em 0;
|
||||
font-weight: 400;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: menlo, inconsolata, monospace;
|
||||
font-size: calc(1em - 2px);
|
||||
color: #555;
|
||||
background-color: #f0f0f0;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@media (min-width: 400px) {
|
||||
body {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
BIN
example/static/great-success.png
Normal file
BIN
example/static/great-success.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
BIN
example/static/logo-192.png
Normal file
BIN
example/static/logo-192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
example/static/logo-512.png
Normal file
BIN
example/static/logo-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
20
example/static/manifest.json
Normal file
20
example/static/manifest.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#333333",
|
||||
"name": "TODO",
|
||||
"short_name": "TODO",
|
||||
"display": "minimal-ui",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "logo-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "logo-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
4056
package-lock.json
generated
4056
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
80
package.json
80
package.json
|
@ -1,12 +1,19 @@
|
|||
{
|
||||
"name": "svelte-i18n",
|
||||
<<<<<<< HEAD
|
||||
"version": "2.1.0",
|
||||
=======
|
||||
"version": "1.1.2",
|
||||
>>>>>>> master
|
||||
"main": "dist/i18n.js",
|
||||
"module": "dist/i18n.mjs",
|
||||
"bin": {
|
||||
"svelte-i18n": "dist/cli.js"
|
||||
},
|
||||
"license": "MIT",
|
||||
"description": "Internationalization library for Svelte",
|
||||
"author": "Christian Kaisermann <christian@kaisermann.me>",
|
||||
"repository": "https://github.com/kaisermann",
|
||||
"repository": "https://github.com/kaisermann/svelte-i18n",
|
||||
"keywords": [
|
||||
"svelte",
|
||||
"i18n",
|
||||
|
@ -14,67 +21,82 @@
|
|||
"localization",
|
||||
"translation"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 11.15.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
"pretest": "npm run build",
|
||||
<<<<<<< HEAD
|
||||
"test": "jest",
|
||||
"test:ci": "jest --silent",
|
||||
"test:watch": "jest --verbose --watchAll",
|
||||
"lint": "eslint \"src/**/*.ts\"",
|
||||
"format": "prettier --loglevel silent --write \"src/**/*.ts\" && eslint --fix \"src/**/*.ts\"",
|
||||
"prerelease": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1 && git add CHANGELOG.md",
|
||||
"release": " git add package.json && git commit -m \"chore(release): v$npm_package_version :tada:\"",
|
||||
"postrelease": "git tag -a v$npm_package_version -m 'Release v$npm_package_version'",
|
||||
"prepublishOnly": "run-s test:ci build release"
|
||||
=======
|
||||
"test": "jest --no-cache --verbose",
|
||||
"test:watch": "jest --no-cache --verbose --watchAll",
|
||||
"test:ci": "jest --no-cache --silent",
|
||||
"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"
|
||||
>>>>>>> master
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"src/"
|
||||
"dist/"
|
||||
],
|
||||
"jest": {
|
||||
"verbose": true,
|
||||
"testURL": "http://localhost/",
|
||||
"collectCoverage": true,
|
||||
"testMatch": [
|
||||
"<rootDir>/test/**/*.test.ts"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.ts"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.jsx?$": "babel-jest"
|
||||
},
|
||||
"testRegex": "(/test/.*|\\.(test|spec))\\.js$",
|
||||
"moduleFileExtensions": [
|
||||
"js"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
"/test/"
|
||||
],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 90,
|
||||
"functions": 95,
|
||||
"lines": 95,
|
||||
"statements": 95
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
}
|
||||
},
|
||||
"collectCoverage": false
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@types/estree": "0.0.39",
|
||||
"@types/intl": "^1.2.0",
|
||||
"@types/jest": "^24.0.23",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^24.9.0",
|
||||
"conventional-changelog-cli": "^2.0.28",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-kaisermann": "0.0.3",
|
||||
"jest": "^24.9.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^1.19.1",
|
||||
"rollup": "^1.26.5",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-terser": "^5.1.2",
|
||||
"svelte": "^3.14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.14.0"
|
||||
"rollup-plugin-typescript2": "^0.25.2",
|
||||
"sass": "^1.23.6",
|
||||
"svelte": "^3.14.1",
|
||||
"svelte-preprocess": "^3.2.6",
|
||||
"ts-jest": "^24.1.0",
|
||||
"typescript": "^3.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^4.0.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"estree-walker": "^0.9.0",
|
||||
"fast-memoize": "^2.5.1",
|
||||
"intl-messageformat": "^7.5.2",
|
||||
"micro-memoize": "^4.0.8",
|
||||
"object-resolve-path": "^1.1.1"
|
||||
"tiny-glob": "^0.2.6"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,41 @@
|
|||
import resolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
import ts from 'rollup-plugin-typescript2'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import autoExternal from 'rollup-plugin-auto-external'
|
||||
|
||||
import pkg from './package.json'
|
||||
|
||||
const plugins = [resolve(), commonjs(), terser()]
|
||||
const PROD = !process.env.ROLLUP_WATCH
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.js',
|
||||
external: [...Object.keys(pkg.dependencies), 'svelte/store'],
|
||||
input: 'src/client/index.ts',
|
||||
external: [
|
||||
...Object.keys(pkg.dependencies),
|
||||
...Object.keys(pkg.peerDependencies),
|
||||
'svelte/store',
|
||||
],
|
||||
output: [
|
||||
{ file: pkg.module, format: 'es' },
|
||||
{ file: pkg.main, format: 'cjs' },
|
||||
],
|
||||
plugins,
|
||||
plugins: [commonjs(), autoExternal(), ts(), PROD && terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/cli/index.ts',
|
||||
|
||||
// external: id => {
|
||||
// if (id.startsWith('/')) return false
|
||||
// return externals.has(id) || id.match(/svelte/gi)
|
||||
// },
|
||||
output: [
|
||||
{
|
||||
file: pkg.bin['svelte-i18n'],
|
||||
name: 'cli.js',
|
||||
format: 'cjs',
|
||||
banner: `#!/usr/bin/env node`,
|
||||
},
|
||||
],
|
||||
plugins: [autoExternal(), commonjs(), ts(), PROD && terser()],
|
||||
},
|
||||
]
|
||||
|
|
184
src/cli/extract.ts
Normal file
184
src/cli/extract.ts
Normal file
|
@ -0,0 +1,184 @@
|
|||
import {
|
||||
Node,
|
||||
ObjectExpression,
|
||||
ImportDeclaration,
|
||||
ImportSpecifier,
|
||||
CallExpression,
|
||||
Identifier,
|
||||
Literal,
|
||||
} from 'estree'
|
||||
import delve from 'dlv'
|
||||
import { walk } from 'estree-walker'
|
||||
import { Ast } from 'svelte/types/compiler/interfaces'
|
||||
import { parse } from 'svelte/compiler'
|
||||
|
||||
import { deepSet } from './includes/deepSet'
|
||||
import { getObjFromExpression } from './includes/getObjFromExpression'
|
||||
import { Message } from './types'
|
||||
|
||||
const LIB_NAME = 'svelte-i18n'
|
||||
const DEFINE_MESSAGES_METHOD_NAME = 'defineMessages'
|
||||
const FORMAT_METHOD_NAMES = new Set(['format', '_', 't'])
|
||||
const IGNORED_UTILITIES = new Set(['number', 'date', 'time'])
|
||||
|
||||
function isFormatCall(node: Node, imports: Set<string>) {
|
||||
if (node.type !== 'CallExpression') return false
|
||||
|
||||
let identifier: Identifier
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.property.type === 'Identifier' &&
|
||||
!IGNORED_UTILITIES.has(node.callee.property.name)
|
||||
) {
|
||||
identifier = node.callee.object as Identifier
|
||||
} else if (node.callee.type === 'Identifier') {
|
||||
identifier = node.callee
|
||||
}
|
||||
if (!identifier || identifier.type !== 'Identifier') {
|
||||
return false
|
||||
}
|
||||
|
||||
const methodName = identifier.name.slice(1)
|
||||
return imports.has(methodName)
|
||||
}
|
||||
|
||||
function isMessagesDefinitionCall(node: Node, methodName: string) {
|
||||
if (node.type !== 'CallExpression') return false
|
||||
|
||||
return (
|
||||
node.callee &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === methodName
|
||||
)
|
||||
}
|
||||
|
||||
function getLibImportDeclarations(ast: Ast) {
|
||||
return (ast.instance
|
||||
? ast.instance.content.body.filter(
|
||||
node =>
|
||||
node.type === 'ImportDeclaration' && node.source.value === LIB_NAME
|
||||
)
|
||||
: []) as ImportDeclaration[]
|
||||
}
|
||||
|
||||
function getDefineMessagesSpecifier(decl: ImportDeclaration) {
|
||||
return decl.specifiers.find(
|
||||
spec =>
|
||||
'imported' in spec && spec.imported.name === DEFINE_MESSAGES_METHOD_NAME
|
||||
) as ImportSpecifier
|
||||
}
|
||||
|
||||
function getFormatSpecifiers(decl: ImportDeclaration) {
|
||||
return decl.specifiers.filter(
|
||||
spec => 'imported' in spec && FORMAT_METHOD_NAMES.has(spec.imported.name)
|
||||
) as ImportSpecifier[]
|
||||
}
|
||||
|
||||
export function collectFormatCalls(ast: Ast) {
|
||||
const importDecls = getLibImportDeclarations(ast)
|
||||
|
||||
if (importDecls.length === 0) return []
|
||||
|
||||
const imports = new Set(
|
||||
importDecls.flatMap(decl =>
|
||||
getFormatSpecifiers(decl).map(n => n.local.name)
|
||||
)
|
||||
)
|
||||
|
||||
if (imports.size === 0) return []
|
||||
|
||||
const calls: CallExpression[] = []
|
||||
function enter(node: Node) {
|
||||
if (isFormatCall(node, imports)) {
|
||||
calls.push(node as CallExpression)
|
||||
this.skip()
|
||||
}
|
||||
}
|
||||
walk(ast.instance as any, { enter })
|
||||
walk(ast.html as any, { enter })
|
||||
|
||||
return calls
|
||||
}
|
||||
|
||||
export function collectMessageDefinitions(ast: Ast) {
|
||||
const definitions: ObjectExpression[] = []
|
||||
const defineImportDecl = getLibImportDeclarations(ast).find(
|
||||
getDefineMessagesSpecifier
|
||||
)
|
||||
|
||||
if (defineImportDecl == null) return []
|
||||
|
||||
const defineMethodName = getDefineMessagesSpecifier(defineImportDecl).local
|
||||
.name
|
||||
|
||||
walk(ast.instance as any, {
|
||||
enter(node: Node) {
|
||||
if (isMessagesDefinitionCall(node, defineMethodName) === false) return
|
||||
const [arg] = (node as CallExpression).arguments
|
||||
if (arg.type === 'ObjectExpression') {
|
||||
definitions.push(arg)
|
||||
this.skip()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return definitions.flatMap(definitionDict =>
|
||||
definitionDict.properties.map(
|
||||
propNode => propNode.value as ObjectExpression
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function collectMessages(markup: string): Message[] {
|
||||
const ast = parse(markup)
|
||||
const calls = collectFormatCalls(ast)
|
||||
const definitions = collectMessageDefinitions(ast)
|
||||
return [
|
||||
...definitions.map(definition => getObjFromExpression(definition)),
|
||||
...calls.map(call => {
|
||||
const [pathNode, options] = call.arguments
|
||||
if (pathNode.type === 'ObjectExpression') {
|
||||
return getObjFromExpression(pathNode)
|
||||
}
|
||||
|
||||
const node = pathNode as Literal
|
||||
const id = node.value as string
|
||||
|
||||
if (options && options.type === 'ObjectExpression') {
|
||||
const messageObj = getObjFromExpression(options)
|
||||
messageObj.meta.id = id
|
||||
return messageObj
|
||||
}
|
||||
|
||||
return { node, meta: { id } }
|
||||
}),
|
||||
].filter(Boolean)
|
||||
}
|
||||
|
||||
export function extractMessages(
|
||||
markup: string,
|
||||
{ accumulator = {}, shallow = false, overwrite = false } = {} as any
|
||||
) {
|
||||
collectMessages(markup).forEach(message => {
|
||||
let defaultValue = message.meta.default
|
||||
if (typeof defaultValue === 'undefined') {
|
||||
defaultValue = ''
|
||||
}
|
||||
|
||||
if (shallow) {
|
||||
if (overwrite === false && message.meta.id in accumulator) {
|
||||
return
|
||||
}
|
||||
accumulator[message.meta.id] = defaultValue
|
||||
} else {
|
||||
if (
|
||||
overwrite === false &&
|
||||
typeof delve(accumulator, message.meta.id) !== 'undefined'
|
||||
) {
|
||||
return
|
||||
}
|
||||
deepSet(accumulator, message.meta.id, defaultValue)
|
||||
}
|
||||
})
|
||||
return accumulator
|
||||
}
|
17
src/cli/includes/deepSet.ts
Normal file
17
src/cli/includes/deepSet.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
const isNumberString = (n: string) => !Number.isNaN(parseInt(n))
|
||||
|
||||
export function deepSet(obj: any, path: string, value: any) {
|
||||
const parts = path.replace(/\[(\w+)\]/gi, '.$1').split('.')
|
||||
return parts.reduce((ref, part, i) => {
|
||||
if (part in ref) return (ref = ref[part])
|
||||
|
||||
if (i < parts.length - 1) {
|
||||
if (isNumberString(parts[i + 1])) {
|
||||
return (ref = ref[part] = [])
|
||||
}
|
||||
return (ref = ref[part] = {})
|
||||
}
|
||||
|
||||
return (ref[part] = value)
|
||||
}, obj)
|
||||
}
|
20
src/cli/includes/getObjFromExpression.ts
Normal file
20
src/cli/includes/getObjFromExpression.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { ObjectExpression, Property, Identifier } from 'estree'
|
||||
|
||||
import { Message } from '../types'
|
||||
|
||||
export function getObjFromExpression(exprNode: ObjectExpression) {
|
||||
return exprNode.properties.reduce<Message>(
|
||||
(acc, prop: Property) => {
|
||||
// we only want primitives
|
||||
if (
|
||||
prop.value.type === 'Literal' &&
|
||||
prop.value.value !== Object(prop.value.value)
|
||||
) {
|
||||
const key = (prop.key as Identifier).name as string
|
||||
acc.meta[key] = prop.value.value
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{ node: exprNode, meta: {} }
|
||||
)
|
||||
}
|
73
src/cli/index.ts
Normal file
73
src/cli/index.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import fs from 'fs'
|
||||
import { dirname, resolve } from 'path'
|
||||
|
||||
import program from 'commander'
|
||||
import glob from 'tiny-glob'
|
||||
import { preprocess } from 'svelte/compiler'
|
||||
|
||||
import { extractMessages } from './extract'
|
||||
|
||||
const { readFile, writeFile, mkdir, access } = fs.promises
|
||||
|
||||
const fileExists = (path: string) =>
|
||||
access(path)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
program
|
||||
.command('extract <glob> [output]')
|
||||
.description('extract all message definitions from files to a json')
|
||||
.option(
|
||||
'-s, --shallow',
|
||||
'extract to a shallow dictionary (ids with dots interpreted as strings, not paths)',
|
||||
false
|
||||
)
|
||||
.option(
|
||||
'--overwrite',
|
||||
'overwrite the content of the output file instead of just appending new properties',
|
||||
false
|
||||
)
|
||||
.option(
|
||||
'-c, --config <dir>',
|
||||
'path to the "svelte.config.js" file',
|
||||
process.cwd()
|
||||
)
|
||||
.action(async (globStr, output, { shallow, overwrite, config }) => {
|
||||
const filesToExtract = (await glob(globStr)).filter(file =>
|
||||
file.match(/\.html|svelte$/i)
|
||||
)
|
||||
const svelteConfig = await import(
|
||||
resolve(config, 'svelte.config.js')
|
||||
).catch(() => null)
|
||||
|
||||
let accumulator = {}
|
||||
if (output != null && overwrite === false && (await fileExists(output))) {
|
||||
accumulator = await readFile(output)
|
||||
.then(file => JSON.parse(file.toString()))
|
||||
.catch((e: Error) => {
|
||||
console.warn(e)
|
||||
accumulator = {}
|
||||
})
|
||||
}
|
||||
|
||||
for await (const filePath of filesToExtract) {
|
||||
const buffer = await readFile(filePath)
|
||||
let content = buffer.toString()
|
||||
|
||||
if (svelteConfig && svelteConfig.preprocess) {
|
||||
const processed = await preprocess(content, svelteConfig.preprocess, {
|
||||
filename: filePath,
|
||||
})
|
||||
content = processed.code
|
||||
}
|
||||
extractMessages(content, { accumulator, shallow })
|
||||
}
|
||||
|
||||
const jsonDictionary = JSON.stringify(accumulator, null, ' ')
|
||||
if (output == null) return console.log(jsonDictionary)
|
||||
|
||||
await mkdir(dirname(output), { recursive: true })
|
||||
await writeFile(output, jsonDictionary)
|
||||
})
|
||||
|
||||
program.parse(process.argv)
|
10
src/cli/types/index.ts
Normal file
10
src/cli/types/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Node } from 'estree'
|
||||
|
||||
export interface Message {
|
||||
node: Node
|
||||
meta: {
|
||||
id?: string
|
||||
default?: string
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
87
src/client/configs.ts
Normal file
87
src/client/configs.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { getClientLocale } from './includes/utils'
|
||||
import { ConfigureOptions } from './types'
|
||||
import { $locale } from './stores/locale'
|
||||
|
||||
interface Formats {
|
||||
number: Record<string, any>
|
||||
date: Record<string, any>
|
||||
time: Record<string, any>
|
||||
}
|
||||
|
||||
interface Options {
|
||||
fallbackLocale: string
|
||||
initialLocale: string
|
||||
formats: Formats
|
||||
loadingDelay: number
|
||||
warnOnMissingMessages: boolean
|
||||
}
|
||||
|
||||
export const defaultFormats: Formats = {
|
||||
number: {
|
||||
scientific: { notation: 'scientific' },
|
||||
engineering: { notation: 'engineering' },
|
||||
compactLong: { notation: 'compact', compactDisplay: 'long' },
|
||||
compactShort: { notation: 'compact', compactDisplay: 'short' },
|
||||
},
|
||||
date: {
|
||||
short: { month: 'numeric', day: 'numeric', year: '2-digit' },
|
||||
medium: { month: 'short', day: 'numeric', year: 'numeric' },
|
||||
long: { month: 'long', day: 'numeric', year: 'numeric' },
|
||||
full: { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' },
|
||||
},
|
||||
time: {
|
||||
short: { hour: 'numeric', minute: 'numeric' },
|
||||
medium: { hour: 'numeric', minute: 'numeric', second: 'numeric' },
|
||||
long: {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
timeZoneName: 'short',
|
||||
},
|
||||
full: {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
timeZoneName: 'short',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const defaultOptions: Options = {
|
||||
fallbackLocale: null,
|
||||
initialLocale: null,
|
||||
loadingDelay: 200,
|
||||
formats: defaultFormats,
|
||||
warnOnMissingMessages: true,
|
||||
}
|
||||
|
||||
const options: Options = defaultOptions
|
||||
|
||||
export function getOptions() {
|
||||
return options
|
||||
}
|
||||
|
||||
export function init(opts: ConfigureOptions) {
|
||||
const { formats, ...rest } = opts
|
||||
const initialLocale = opts.initialLocale
|
||||
? typeof opts.initialLocale === 'string'
|
||||
? opts.initialLocale
|
||||
: getClientLocale(opts.initialLocale) || opts.fallbackLocale
|
||||
: opts.fallbackLocale
|
||||
|
||||
Object.assign(options, rest, { initialLocale })
|
||||
|
||||
if (formats) {
|
||||
if ('number' in formats) {
|
||||
Object.assign(options.formats.number, formats.number)
|
||||
}
|
||||
if ('date' in formats) {
|
||||
Object.assign(options.formats.date, formats.date)
|
||||
}
|
||||
if ('time' in formats) {
|
||||
Object.assign(options.formats.time, formats.time)
|
||||
}
|
||||
}
|
||||
|
||||
return $locale.set(initialLocale)
|
||||
}
|
75
src/client/includes/formatters.ts
Normal file
75
src/client/includes/formatters.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import IntlMessageFormat from 'intl-messageformat'
|
||||
import memoize from 'fast-memoize'
|
||||
|
||||
import { MemoizedIntlFormatter } from '../types'
|
||||
import { getCurrentLocale } from '../stores/locale'
|
||||
import { getOptions } from '../configs'
|
||||
|
||||
const getIntlFormatterOptions = (
|
||||
type: 'time' | 'number' | 'date',
|
||||
name: string
|
||||
): any => {
|
||||
const formats = getOptions().formats
|
||||
if (type in formats && name in formats[type]) {
|
||||
return formats[type][name]
|
||||
}
|
||||
|
||||
throw new Error(`[svelte-i18n] Unknown "${name}" ${type} format.`)
|
||||
}
|
||||
|
||||
export const getNumberFormatter: MemoizedIntlFormatter<
|
||||
Intl.NumberFormat,
|
||||
Intl.NumberFormatOptions
|
||||
> = memoize(({ locale, format, ...options } = {}) => {
|
||||
locale = locale || getCurrentLocale()
|
||||
if (locale == null) {
|
||||
throw new Error('[svelte-i18n] A "locale" must be set to format numbers')
|
||||
}
|
||||
|
||||
if (format) {
|
||||
options = getIntlFormatterOptions('number', format)
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat(locale, options)
|
||||
})
|
||||
|
||||
export const getDateFormatter: MemoizedIntlFormatter<
|
||||
Intl.DateTimeFormat,
|
||||
Intl.DateTimeFormatOptions
|
||||
> = memoize(({ locale, format, ...options } = {}) => {
|
||||
locale = locale || getCurrentLocale()
|
||||
if (locale == null) {
|
||||
throw new Error('[svelte-i18n] A "locale" must be set to format dates')
|
||||
}
|
||||
|
||||
if (format) options = getIntlFormatterOptions('date', format)
|
||||
else if (Object.keys(options).length === 0) {
|
||||
options = getIntlFormatterOptions('date', 'short')
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(locale, options)
|
||||
})
|
||||
|
||||
export const getTimeFormatter: MemoizedIntlFormatter<
|
||||
Intl.DateTimeFormat,
|
||||
Intl.DateTimeFormatOptions
|
||||
> = memoize(({ locale, format, ...options } = {}) => {
|
||||
locale = locale || getCurrentLocale()
|
||||
if (locale == null) {
|
||||
throw new Error(
|
||||
'[svelte-i18n] A "locale" must be set to format time values'
|
||||
)
|
||||
}
|
||||
|
||||
if (format) options = getIntlFormatterOptions('time', format)
|
||||
else if (Object.keys(options).length === 0) {
|
||||
options = getIntlFormatterOptions('time', 'short')
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(locale, options)
|
||||
})
|
||||
|
||||
export const getMessageFormatter = memoize(
|
||||
(message: string, locale: string) =>
|
||||
new IntlMessageFormat(message, locale, getOptions().formats)
|
||||
)
|
96
src/client/includes/loaderQueue.ts
Normal file
96
src/client/includes/loaderQueue.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
import { MessagesLoader } from '../types'
|
||||
import {
|
||||
hasLocaleDictionary,
|
||||
$dictionary,
|
||||
addMessages,
|
||||
} from '../stores/dictionary'
|
||||
import { getRelatedLocalesOf } from '../stores/locale'
|
||||
import { $isLoading } from '../stores/loading'
|
||||
import { getOptions } from '../configs'
|
||||
|
||||
type Queue = Set<MessagesLoader>
|
||||
const loaderQueue: Record<string, Queue> = {}
|
||||
|
||||
export function resetQueues() {
|
||||
Object.keys(loaderQueue).forEach(key => {
|
||||
delete loaderQueue[key]
|
||||
})
|
||||
}
|
||||
|
||||
function createLocaleQueue(locale: string) {
|
||||
loaderQueue[locale] = new Set()
|
||||
}
|
||||
|
||||
function removeLocaleFromQueue(locale: string) {
|
||||
delete loaderQueue[locale]
|
||||
}
|
||||
|
||||
function getLocaleQueue(locale: string) {
|
||||
return loaderQueue[locale]
|
||||
}
|
||||
|
||||
function getLocalesQueues(locale: string) {
|
||||
return getRelatedLocalesOf(locale)
|
||||
.reverse()
|
||||
.map<[string, MessagesLoader[]]>(localeItem => {
|
||||
const queue = getLocaleQueue(localeItem)
|
||||
return [localeItem, queue ? [...queue] : []]
|
||||
})
|
||||
.filter(([, queue]) => queue.length > 0)
|
||||
}
|
||||
|
||||
export function hasLocaleQueue(locale: string) {
|
||||
return getRelatedLocalesOf(locale)
|
||||
.reverse()
|
||||
.some(getLocaleQueue)
|
||||
}
|
||||
|
||||
const activeLocaleFlushes: { [key: string]: Promise<void> } = {}
|
||||
export function flush(locale: string) {
|
||||
if (!hasLocaleQueue(locale)) return
|
||||
if (locale in activeLocaleFlushes) return activeLocaleFlushes[locale]
|
||||
|
||||
// get queue of XX-YY and XX locales
|
||||
const queues = getLocalesQueues(locale)
|
||||
// istanbul ignore if
|
||||
if (queues.length === 0) return
|
||||
|
||||
const loadingDelay = setTimeout(
|
||||
() => $isLoading.set(true),
|
||||
getOptions().loadingDelay
|
||||
)
|
||||
|
||||
// TODO what happens if some loader fails
|
||||
activeLocaleFlushes[locale] = Promise.all(
|
||||
queues.map(([locale, queue]) => {
|
||||
return Promise.all(queue.map(loader => loader())).then(partials => {
|
||||
removeLocaleFromQueue(locale)
|
||||
partials = partials.map(partial => partial.default || partial)
|
||||
addMessages(locale, ...partials)
|
||||
})
|
||||
})
|
||||
).then(() => {
|
||||
clearTimeout(loadingDelay)
|
||||
$isLoading.set(false)
|
||||
delete activeLocaleFlushes[locale]
|
||||
})
|
||||
|
||||
return activeLocaleFlushes[locale]
|
||||
}
|
||||
|
||||
export function registerLocaleLoader(locale: string, loader: MessagesLoader) {
|
||||
if (!getLocaleQueue(locale)) createLocaleQueue(locale)
|
||||
|
||||
const queue = getLocaleQueue(locale)
|
||||
// istanbul ignore if
|
||||
if (getLocaleQueue(locale).has(loader)) return
|
||||
|
||||
if (!hasLocaleDictionary(locale)) {
|
||||
$dictionary.update(d => {
|
||||
d[locale] = {}
|
||||
return d
|
||||
})
|
||||
}
|
||||
|
||||
queue.add(loader)
|
||||
}
|
23
src/client/includes/lookup.ts
Normal file
23
src/client/includes/lookup.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { getMessageFromDictionary } from '../stores/dictionary'
|
||||
import { getFallbackOf } from '../stores/locale'
|
||||
|
||||
export const lookupCache: Record<string, Record<string, string>> = {}
|
||||
|
||||
const addToCache = (path: string, locale: string, message: string) => {
|
||||
if (!message) return message
|
||||
if (!(locale in lookupCache)) lookupCache[locale] = {}
|
||||
if (!(path in lookupCache[locale])) lookupCache[locale][path] = message
|
||||
return message
|
||||
}
|
||||
|
||||
export const lookupMessage = (path: string, locale: string): string => {
|
||||
if (locale == null) return null
|
||||
if (locale in lookupCache && path in lookupCache[locale]) {
|
||||
return lookupCache[locale][path]
|
||||
}
|
||||
|
||||
const message = getMessageFromDictionary(locale, path)
|
||||
if (message) return message
|
||||
|
||||
return addToCache(path, locale, lookupMessage(path, getFallbackOf(locale)))
|
||||
}
|
75
src/client/includes/utils.ts
Normal file
75
src/client/includes/utils.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { GetClientLocaleOptions } from '../types'
|
||||
|
||||
export function capital(str: string) {
|
||||
return str.replace(/(^|\s)\S/, l => l.toLocaleUpperCase())
|
||||
}
|
||||
|
||||
export function title(str: string) {
|
||||
return str.replace(/(^|\s)\S/g, l => l.toLocaleUpperCase())
|
||||
}
|
||||
|
||||
export function upper(str: string) {
|
||||
return str.toLocaleUpperCase()
|
||||
}
|
||||
|
||||
export function lower(str: string) {
|
||||
return str.toLocaleLowerCase()
|
||||
}
|
||||
|
||||
const getFromQueryString = (queryString: string, key: string) => {
|
||||
const keyVal = queryString.split('&').find(i => i.indexOf(`${key}=`) === 0)
|
||||
|
||||
if (keyVal) {
|
||||
return keyVal.split('=').pop()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const getFirstMatch = (base: string, pattern: RegExp) => {
|
||||
const match = pattern.exec(base)
|
||||
// istanbul ignore if
|
||||
if (!match) return null
|
||||
// istanbul ignore else
|
||||
return match[1] || null
|
||||
}
|
||||
|
||||
export const getClientLocale = ({
|
||||
navigator,
|
||||
hash,
|
||||
search,
|
||||
pathname,
|
||||
hostname,
|
||||
}: GetClientLocaleOptions) => {
|
||||
let locale
|
||||
|
||||
// istanbul ignore next
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
if (hostname) {
|
||||
locale = getFirstMatch(window.location.hostname, hostname)
|
||||
if (locale) return locale
|
||||
}
|
||||
|
||||
if (pathname) {
|
||||
locale = getFirstMatch(window.location.pathname, pathname)
|
||||
if (locale) return locale
|
||||
}
|
||||
|
||||
if (navigator) {
|
||||
// istanbul ignore else
|
||||
locale = window.navigator.language || window.navigator.languages[0]
|
||||
if (locale) return locale
|
||||
}
|
||||
|
||||
if (search) {
|
||||
locale = getFromQueryString(window.location.search.substr(1), search)
|
||||
if (locale) return locale
|
||||
}
|
||||
|
||||
if (hash) {
|
||||
locale = getFromQueryString(window.location.hash.substr(1), hash)
|
||||
if (locale) return locale
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
26
src/client/index.ts
Normal file
26
src/client/index.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { MessageObject } from './types'
|
||||
import { getCurrentLocale } from './stores/locale'
|
||||
import { getOptions } from './configs'
|
||||
import { flush } from './includes/loaderQueue'
|
||||
|
||||
// defineMessages allow us to define and extract dynamic message ids
|
||||
export function defineMessages(i: Record<string, MessageObject>) {
|
||||
return i
|
||||
}
|
||||
|
||||
export function waitLocale(locale?: string) {
|
||||
return flush(locale || getCurrentLocale() || getOptions().initialLocale)
|
||||
}
|
||||
|
||||
export { init } from './configs'
|
||||
export { $locale as locale } from './stores/locale'
|
||||
export {
|
||||
$dictionary as dictionary,
|
||||
$locales as locales,
|
||||
addMessages,
|
||||
} from './stores/dictionary'
|
||||
export { $isLoading as isLoading } from './stores/loading'
|
||||
export { $format as format, $format as _, $format as t } from './stores/format'
|
||||
|
||||
// utilities
|
||||
export { registerLocaleLoader as register } from './includes/loaderQueue'
|
56
src/client/stores/dictionary.ts
Normal file
56
src/client/stores/dictionary.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import delve from 'dlv'
|
||||
import merge from 'deepmerge'
|
||||
import { writable, derived } from 'svelte/store'
|
||||
|
||||
import { Dictionary } from '../types/index'
|
||||
|
||||
import { getFallbackOf } from './locale'
|
||||
|
||||
let dictionary: Dictionary
|
||||
const $dictionary = writable<Dictionary>({})
|
||||
|
||||
export function getLocaleDictionary(locale: string) {
|
||||
return (dictionary[locale] as Dictionary) || null
|
||||
}
|
||||
|
||||
export function getDictionary() {
|
||||
return dictionary
|
||||
}
|
||||
|
||||
export function hasLocaleDictionary(locale: string) {
|
||||
return locale in dictionary
|
||||
}
|
||||
|
||||
export function getMessageFromDictionary(locale: string, id: string) {
|
||||
if (hasLocaleDictionary(locale)) {
|
||||
const localeDictionary = getLocaleDictionary(locale)
|
||||
if (id in localeDictionary) {
|
||||
return localeDictionary[id]
|
||||
}
|
||||
const message = delve(localeDictionary, id)
|
||||
if (message) return message
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getClosestAvailableLocale(locale: string): string | null {
|
||||
if (locale == null || hasLocaleDictionary(locale)) return locale
|
||||
return getClosestAvailableLocale(getFallbackOf(locale))
|
||||
}
|
||||
|
||||
export function addMessages(locale: string, ...partials: Dictionary[]) {
|
||||
$dictionary.update(d => {
|
||||
dictionary[locale] = merge.all<Dictionary>(
|
||||
[getLocaleDictionary(locale) || {}].concat(partials)
|
||||
)
|
||||
return d
|
||||
})
|
||||
}
|
||||
|
||||
const $locales = derived([$dictionary], ([$dictionary]) =>
|
||||
Object.keys($dictionary)
|
||||
)
|
||||
|
||||
$dictionary.subscribe(newDictionary => (dictionary = newDictionary))
|
||||
|
||||
export { $dictionary, $locales }
|
65
src/client/stores/format.ts
Normal file
65
src/client/stores/format.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { derived } from 'svelte/store'
|
||||
|
||||
import { Formatter, MessageObject } from '../types'
|
||||
import { lookupMessage } from '../includes/lookup'
|
||||
import { hasLocaleQueue } from '../includes/loaderQueue'
|
||||
import { capital, upper, lower, title } from '../includes/utils'
|
||||
import {
|
||||
getMessageFormatter,
|
||||
getTimeFormatter,
|
||||
getDateFormatter,
|
||||
getNumberFormatter,
|
||||
} from '../includes/formatters'
|
||||
import { getOptions } from '../configs'
|
||||
|
||||
import { $dictionary } from './dictionary'
|
||||
import { getCurrentLocale, getRelatedLocalesOf, $locale } from './locale'
|
||||
|
||||
const formatMessage: Formatter = (id, options = {}) => {
|
||||
if (typeof id === 'object') {
|
||||
options = id as MessageObject
|
||||
id = options.id
|
||||
}
|
||||
|
||||
const { values, locale = getCurrentLocale(), default: defaultValue } = options
|
||||
|
||||
if (locale == null) {
|
||||
throw new Error(
|
||||
'[svelte-i18n] Cannot format a message without first setting the initial locale.'
|
||||
)
|
||||
}
|
||||
|
||||
const message = lookupMessage(id, locale)
|
||||
|
||||
if (!message) {
|
||||
if (getOptions().warnOnMissingMessages) {
|
||||
// istanbul ignore next
|
||||
console.warn(
|
||||
`[svelte-i18n] The message "${id}" was not found in "${getRelatedLocalesOf(
|
||||
locale
|
||||
).join('", "')}".${
|
||||
hasLocaleQueue(getCurrentLocale())
|
||||
? `\n\nNote: there are at least one loader still registered to this locale that wasn't executed.`
|
||||
: ''
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
||||
return defaultValue || id
|
||||
}
|
||||
|
||||
if (!values) return message
|
||||
return getMessageFormatter(message, locale).format(values)
|
||||
}
|
||||
|
||||
formatMessage.time = (t, options) => getTimeFormatter(options).format(t)
|
||||
formatMessage.date = (d, options) => getDateFormatter(options).format(d)
|
||||
formatMessage.number = (n, options) => getNumberFormatter(options).format(n)
|
||||
formatMessage.capital = (id, options) => capital(formatMessage(id, options))
|
||||
formatMessage.title = (id, options) => title(formatMessage(id, options))
|
||||
formatMessage.upper = (id, options) => upper(formatMessage(id, options))
|
||||
formatMessage.lower = (id, options) => lower(formatMessage(id, options))
|
||||
|
||||
const $format = derived([$locale, $dictionary], () => formatMessage)
|
||||
|
||||
export { $format }
|
3
src/client/stores/loading.ts
Normal file
3
src/client/stores/loading.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { writable } from 'svelte/store'
|
||||
|
||||
export const $isLoading = writable(false)
|
70
src/client/stores/locale.ts
Normal file
70
src/client/stores/locale.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { writable } from 'svelte/store'
|
||||
|
||||
import { flush, hasLocaleQueue } from '../includes/loaderQueue'
|
||||
import { getOptions } from '../configs'
|
||||
|
||||
import { getClosestAvailableLocale } from './dictionary'
|
||||
|
||||
let current: string
|
||||
const $locale = writable(null)
|
||||
|
||||
export function isFallbackLocaleOf(localeA: string, localeB: string) {
|
||||
return localeB.indexOf(localeA) === 0 && localeA !== localeB
|
||||
}
|
||||
|
||||
export function isRelatedLocale(localeA: string, localeB: string) {
|
||||
return (
|
||||
localeA === localeB ||
|
||||
isFallbackLocaleOf(localeA, localeB) ||
|
||||
isFallbackLocaleOf(localeB, localeA)
|
||||
)
|
||||
}
|
||||
|
||||
export function getFallbackOf(locale: string) {
|
||||
const index = locale.lastIndexOf('-')
|
||||
if (index > 0) return locale.slice(0, index)
|
||||
|
||||
const { fallbackLocale } = getOptions()
|
||||
if (fallbackLocale && !isRelatedLocale(locale, fallbackLocale)) {
|
||||
return fallbackLocale
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getRelatedLocalesOf(locale: string): string[] {
|
||||
const locales = locale
|
||||
.split('-')
|
||||
.map((_, i, arr) => arr.slice(0, i + 1).join('-'))
|
||||
|
||||
const { fallbackLocale } = getOptions()
|
||||
if (fallbackLocale && !isRelatedLocale(locale, fallbackLocale)) {
|
||||
return locales.concat(getRelatedLocalesOf(fallbackLocale))
|
||||
}
|
||||
return locales
|
||||
}
|
||||
|
||||
export function getCurrentLocale() {
|
||||
return current
|
||||
}
|
||||
|
||||
$locale.subscribe((newLocale: string) => {
|
||||
current = newLocale
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
document.documentElement.setAttribute('lang', newLocale)
|
||||
}
|
||||
})
|
||||
|
||||
const localeSet = $locale.set
|
||||
$locale.set = (newLocale: string): void | Promise<void> => {
|
||||
if (getClosestAvailableLocale(newLocale) && hasLocaleQueue(newLocale)) {
|
||||
return flush(newLocale).then(() => localeSet(newLocale))
|
||||
}
|
||||
return localeSet(newLocale)
|
||||
}
|
||||
|
||||
// istanbul ignore next
|
||||
$locale.update = (fn: (locale: string) => void | Promise<void>) =>
|
||||
localeSet(fn(current))
|
||||
|
||||
export { $locale }
|
64
src/client/types/index.ts
Normal file
64
src/client/types/index.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { Formats } from 'intl-messageformat'
|
||||
|
||||
export interface Dictionary {
|
||||
[key: string]: string | string[] | Dictionary | Dictionary[]
|
||||
}
|
||||
|
||||
export interface MessageObject {
|
||||
id?: string
|
||||
locale?: string
|
||||
format?: string
|
||||
default?: string
|
||||
values?: Record<string, string | number | Date>
|
||||
}
|
||||
|
||||
interface FormatterFn {
|
||||
(id: string | MessageObject, options?: MessageObject): string
|
||||
}
|
||||
|
||||
type IntlFormatterOptions<T> = T & {
|
||||
format?: string
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export interface MemoizedIntlFormatter<T, U> {
|
||||
(options?: IntlFormatterOptions<U>): T
|
||||
}
|
||||
|
||||
export interface Formatter extends FormatterFn {
|
||||
time: (
|
||||
d: Date | number,
|
||||
options?: IntlFormatterOptions<Intl.DateTimeFormatOptions>
|
||||
) => string
|
||||
date: (
|
||||
d: Date | number,
|
||||
options?: IntlFormatterOptions<Intl.DateTimeFormatOptions>
|
||||
) => string
|
||||
number: (
|
||||
d: number,
|
||||
options?: IntlFormatterOptions<Intl.NumberFormatOptions>
|
||||
) => string
|
||||
capital: FormatterFn
|
||||
title: FormatterFn
|
||||
upper: FormatterFn
|
||||
lower: FormatterFn
|
||||
}
|
||||
|
||||
export interface MessagesLoader {
|
||||
(): Promise<any>
|
||||
}
|
||||
|
||||
export interface GetClientLocaleOptions {
|
||||
navigator?: boolean
|
||||
hash?: string
|
||||
search?: string
|
||||
pathname?: RegExp
|
||||
hostname?: RegExp
|
||||
}
|
||||
|
||||
export interface ConfigureOptions {
|
||||
fallbackLocale: string
|
||||
initialLocale?: string | GetClientLocaleOptions
|
||||
formats?: Partial<Formats>
|
||||
loadingDelay?: number
|
||||
}
|
2
src/client/types/modules.d.ts
vendored
Normal file
2
src/client/types/modules.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
declare module 'dlv'
|
||||
declare module 'nano-memoize'
|
119
src/index.js
119
src/index.js
|
@ -1,119 +0,0 @@
|
|||
import { writable, derived } from 'svelte/store/index.js'
|
||||
import resolvePath from 'object-resolve-path'
|
||||
import IntlMessageFormat from 'intl-messageformat'
|
||||
import memoize from 'micro-memoize'
|
||||
|
||||
import { capital, title, upper, lower, getClientLocale } from './utils.js'
|
||||
|
||||
let currentLocale
|
||||
let currentDictionary
|
||||
|
||||
const customFormats = {
|
||||
number: {
|
||||
scientific: { notation: 'scientific' },
|
||||
engineering: { notation: 'engineering' },
|
||||
compactLong: { notation: 'compact', compactDisplay: 'long' },
|
||||
compactShort: { notation: 'compact', compactDisplay: 'short' },
|
||||
},
|
||||
}
|
||||
|
||||
function addCustomFormats(formats) {
|
||||
if ('number' in formats) Object.assign(customFormats.number, formats.number)
|
||||
if ('date' in formats) Object.assign(customFormats.date, formats.date)
|
||||
if ('time' in formats) Object.assign(customFormats.time, formats.time)
|
||||
}
|
||||
|
||||
function getAvailableLocale(newLocale) {
|
||||
if (currentDictionary[newLocale]) return newLocale
|
||||
|
||||
// istanbul ignore else
|
||||
if (typeof newLocale === 'string') {
|
||||
const fallbackLocale = newLocale.split('-').shift()
|
||||
|
||||
if (currentDictionary[fallbackLocale]) {
|
||||
return fallbackLocale
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const getMessageFormatter = memoize(
|
||||
(message, locale) => new IntlMessageFormat(message, locale, customFormats),
|
||||
)
|
||||
|
||||
const lookupMessage = memoize((path, locale) => {
|
||||
return (
|
||||
currentDictionary[locale][path] ||
|
||||
resolvePath(currentDictionary[locale], path)
|
||||
)
|
||||
})
|
||||
|
||||
function formatString(string, { values, locale = currentLocale } = {}) {
|
||||
return getMessageFormatter(string, locale).format(values)
|
||||
}
|
||||
|
||||
function formatMessage(path, { values, locale = currentLocale } = {}) {
|
||||
const message = lookupMessage(path, locale)
|
||||
|
||||
if (!message) {
|
||||
console.warn(
|
||||
`[svelte-i18n] The message "${path}" was not found in the locale "${locale}".`,
|
||||
)
|
||||
return path
|
||||
}
|
||||
|
||||
if (!values) return message
|
||||
|
||||
return getMessageFormatter(message, locale).format(values)
|
||||
}
|
||||
|
||||
formatMessage.time = (t, { format = 'short' } = {}) =>
|
||||
formatString(`{t,time,${format}}`, { values: { t } })
|
||||
|
||||
formatMessage.date = (d, { format = 'short' } = {}) =>
|
||||
formatString(`{d,date,${format}}`, { values: { d } })
|
||||
|
||||
formatMessage.number = (n, { format } = {}) =>
|
||||
formatString(`{n,number,${format}}`, { values: { n } })
|
||||
|
||||
formatMessage.capital = (path, options) => capital(formatMessage(path, options))
|
||||
|
||||
formatMessage.title = (path, options) => title(formatMessage(path, options))
|
||||
|
||||
formatMessage.upper = (path, options) => upper(formatMessage(path, options))
|
||||
|
||||
formatMessage.lower = (path, options) => lower(formatMessage(path, options))
|
||||
|
||||
const dictionary = writable({})
|
||||
dictionary.subscribe(newDictionary => {
|
||||
currentDictionary = newDictionary
|
||||
})
|
||||
|
||||
const locale = writable({})
|
||||
const localeSet = locale.set
|
||||
locale.set = newLocale => {
|
||||
const availableLocale = getAvailableLocale(newLocale)
|
||||
if (availableLocale) {
|
||||
return localeSet(availableLocale)
|
||||
}
|
||||
|
||||
throw Error(`[svelte-i18n] Locale "${newLocale}" not found.`)
|
||||
}
|
||||
locale.update = fn => localeSet(fn(currentLocale))
|
||||
locale.subscribe(newLocale => {
|
||||
currentLocale = newLocale
|
||||
})
|
||||
|
||||
const format = derived([locale, dictionary], () => formatMessage)
|
||||
|
||||
export {
|
||||
locale,
|
||||
format as _,
|
||||
format,
|
||||
formatString,
|
||||
dictionary,
|
||||
getClientLocale,
|
||||
customFormats,
|
||||
addCustomFormats,
|
||||
}
|
37
src/utils.js
37
src/utils.js
|
@ -1,37 +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 getClientLocale = ({ navigator, hash, search, fallback } = {}) => {
|
||||
let locale
|
||||
|
||||
const getFromURL = (urlPart, key) => {
|
||||
const keyVal = urlPart
|
||||
.substr(1)
|
||||
.split('&')
|
||||
.find(i => i.indexOf(key) === 0)
|
||||
|
||||
if (keyVal) {
|
||||
return keyVal.split('=').pop()
|
||||
}
|
||||
}
|
||||
|
||||
// istanbul ignore else
|
||||
if (typeof window !== 'undefined') {
|
||||
if (navigator) {
|
||||
// istanbul ignore next
|
||||
locale = window.navigator.language || window.navigator.languages[0]
|
||||
}
|
||||
|
||||
if (search) {
|
||||
locale = getFromURL(window.location.search, search)
|
||||
}
|
||||
|
||||
if (hash) {
|
||||
locale = getFromURL(window.location.hash, hash)
|
||||
}
|
||||
}
|
||||
|
||||
return locale || fallback
|
||||
}
|
267
test/cli/extract.test.ts
Normal file
267
test/cli/extract.test.ts
Normal file
|
@ -0,0 +1,267 @@
|
|||
// TODO: better tests. these are way too generic.
|
||||
|
||||
import { parse } from 'svelte/compiler'
|
||||
|
||||
import {
|
||||
collectFormatCalls,
|
||||
collectMessageDefinitions,
|
||||
collectMessages,
|
||||
extractMessages,
|
||||
} from '../../src/cli/extract'
|
||||
|
||||
describe('collecting format calls', () => {
|
||||
test('returns nothing if there are no script tag', () => {
|
||||
const ast = parse(`<div>Hey</div>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('returns nothing if there are no imports', () => {
|
||||
const ast = parse(`<script>
|
||||
import Foo from 'foo';
|
||||
const $_ = () => 0; $_();
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('returns nothing if there are no format imports', () => {
|
||||
const ast = parse(
|
||||
`<script>
|
||||
import { init } from 'svelte-i18n';
|
||||
init({})
|
||||
</script>`
|
||||
)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('collects all format calls in the instance script', () => {
|
||||
const ast = parse(`<script>
|
||||
import { format, _ } from 'svelte-i18n'
|
||||
$format('foo')
|
||||
format('bar')
|
||||
let label = $_({id:'bar'})
|
||||
const a = { b: () => 0}
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(2)
|
||||
expect(calls[0]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
|
||||
})
|
||||
|
||||
test('collects all format calls with renamed imports', () => {
|
||||
const ast = parse(`<script>
|
||||
import { format as _x, _ as intl, t as f } from 'svelte-i18n'
|
||||
$_x('foo')
|
||||
$intl({ id: 'bar' })
|
||||
$f({ id: 'bar' })
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(3)
|
||||
expect(calls[0]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[2]).toMatchObject({ type: 'CallExpression' })
|
||||
})
|
||||
|
||||
test('collects all string format utility calls', () => {
|
||||
const ast = parse(`<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
$_.title('foo')
|
||||
$_.capitalize({ id: 'bar' })
|
||||
$_.lower({ id: 'bar' })
|
||||
$_.upper({ id: 'bar' })
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(4)
|
||||
expect(calls[0]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[2]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[3]).toMatchObject({ type: 'CallExpression' })
|
||||
})
|
||||
|
||||
test('ignores date, time and number calls', () => {
|
||||
const ast = parse(`<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
$_.number(1000)
|
||||
$_.date(new Date())
|
||||
$_.time(new Date())
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('collecting message definitions', () => {
|
||||
test('returns nothing if there are no imports from the library', () => {
|
||||
const ast = parse(
|
||||
`<script>
|
||||
import foo from 'foo';
|
||||
import { dictionary } from 'svelte-i18n';
|
||||
</script>`
|
||||
)
|
||||
expect(collectMessageDefinitions(ast)).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('gets all message definition objects', () => {
|
||||
const ast = parse(`<script>
|
||||
import { defineMessages } from 'svelte-i18n';
|
||||
defineMessages({ foo: { id: 'foo' }, bar: { id: 'bar' } })
|
||||
defineMessages({ baz: { id: 'baz' }, quix: { id: 'qux' } })
|
||||
</script>`)
|
||||
const definitions = collectMessageDefinitions(ast)
|
||||
expect(definitions).toHaveLength(4)
|
||||
expect(definitions[0]).toMatchObject({ type: 'ObjectExpression' })
|
||||
expect(definitions[1]).toMatchObject({ type: 'ObjectExpression' })
|
||||
expect(definitions[2]).toMatchObject({ type: 'ObjectExpression' })
|
||||
expect(definitions[3]).toMatchObject({ type: 'ObjectExpression' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('collecting messages', () => {
|
||||
test('collects all messages in both instance and html ASTs', () => {
|
||||
const markup = `
|
||||
<script>
|
||||
import { _, defineMessages } from 'svelte-i18n';
|
||||
|
||||
console.log($_({ id: 'foo' }))
|
||||
console.log($_.title({ id: 'page.title' }))
|
||||
|
||||
const messages = defineMessages({
|
||||
enabled: { id: 'enabled' , default: 'Enabled' },
|
||||
disabled: { id: 'disabled', default: 'Disabled' }
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>{$_('msg_1')}</div>
|
||||
<div>{$_({id: 'msg_2'})}</div>
|
||||
<div>{$_('msg_3', { default: 'Message'})}</div>`
|
||||
|
||||
const messages = collectMessages(markup)
|
||||
|
||||
expect(messages).toHaveLength(7)
|
||||
expect(messages).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ meta: { id: 'foo' } }),
|
||||
expect.objectContaining({ meta: { id: 'msg_1' } }),
|
||||
expect.objectContaining({ meta: { id: 'msg_2' } }),
|
||||
expect.objectContaining({ meta: { id: 'msg_3', default: 'Message' } }),
|
||||
expect.objectContaining({ meta: { id: 'page.title' } }),
|
||||
expect.objectContaining({
|
||||
meta: { id: 'disabled', default: 'Disabled' },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
meta: { id: 'enabled', default: 'Enabled' },
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('messages extraction', () => {
|
||||
test('returns a object built based on all found message paths', () => {
|
||||
const markup = `<script>
|
||||
import { _ } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<h1>{$_.title('title')}</h1>
|
||||
<h2>{$_({ id: 'subtitle'})}</h2>
|
||||
`
|
||||
const dict = extractMessages(markup)
|
||||
expect(dict).toMatchObject({ title: '', subtitle: '' })
|
||||
})
|
||||
|
||||
test('creates deep nested properties', () => {
|
||||
const markup = `
|
||||
<script>import { _ } from 'svelte-i18n';</script>
|
||||
|
||||
<h1>{$_.title('home.page.title')}</h1>
|
||||
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
|
||||
<ul>
|
||||
<li>{$_('list.0')}</li>
|
||||
<li>{$_('list.1')}</li>
|
||||
<li>{$_('list.2')}</li>
|
||||
</ul>
|
||||
`
|
||||
const dict = extractMessages(markup)
|
||||
expect(dict).toMatchObject({
|
||||
home: { page: { title: '', subtitle: '' } },
|
||||
list: ['', '', ''],
|
||||
})
|
||||
})
|
||||
|
||||
test('creates a shallow dictionary', () => {
|
||||
const markup = `
|
||||
<script>import { _ } from 'svelte-i18n';</script>
|
||||
|
||||
<h1>{$_.title('home.page.title')}</h1>
|
||||
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
|
||||
<ul>
|
||||
<li>{$_('list.0')}</li>
|
||||
<li>{$_('list.1')}</li>
|
||||
<li>{$_('list.2')}</li>
|
||||
</ul>
|
||||
`
|
||||
const dict = extractMessages(markup, { shallow: true })
|
||||
expect(dict).toMatchObject({
|
||||
'home.page.title': '',
|
||||
'home.page.subtitle': '',
|
||||
'list.0': '',
|
||||
'list.1': '',
|
||||
'list.2': '',
|
||||
})
|
||||
})
|
||||
|
||||
test('allow to pass a initial dictionary and only append non-existing props', () => {
|
||||
const markup = `
|
||||
<script>import { _ } from 'svelte-i18n';</script>
|
||||
|
||||
<h1>{$_.title('home.page.title')}</h1>
|
||||
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
|
||||
<ul>
|
||||
<li>{$_('list.0')}</li>
|
||||
<li>{$_('list.1')}</li>
|
||||
<li>{$_('list.2')}</li>
|
||||
</ul>
|
||||
`
|
||||
const dict = extractMessages(markup, {
|
||||
overwrite: false,
|
||||
accumulator: {
|
||||
home: {
|
||||
page: {
|
||||
title: 'Page title',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(dict).toMatchObject({
|
||||
home: {
|
||||
page: {
|
||||
title: 'Page title',
|
||||
subtitle: '',
|
||||
},
|
||||
},
|
||||
list: ['', '', ''],
|
||||
})
|
||||
})
|
||||
|
||||
test('allow to pass a initial dictionary and only append shallow non-existing props', () => {
|
||||
const markup = `
|
||||
<script>import { _ } from 'svelte-i18n';</script>
|
||||
|
||||
<h1>{$_.title('home.page.title')}</h1>
|
||||
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
|
||||
`
|
||||
const dict = extractMessages(markup, {
|
||||
overwrite: false,
|
||||
shallow: true,
|
||||
accumulator: {
|
||||
'home.page.title': 'Page title',
|
||||
},
|
||||
})
|
||||
expect(dict).toMatchObject({
|
||||
'home.page.title': 'Page title',
|
||||
'home.page.subtitle': '',
|
||||
})
|
||||
})
|
||||
})
|
65
test/client/configs.test.ts
Normal file
65
test/client/configs.test.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { get } from 'svelte/store'
|
||||
|
||||
import {
|
||||
init,
|
||||
getOptions,
|
||||
defaultOptions,
|
||||
defaultFormats,
|
||||
} from '../../src/client/configs'
|
||||
import { $locale } from '../../src/client/stores/locale'
|
||||
|
||||
beforeEach(() => {
|
||||
init(defaultOptions)
|
||||
})
|
||||
|
||||
test('inits the fallback locale', () => {
|
||||
expect(getOptions().fallbackLocale).toBe(null)
|
||||
init({
|
||||
fallbackLocale: 'en',
|
||||
})
|
||||
expect(getOptions().fallbackLocale).toBe('en')
|
||||
})
|
||||
|
||||
test('inits the initial locale by string', () => {
|
||||
init({
|
||||
fallbackLocale: 'pt',
|
||||
initialLocale: 'en',
|
||||
})
|
||||
expect(getOptions().initialLocale).toBe('en')
|
||||
expect(get($locale)).toBe('en')
|
||||
})
|
||||
|
||||
test('inits the initial locale by client heuristics', () => {
|
||||
delete window.location
|
||||
window.location = {
|
||||
search: '?lang=en-US&foo',
|
||||
pathname: '/',
|
||||
hostname: 'example.com',
|
||||
hash: '',
|
||||
} as any
|
||||
|
||||
init({
|
||||
fallbackLocale: 'pt',
|
||||
initialLocale: {
|
||||
search: 'lang',
|
||||
},
|
||||
})
|
||||
expect(getOptions().initialLocale).toBe('en-US')
|
||||
expect(get($locale)).toBe('en-US')
|
||||
})
|
||||
|
||||
test('adds custom formats for time, date and number values', () => {
|
||||
const customFormats = require('../fixtures/formats.json')
|
||||
|
||||
init({
|
||||
fallbackLocale: 'en',
|
||||
formats: customFormats,
|
||||
})
|
||||
expect(getOptions().formats).toMatchObject(defaultFormats)
|
||||
expect(getOptions().formats).toMatchObject(customFormats)
|
||||
})
|
||||
|
||||
test('sets the minimum delay to set the loading store value', () => {
|
||||
init({ fallbackLocale: 'en', loadingDelay: 300 })
|
||||
expect(getOptions().loadingDelay).toBe(300)
|
||||
})
|
173
test/client/includes/formatters.test.ts
Normal file
173
test/client/includes/formatters.test.ts
Normal file
|
@ -0,0 +1,173 @@
|
|||
import {
|
||||
getNumberFormatter,
|
||||
getDateFormatter,
|
||||
getTimeFormatter,
|
||||
getMessageFormatter,
|
||||
} from '../../../src/client/includes/formatters'
|
||||
import { init } from '../../../src/client/configs'
|
||||
|
||||
beforeEach(() => {
|
||||
init({ fallbackLocale: undefined })
|
||||
})
|
||||
|
||||
describe('number formatter', () => {
|
||||
const number = 123123
|
||||
|
||||
test('throws if no locale is set', () => {
|
||||
expect(() => getNumberFormatter().format(number)).toThrow(
|
||||
'[svelte-i18n] A "locale" must be set to format numbers'
|
||||
)
|
||||
})
|
||||
|
||||
test('formats a date according to the current locale', () => {
|
||||
init({ fallbackLocale: 'en' })
|
||||
expect(getNumberFormatter().format(number)).toBe('123,123')
|
||||
})
|
||||
|
||||
test('formats a number according to a locale', () => {
|
||||
init({ fallbackLocale: 'en' })
|
||||
expect(getNumberFormatter({ locale: 'pt-BR' }).format(number)).toBe(
|
||||
'123.123'
|
||||
)
|
||||
})
|
||||
|
||||
test('formats a number with a custom format', () => {
|
||||
init({
|
||||
fallbackLocale: 'en',
|
||||
formats: require('../../fixtures/formats.json'),
|
||||
})
|
||||
|
||||
expect(getNumberFormatter({ format: 'brl' }).format(number)).toBe(
|
||||
'R$123,123.00'
|
||||
)
|
||||
})
|
||||
|
||||
test('formats a number with inline options', () => {
|
||||
init({ fallbackLocale: 'en' })
|
||||
|
||||
expect(
|
||||
getNumberFormatter({ style: 'currency', currency: 'BRL' }).format(number)
|
||||
).toBe('R$123,123.00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('date formatter', () => {
|
||||
const date = new Date(2019, 1, 1)
|
||||
|
||||
test('throws if no locale is set', () => {
|
||||
expect(() => getDateFormatter().format(date)).toThrow(
|
||||
'[svelte-i18n] A "locale" must be set to format dates'
|
||||
)
|
||||
})
|
||||
|
||||
test('formats a date according to the current locale', () => {
|
||||
init({ fallbackLocale: 'en' })
|
||||
expect(getDateFormatter().format(date)).toBe('2/1/19')
|
||||
})
|
||||
|
||||
test('formats a date according to a locale', () => {
|
||||
expect(getDateFormatter({ locale: 'pt-BR' }).format(date)).toBe('01/02/19')
|
||||
})
|
||||
|
||||
test('throws if passed a non-existing format', () => {
|
||||
init({
|
||||
fallbackLocale: 'en',
|
||||
formats: require('../../fixtures/formats.json'),
|
||||
})
|
||||
|
||||
expect(() =>
|
||||
getDateFormatter({ locale: 'pt-BR', format: 'foo' }).format(date)
|
||||
).toThrowError(`[svelte-i18n] Unknown "foo" date format.`)
|
||||
})
|
||||
|
||||
test('formats a date with a custom format', () => {
|
||||
init({
|
||||
fallbackLocale: 'en',
|
||||
formats: require('../../fixtures/formats.json'),
|
||||
})
|
||||
|
||||
expect(getDateFormatter({ format: 'customDate' }).format(date)).toBe(
|
||||
'2019 AD'
|
||||
)
|
||||
})
|
||||
|
||||
test('formats a date with inline options', () => {
|
||||
init({ fallbackLocale: 'en' })
|
||||
|
||||
expect(
|
||||
getDateFormatter({ year: 'numeric', era: 'short' }).format(date)
|
||||
).toBe('2019 AD')
|
||||
})
|
||||
})
|
||||
|
||||
describe('time formatter', () => {
|
||||
const time = new Date(2019, 1, 1, 20, 37, 32)
|
||||
|
||||
test('throws if no locale is set', () => {
|
||||
expect(() => getTimeFormatter().format(time)).toThrow(
|
||||
'[svelte-i18n] A "locale" must be set to format time'
|
||||
)
|
||||
})
|
||||
|
||||
test('formats a time according to the current locale', () => {
|
||||
init({ fallbackLocale: 'en' })
|
||||
expect(getTimeFormatter().format(time)).toBe('8:37 PM')
|
||||
})
|
||||
|
||||
test('formats a time according to a locale', () => {
|
||||
expect(getTimeFormatter({ locale: 'pt-BR' }).format(time)).toBe('20:37')
|
||||
})
|
||||
|
||||
test('formats a time with a custom format', () => {
|
||||
init({
|
||||
fallbackLocale: 'en',
|
||||
formats: require('../../fixtures/formats.json'),
|
||||
})
|
||||
|
||||
expect(getTimeFormatter({ format: 'customTime' }).format(time)).toBe(
|
||||
'08:37:32 PM'
|
||||
)
|
||||
})
|
||||
|
||||
test('throws if passed a non-existing format', () => {
|
||||
init({
|
||||
fallbackLocale: 'en',
|
||||
formats: require('../../fixtures/formats.json'),
|
||||
})
|
||||
|
||||
expect(() =>
|
||||
getTimeFormatter({ locale: 'pt-BR', format: 'foo' }).format(time)
|
||||
).toThrowError(`[svelte-i18n] Unknown "foo" time format.`)
|
||||
})
|
||||
|
||||
test('formats a time with inline options', () => {
|
||||
init({ fallbackLocale: 'en' })
|
||||
|
||||
expect(
|
||||
getTimeFormatter({
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}).format(time)
|
||||
).toBe('08:37:32 PM')
|
||||
})
|
||||
})
|
||||
|
||||
describe('message formatter', () => {
|
||||
test('formats a message with interpolated values', () => {
|
||||
expect(
|
||||
getMessageFormatter('Page: {current,number}/{max,number}', 'en').format({
|
||||
current: 2,
|
||||
max: 10,
|
||||
})
|
||||
).toBe('Page: 2/10')
|
||||
})
|
||||
|
||||
test('formats number with custom formats', () => {
|
||||
expect(
|
||||
getMessageFormatter('Number: {n, number, compactShort}', 'en').format({
|
||||
n: 2000000,
|
||||
})
|
||||
).toBe('Number: 2M')
|
||||
})
|
||||
})
|
83
test/client/includes/loaderQueue.test.ts
Normal file
83
test/client/includes/loaderQueue.test.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { get } from 'svelte/store'
|
||||
|
||||
import {
|
||||
hasLocaleQueue,
|
||||
flush,
|
||||
registerLocaleLoader,
|
||||
resetQueues,
|
||||
} from '../../../src/client/includes/loaderQueue'
|
||||
import { getMessageFromDictionary } from '../../../src/client/stores/dictionary'
|
||||
import { $isLoading } from '../../../src/client/stores/loading'
|
||||
import { getOptions } from '../../../src/client/configs'
|
||||
|
||||
beforeEach(() => {
|
||||
resetQueues()
|
||||
})
|
||||
|
||||
const loader = (content: any) => () => new Promise(res => res(content))
|
||||
|
||||
test('registers a locale loader', () => {
|
||||
expect(hasLocaleQueue('pt-BR')).toBe(false)
|
||||
registerLocaleLoader('pt-BR', loader({ message: 'Mensagem' }))
|
||||
expect(hasLocaleQueue('pt-BR')).toBe(true)
|
||||
})
|
||||
|
||||
test('checks if exist queues of locale and its fallbacks', () => {
|
||||
registerLocaleLoader('en', loader({ field: 'Name' }))
|
||||
expect(hasLocaleQueue('en-US')).toBe(true)
|
||||
})
|
||||
|
||||
test("does nothing if there's no queue for a locale", () => {
|
||||
expect(flush('foo')).toBe(undefined)
|
||||
})
|
||||
|
||||
test('flushes the queue of a locale and its fallbacks and merge the result with the dictionary', async () => {
|
||||
registerLocaleLoader('en', loader({ field: 'Name' }))
|
||||
registerLocaleLoader('en-US', loader({ field_2: 'Lastname' }))
|
||||
|
||||
await flush('en-US')
|
||||
|
||||
expect(getMessageFromDictionary('en', 'field')).toBe('Name')
|
||||
expect(getMessageFromDictionary('en-US', 'field_2')).toBe('Lastname')
|
||||
|
||||
expect(hasLocaleQueue('en')).toBe(false)
|
||||
expect(hasLocaleQueue('en-US')).toBe(false)
|
||||
})
|
||||
|
||||
test('consecutive flushes return the same promise', async () => {
|
||||
registerLocaleLoader('en', async () => ({}))
|
||||
|
||||
const flushA = flush('en')
|
||||
const flushB = flush('en')
|
||||
const flushC = flush('en')
|
||||
|
||||
expect(flushB).toStrictEqual(flushA)
|
||||
expect(flushC).toStrictEqual(flushA)
|
||||
})
|
||||
|
||||
test('should set loading to true if passed min delay and false after loading', () => {
|
||||
registerLocaleLoader(
|
||||
'en',
|
||||
() =>
|
||||
new Promise(res =>
|
||||
setTimeout(() => res({}), getOptions().loadingDelay * 2)
|
||||
)
|
||||
)
|
||||
|
||||
const flushPromise = flush('en')
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
setTimeout(() => {
|
||||
if (get($isLoading) === true) return res()
|
||||
return rej('$isLoading should be "true"')
|
||||
}, getOptions().loadingDelay)
|
||||
}).then(() => {
|
||||
flushPromise.then(
|
||||
() =>
|
||||
new Promise((res, rej) => {
|
||||
if (get($isLoading) === false) return res()
|
||||
return rej('$isLoading should be "false" after loading')
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
48
test/client/includes/lookup.test.ts
Normal file
48
test/client/includes/lookup.test.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { lookupMessage, lookupCache } from '../../../src/client/includes/lookup'
|
||||
import { $dictionary, addMessages } from '../../../src/client/stores/dictionary'
|
||||
|
||||
beforeEach(() => {
|
||||
$dictionary.set({})
|
||||
})
|
||||
|
||||
test('returns null if no locale was passed', () => {
|
||||
expect(lookupMessage('message.id', undefined)).toBe(null)
|
||||
expect(lookupMessage('message.id', null)).toBe(null)
|
||||
})
|
||||
|
||||
test('gets a shallow message of a locale dictionary', () => {
|
||||
addMessages('en', { field: 'name' })
|
||||
expect(lookupMessage('field', 'en')).toBe('name')
|
||||
})
|
||||
|
||||
test('gets a deep message of a locale dictionary', () => {
|
||||
addMessages('en', { deep: { field: 'lastname' } })
|
||||
expect(lookupMessage('deep.field', 'en')).toBe('lastname')
|
||||
})
|
||||
|
||||
test('gets a message from the fallback dictionary', () => {
|
||||
addMessages('en', { field: 'name' })
|
||||
expect(lookupMessage('field', 'en-US')).toBe('name')
|
||||
})
|
||||
|
||||
test('caches found messages by locale', () => {
|
||||
addMessages('en', { field: 'name' })
|
||||
addMessages('pt', { field: 'nome' })
|
||||
lookupMessage('field', 'en-US')
|
||||
lookupMessage('field', 'pt-BR')
|
||||
expect(lookupCache).toMatchObject({
|
||||
'en-US': { field: 'name' },
|
||||
'pt-BR': { field: 'nome' },
|
||||
})
|
||||
})
|
||||
|
||||
test("doesn't cache falsy messages", () => {
|
||||
addMessages('en', { field: 'name' })
|
||||
addMessages('pt', { field: 'nome' })
|
||||
lookupMessage('field_2', 'en-US')
|
||||
lookupMessage('field_2', 'pt-BR')
|
||||
expect(lookupCache).not.toMatchObject({
|
||||
'en-US': { field_2: 'name' },
|
||||
'pt-BR': { field_2: 'nome' },
|
||||
})
|
||||
})
|
131
test/client/includes/utils.test.ts
Normal file
131
test/client/includes/utils.test.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
import {
|
||||
getClientLocale,
|
||||
capital,
|
||||
title,
|
||||
upper,
|
||||
lower,
|
||||
} from '../../../src/client/includes/utils'
|
||||
|
||||
describe('getting client locale', () => {
|
||||
beforeEach(() => {
|
||||
delete window.location
|
||||
window.location = {
|
||||
pathname: '/',
|
||||
hostname: 'example.com',
|
||||
hash: '',
|
||||
search: '',
|
||||
} as any
|
||||
})
|
||||
|
||||
test('gets the locale based on the passed hash parameter', () => {
|
||||
window.location.hash = '#locale=en-US&lang=pt-BR'
|
||||
expect(
|
||||
getClientLocale({
|
||||
hash: 'lang',
|
||||
})
|
||||
).toBe('pt-BR')
|
||||
})
|
||||
|
||||
test('gets the locale based on the passed search parameter', () => {
|
||||
window.location.search = '?locale=en-US&lang=pt-BR'
|
||||
expect(
|
||||
getClientLocale({
|
||||
search: 'lang',
|
||||
})
|
||||
).toBe('pt-BR')
|
||||
})
|
||||
|
||||
test('gets the locale based on the navigator language', () => {
|
||||
expect(
|
||||
getClientLocale({
|
||||
navigator: true,
|
||||
})
|
||||
).toBe(window.navigator.language)
|
||||
})
|
||||
|
||||
test('gets the locale based on the pathname', () => {
|
||||
window.location.pathname = '/en-US/foo/'
|
||||
expect(
|
||||
getClientLocale({
|
||||
pathname: /^\/(.*?)\//,
|
||||
})
|
||||
).toBe('en-US')
|
||||
})
|
||||
|
||||
test('gets the locale base on the hostname', () => {
|
||||
window.location.hostname = 'pt.example.com'
|
||||
expect(
|
||||
getClientLocale({
|
||||
hostname: /^(.*?)\./,
|
||||
})
|
||||
).toBe('pt')
|
||||
})
|
||||
|
||||
test('hostname precedes pathname', () => {
|
||||
window.location.pathname = '/en-US/foo/'
|
||||
window.location.hostname = 'pt.example.com'
|
||||
expect(
|
||||
getClientLocale({
|
||||
hostname: /^(.*?)\./,
|
||||
pathname: /^\/(.*?)\//,
|
||||
})
|
||||
).toBe('pt')
|
||||
})
|
||||
|
||||
test('pathname precedes navigator', () => {
|
||||
window.location.pathname = '/it-IT/foo/'
|
||||
expect(
|
||||
getClientLocale({
|
||||
pathname: /^\/(.*?)\//,
|
||||
navigator: true,
|
||||
})
|
||||
).toBe('it-IT')
|
||||
})
|
||||
|
||||
test('navigator precedes search', () => {
|
||||
window.location.search = '?lang=pt-BR'
|
||||
expect(
|
||||
getClientLocale({
|
||||
navigator: true,
|
||||
search: 'lang',
|
||||
})
|
||||
).toBe('en-US')
|
||||
})
|
||||
|
||||
test('search precedes hash', () => {
|
||||
window.location.hash = '#lang=pt-BR'
|
||||
window.location.search = '?lang=it-IT'
|
||||
expect(
|
||||
getClientLocale({
|
||||
hash: 'lang',
|
||||
search: 'lang',
|
||||
})
|
||||
).toBe('it-IT')
|
||||
})
|
||||
|
||||
test('returns null if no locale was found', () => {
|
||||
expect(
|
||||
getClientLocale({
|
||||
search: 'lang',
|
||||
})
|
||||
).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('string utilities', () => {
|
||||
test('transforms a string into capital case', () => {
|
||||
expect(capital('lowercase string')).toMatch('Lowercase string')
|
||||
})
|
||||
|
||||
test('transforms a string into title case', () => {
|
||||
expect(title('lowercase string')).toMatch('Lowercase String')
|
||||
})
|
||||
|
||||
test('transforms a string into uppercase', () => {
|
||||
expect(upper('lowercase string')).toMatch('LOWERCASE STRING')
|
||||
})
|
||||
|
||||
test('transforms a string into lowercase', () => {
|
||||
expect(lower('UPPERCASE STRING')).toMatch('uppercase string')
|
||||
})
|
||||
})
|
73
test/client/index.test.ts
Normal file
73
test/client/index.test.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { defineMessages, waitLocale, register, init } from '../../src/client'
|
||||
import { $locale } from '../../src/client/stores/locale'
|
||||
import { hasLocaleQueue } from '../../src/client/includes/loaderQueue'
|
||||
import {
|
||||
getLocaleDictionary,
|
||||
$dictionary,
|
||||
} from '../../src/client/stores/dictionary'
|
||||
import { $format } from '../../src/client/stores/format'
|
||||
|
||||
test('defineMessages returns the identity of its first argument', () => {
|
||||
const obj = {}
|
||||
expect(obj).toBe(defineMessages(obj))
|
||||
})
|
||||
|
||||
describe('waiting for a locale to load', () => {
|
||||
beforeEach(() => {
|
||||
$dictionary.set({})
|
||||
$locale.set(undefined)
|
||||
})
|
||||
|
||||
test('should wait for a locale queue to be flushed', async () => {
|
||||
register('en', () => Promise.resolve({ foo: 'foo' }))
|
||||
$locale.set('en')
|
||||
|
||||
await waitLocale('en')
|
||||
|
||||
expect(hasLocaleQueue('en')).toBe(false)
|
||||
expect(getLocaleDictionary('en')).toMatchObject({ foo: 'foo' })
|
||||
})
|
||||
|
||||
test('should wait for the current locale queue to be flushed', async () => {
|
||||
register('en', () => Promise.resolve({ foo: 'foo' }))
|
||||
init({ fallbackLocale: 'pt', initialLocale: 'en' })
|
||||
|
||||
await waitLocale()
|
||||
|
||||
expect(hasLocaleQueue('en')).toBe(false)
|
||||
expect(getLocaleDictionary('en')).toMatchObject({ foo: 'foo' })
|
||||
})
|
||||
|
||||
test('should wait for the fallback locale queue to be flushed if initial not set', async () => {
|
||||
register('pt', () => Promise.resolve({ foo: 'foo' }))
|
||||
init({ fallbackLocale: 'pt' })
|
||||
|
||||
await waitLocale()
|
||||
|
||||
expect(hasLocaleQueue('pt')).toBe(false)
|
||||
expect(getLocaleDictionary('pt')).toMatchObject({ foo: 'foo' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('format updates', () => {
|
||||
beforeEach(() => {
|
||||
init({ fallbackLocale: 'en' })
|
||||
})
|
||||
|
||||
test('format store is updated when locale changes', () => {
|
||||
const fn = jest.fn()
|
||||
const cancel = $format.subscribe(fn)
|
||||
|
||||
$locale.set('pt')
|
||||
expect(fn).toHaveBeenCalledTimes(2)
|
||||
cancel()
|
||||
})
|
||||
|
||||
test('format store is updated when dictionary changes', () => {
|
||||
const fn = jest.fn()
|
||||
const cancel = $format.subscribe(fn)
|
||||
$dictionary.set({})
|
||||
expect(fn).toHaveBeenCalledTimes(2)
|
||||
cancel()
|
||||
})
|
||||
})
|
113
test/client/stores/dictionary.test.ts
Normal file
113
test/client/stores/dictionary.test.ts
Normal file
|
@ -0,0 +1,113 @@
|
|||
import { get } from 'svelte/store'
|
||||
|
||||
import {
|
||||
getDictionary,
|
||||
hasLocaleDictionary,
|
||||
getClosestAvailableLocale,
|
||||
getMessageFromDictionary,
|
||||
addMessages,
|
||||
$dictionary,
|
||||
$locales,
|
||||
getLocaleDictionary,
|
||||
} from '../../../src/client/stores/dictionary'
|
||||
|
||||
beforeEach(() => {
|
||||
$dictionary.set({})
|
||||
})
|
||||
|
||||
test('adds a new dictionary to a locale', () => {
|
||||
addMessages('en', { field_1: 'name' })
|
||||
addMessages('pt', { field_1: 'nome' })
|
||||
|
||||
expect(get($dictionary)).toMatchObject({
|
||||
en: { field_1: 'name' },
|
||||
pt: { field_1: 'nome' },
|
||||
})
|
||||
})
|
||||
|
||||
test('gets the whole current dictionary', () => {
|
||||
addMessages('en', { field_1: 'name' })
|
||||
expect(getDictionary()).toMatchObject(get($dictionary))
|
||||
})
|
||||
|
||||
test('merges the existing dictionaries with new ones', () => {
|
||||
addMessages('en', { field_1: 'name', deep: { prop1: 'foo' } })
|
||||
addMessages('en', { field_2: 'lastname', deep: { prop2: 'foo' } })
|
||||
addMessages('pt', { field_1: 'nome', deep: { prop1: 'foo' } })
|
||||
addMessages('pt', { field_2: 'sobrenome', deep: { prop2: 'foo' } })
|
||||
|
||||
expect(get($dictionary)).toMatchObject({
|
||||
en: {
|
||||
field_1: 'name',
|
||||
field_2: 'lastname',
|
||||
deep: { prop1: 'foo', prop2: 'foo' },
|
||||
},
|
||||
pt: {
|
||||
field_1: 'nome',
|
||||
field_2: 'sobrenome',
|
||||
deep: { prop1: 'foo', prop2: 'foo' },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('gets the dictionary of a locale', () => {
|
||||
addMessages('en', { field_1: 'name' })
|
||||
expect(getLocaleDictionary('en')).toMatchObject({ field_1: 'name' })
|
||||
})
|
||||
|
||||
test('checks if a locale dictionary exists', () => {
|
||||
addMessages('pt', { field_1: 'name' })
|
||||
expect(hasLocaleDictionary('en')).toBe(false)
|
||||
expect(hasLocaleDictionary('pt')).toBe(true)
|
||||
})
|
||||
|
||||
test('gets the closest available locale', () => {
|
||||
addMessages('pt', { field_1: 'name' })
|
||||
expect(getClosestAvailableLocale('pt-BR')).toBe('pt')
|
||||
})
|
||||
|
||||
test("returns null if there's no closest locale available", () => {
|
||||
addMessages('pt', { field_1: 'name' })
|
||||
expect(getClosestAvailableLocale('it-IT')).toBe(null)
|
||||
})
|
||||
|
||||
test('lists all locales in the dictionary', () => {
|
||||
addMessages('en', {})
|
||||
addMessages('pt', {})
|
||||
addMessages('pt-BR', {})
|
||||
expect(get($locales)).toEqual(['en', 'pt', 'pt-BR'])
|
||||
})
|
||||
|
||||
describe('getting messages', () => {
|
||||
test('gets a message from a shallow dictionary', () => {
|
||||
addMessages('en', { message: 'Some message' })
|
||||
expect(getMessageFromDictionary('en', 'message')).toBe('Some message')
|
||||
})
|
||||
|
||||
test('gets a message from a deep object in the dictionary', () => {
|
||||
addMessages('en', { messages: { message_1: 'Some message' } })
|
||||
expect(getMessageFromDictionary('en', 'messages.message_1')).toBe(
|
||||
'Some message'
|
||||
)
|
||||
})
|
||||
|
||||
test('gets a message from an array in the dictionary', () => {
|
||||
addMessages('en', { messages: ['Some message', 'Other message'] })
|
||||
expect(getMessageFromDictionary('en', 'messages.0')).toBe('Some message')
|
||||
expect(getMessageFromDictionary('en', 'messages.1')).toBe('Other message')
|
||||
})
|
||||
|
||||
test('accepts english in dictionary keys', () => {
|
||||
addMessages('pt', {
|
||||
'Hey man, how are you today?': 'E ai cara, como você vai hoje?',
|
||||
})
|
||||
expect(getMessageFromDictionary('pt', 'Hey man, how are you today?')).toBe(
|
||||
'E ai cara, como você vai hoje?'
|
||||
)
|
||||
})
|
||||
|
||||
test('returns null for missing messages', () => {
|
||||
addMessages('en', {})
|
||||
expect(getMessageFromDictionary('en', 'foo')).toBe(null)
|
||||
})
|
||||
})
|
92
test/client/stores/format.test.ts
Normal file
92
test/client/stores/format.test.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
import { Formatter } from '../../../src/client/types/index'
|
||||
import { $format } from '../../../src/client/stores/format'
|
||||
import { init } from '../../../src/client/configs'
|
||||
import { addMessages } from '../../../src/client/stores/dictionary'
|
||||
import { $locale } from '../../../src/client/stores/locale'
|
||||
|
||||
let format: Formatter
|
||||
$format.subscribe(f => (format = f))
|
||||
|
||||
addMessages('en', require('../../fixtures/en.json'))
|
||||
addMessages('en-GB', require('../../fixtures/en-GB.json'))
|
||||
addMessages('pt', require('../../fixtures/pt.json'))
|
||||
addMessages('pt-BR', require('../../fixtures/pt-BR.json'))
|
||||
addMessages('pt-PT', require('../../fixtures/pt-PT.json'))
|
||||
|
||||
beforeEach(() => {
|
||||
init({ fallbackLocale: 'en' })
|
||||
})
|
||||
|
||||
test('formats a message by its id and the current locale', () => {
|
||||
expect(format({ id: 'form.field_1_name' })).toBe('Name')
|
||||
})
|
||||
|
||||
test('formats a message by its id and the a passed locale', () => {
|
||||
expect(format({ id: 'form.field_1_name', locale: 'pt' })).toBe('Nome')
|
||||
})
|
||||
|
||||
test('formats a message with interpolated values', () => {
|
||||
expect(format({ id: 'photos', values: { n: 0 } })).toBe('You have no photos.')
|
||||
expect(format({ id: 'photos', values: { n: 1 } })).toBe('You have one photo.')
|
||||
expect(format({ id: 'photos', values: { n: 21 } })).toBe(
|
||||
'You have 21 photos.'
|
||||
)
|
||||
})
|
||||
|
||||
test('accepts a message id as first argument', () => {
|
||||
expect(format('form.field_1_name')).toBe('Name')
|
||||
})
|
||||
|
||||
test('accepts a message id as first argument and formatting options as second', () => {
|
||||
expect(format('form.field_1_name', { locale: 'pt' })).toBe('Nome')
|
||||
})
|
||||
|
||||
test('throws if no locale is set', () => {
|
||||
$locale.set(null)
|
||||
expect(() => format('form.field_1_name')).toThrow(
|
||||
'[svelte-i18n] Cannot format a message without first setting the initial locale.'
|
||||
)
|
||||
})
|
||||
|
||||
test('uses a missing message default value', () => {
|
||||
expect(format('missing', { default: 'Missing Default' })).toBe(
|
||||
'Missing Default'
|
||||
)
|
||||
})
|
||||
|
||||
test('warn on missing messages', () => {
|
||||
const warn = global.console.warn
|
||||
global.console.warn = jest.fn()
|
||||
|
||||
format('missing')
|
||||
|
||||
expect(console.warn).toBeCalledWith(
|
||||
`[svelte-i18n] The message "missing" was not found in "en".`
|
||||
)
|
||||
|
||||
global.console.warn = warn
|
||||
})
|
||||
|
||||
describe('format utilities', () => {
|
||||
test('time', () => {
|
||||
expect(format.time(new Date(2019, 0, 1, 20, 37))).toBe('8:37 PM')
|
||||
})
|
||||
test('date', () => {
|
||||
expect(format.date(new Date(2019, 0, 1, 20, 37))).toBe('1/1/19')
|
||||
})
|
||||
test('number', () => {
|
||||
expect(format.number(123123123)).toBe('123,123,123')
|
||||
})
|
||||
test('capital', () => {
|
||||
expect(format.capital('title')).toBe('Page title')
|
||||
})
|
||||
test('title', () => {
|
||||
expect(format.title('title')).toBe('Page Title')
|
||||
})
|
||||
test('upper', () => {
|
||||
expect(format.upper('title')).toBe('PAGE TITLE')
|
||||
})
|
||||
test('lower', () => {
|
||||
expect(format.lower('title')).toBe('page title')
|
||||
})
|
||||
})
|
136
test/client/stores/locale.test.ts
Normal file
136
test/client/stores/locale.test.ts
Normal file
|
@ -0,0 +1,136 @@
|
|||
import { get } from 'svelte/store'
|
||||
|
||||
import { lookupMessage } from '../../../src/client/includes/lookup'
|
||||
import {
|
||||
isFallbackLocaleOf,
|
||||
getFallbackOf,
|
||||
getRelatedLocalesOf,
|
||||
getCurrentLocale,
|
||||
$locale,
|
||||
isRelatedLocale,
|
||||
} from '../../../src/client/stores/locale'
|
||||
import { getOptions, init } from '../../../src/client/configs'
|
||||
import { register } from '../../../src/client'
|
||||
import { hasLocaleQueue } from '../../../src/client/includes/loaderQueue'
|
||||
|
||||
beforeEach(() => {
|
||||
init({ fallbackLocale: undefined })
|
||||
$locale.set(undefined)
|
||||
})
|
||||
|
||||
test('sets and gets the fallback locale', () => {
|
||||
init({ fallbackLocale: 'en' })
|
||||
expect(getOptions().fallbackLocale).toBe('en')
|
||||
})
|
||||
|
||||
test('checks if a locale is a fallback locale of another locale', () => {
|
||||
expect(isFallbackLocaleOf('en', 'en-US')).toBe(true)
|
||||
expect(isFallbackLocaleOf('en', 'en')).toBe(false)
|
||||
expect(isFallbackLocaleOf('it', 'en-US')).toBe(false)
|
||||
})
|
||||
|
||||
test('checks if a locale is a fallback locale of another locale', () => {
|
||||
expect(isRelatedLocale('en', 'en-US')).toBe(true)
|
||||
expect(isRelatedLocale('pt-BR', 'pt')).toBe(true)
|
||||
expect(isRelatedLocale('en', 'en')).toBe(true)
|
||||
expect(isRelatedLocale('en', 'it-IT')).toBe(false)
|
||||
expect(isRelatedLocale('en-US', 'it')).toBe(false)
|
||||
})
|
||||
|
||||
test('gets the next fallback locale of a locale', () => {
|
||||
expect(getFallbackOf('az-Cyrl-AZ')).toBe('az-Cyrl')
|
||||
expect(getFallbackOf('en-US')).toBe('en')
|
||||
expect(getFallbackOf('en')).toBe(null)
|
||||
})
|
||||
|
||||
test('gets the global fallback locale if set', () => {
|
||||
init({ fallbackLocale: 'en' })
|
||||
expect(getFallbackOf('it')).toBe('en')
|
||||
})
|
||||
|
||||
test('should not get the global fallback as the fallback of itself', () => {
|
||||
init({ fallbackLocale: 'en' })
|
||||
expect(getFallbackOf('en')).toBe(null)
|
||||
})
|
||||
|
||||
test('if global fallback locale has a fallback, it should return it', () => {
|
||||
init({ fallbackLocale: 'en-US' })
|
||||
expect(getFallbackOf('en-US')).toBe('en')
|
||||
})
|
||||
|
||||
test('gets all fallback locales of a locale', () => {
|
||||
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US'])
|
||||
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US'])
|
||||
expect(getRelatedLocalesOf('az-Cyrl-AZ')).toEqual([
|
||||
'az',
|
||||
'az-Cyrl',
|
||||
'az-Cyrl-AZ',
|
||||
])
|
||||
})
|
||||
|
||||
test('gets all fallback locales of a locale including the global fallback locale', () => {
|
||||
init({ fallbackLocale: 'pt' })
|
||||
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt'])
|
||||
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt'])
|
||||
expect(getRelatedLocalesOf('az-Cyrl-AZ')).toEqual([
|
||||
'az',
|
||||
'az-Cyrl',
|
||||
'az-Cyrl-AZ',
|
||||
'pt',
|
||||
])
|
||||
})
|
||||
test('gets all fallback locales of a locale including the global fallback locale and its fallbacks', () => {
|
||||
init({ fallbackLocale: 'pt-BR' })
|
||||
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt', 'pt-BR'])
|
||||
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt', 'pt-BR'])
|
||||
expect(getRelatedLocalesOf('az-Cyrl-AZ')).toEqual([
|
||||
'az',
|
||||
'az-Cyrl',
|
||||
'az-Cyrl-AZ',
|
||||
'pt',
|
||||
'pt-BR',
|
||||
])
|
||||
})
|
||||
|
||||
test("don't list fallback locale twice", () => {
|
||||
init({ fallbackLocale: 'pt-BR' })
|
||||
expect(getRelatedLocalesOf('pt-BR')).toEqual(['pt', 'pt-BR'])
|
||||
expect(getRelatedLocalesOf('pt')).toEqual(['pt'])
|
||||
})
|
||||
|
||||
test('gets the current locale', () => {
|
||||
expect(getCurrentLocale()).toBe(undefined)
|
||||
$locale.set('es-ES')
|
||||
expect(getCurrentLocale()).toBe('es-ES')
|
||||
})
|
||||
|
||||
test('if no initial locale is set, set the locale to the fallback', () => {
|
||||
init({ fallbackLocale: 'pt' })
|
||||
expect(get($locale)).toBe('pt')
|
||||
expect(getOptions().fallbackLocale).toBe('pt')
|
||||
})
|
||||
|
||||
test('if no initial locale was found, set to the fallback locale', () => {
|
||||
init({
|
||||
fallbackLocale: 'en',
|
||||
initialLocale: {
|
||||
hash: 'lang',
|
||||
},
|
||||
})
|
||||
expect(get($locale)).toBe('en')
|
||||
expect(getOptions().fallbackLocale).toBe('en')
|
||||
})
|
||||
|
||||
test('should flush the queue of the locale when changing the store value', async () => {
|
||||
register(
|
||||
'en',
|
||||
() => new Promise(res => setTimeout(() => res({ foo: 'Foo' }), 50))
|
||||
)
|
||||
|
||||
expect(hasLocaleQueue('en')).toBe(true)
|
||||
|
||||
await $locale.set('en')
|
||||
|
||||
expect(hasLocaleQueue('en')).toBe(false)
|
||||
expect(lookupMessage('foo', 'en')).toBe('Foo')
|
||||
})
|
3
test/fixtures/en-GB.json
vendored
Normal file
3
test/fixtures/en-GB.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"sneakers": "trainers"
|
||||
}
|
9
test/fixtures/en.json
vendored
Normal file
9
test/fixtures/en.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"form": {
|
||||
"field_1_name": "Name",
|
||||
"field_2_name": "Lastname"
|
||||
},
|
||||
"photos": "You have {n, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}",
|
||||
"title": "Page title",
|
||||
"sneakers": "sneakers"
|
||||
}
|
8
test/fixtures/es.json
vendored
Normal file
8
test/fixtures/es.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"form": {
|
||||
"field_1_name": "Nombre",
|
||||
"field_2_name": "Apellido"
|
||||
},
|
||||
"title": "Título de la página",
|
||||
"sneakers": "zapatillas"
|
||||
}
|
16
test/fixtures/formats.json
vendored
Normal file
16
test/fixtures/formats.json
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"number": {
|
||||
"usd": { "style": "currency", "currency": "USD" },
|
||||
"brl": { "style": "currency", "currency": "BRL" }
|
||||
},
|
||||
"date": {
|
||||
"customDate": { "year": "numeric", "era": "short" }
|
||||
},
|
||||
"time": {
|
||||
"customTime": {
|
||||
"hour": "2-digit",
|
||||
"minute": "2-digit",
|
||||
"second": "2-digit"
|
||||
}
|
||||
}
|
||||
}
|
5
test/fixtures/partials/en.json
vendored
Normal file
5
test/fixtures/partials/en.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"page": {
|
||||
"title_about": "About"
|
||||
}
|
||||
}
|
3
test/fixtures/pt-BR.json
vendored
Normal file
3
test/fixtures/pt-BR.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"french_bread": "pão francês"
|
||||
}
|
3
test/fixtures/pt-PT.json
vendored
Normal file
3
test/fixtures/pt-PT.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"french_bread": "cacetinhos"
|
||||
}
|
9
test/fixtures/pt.json
vendored
Normal file
9
test/fixtures/pt.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"form": {
|
||||
"field_1_name": "Nome",
|
||||
"field_2_name": "Sobrenome"
|
||||
},
|
||||
"photos": "Você {n, plural, =0 {não tem fotos.} =1 {tem uma foto.} other {tem # fotos.}}",
|
||||
"title": "Título da página",
|
||||
"sneakers": "tênis"
|
||||
}
|
5
test/fixtures/svelte.config.js
vendored
Normal file
5
test/fixtures/svelte.config.js
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
const preprocess = require('svelte-preprocess')
|
||||
|
||||
module.exports = {
|
||||
preprocess: preprocess(),
|
||||
}
|
|
@ -1,206 +0,0 @@
|
|||
import {
|
||||
dictionary,
|
||||
locale,
|
||||
format,
|
||||
getClientLocale,
|
||||
addCustomFormats,
|
||||
customFormats,
|
||||
} from '../src/index.js'
|
||||
|
||||
let _
|
||||
let currentLocale
|
||||
|
||||
const dict = {
|
||||
pt: {
|
||||
hi: 'olá você',
|
||||
'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: {
|
||||
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}}',
|
||||
},
|
||||
}
|
||||
|
||||
format.subscribe(formatFn => {
|
||||
_ = formatFn
|
||||
})
|
||||
dictionary.set(dict)
|
||||
locale.subscribe(l => (currentLocale = l))
|
||||
locale.set('pt')
|
||||
|
||||
it('should change locale', () => {
|
||||
locale.set('pt')
|
||||
expect(currentLocale).toBe('pt')
|
||||
locale.set('en')
|
||||
expect(currentLocale).toBe('en')
|
||||
})
|
||||
|
||||
it('should fallback to existing locale', () => {
|
||||
locale.set('pt-BR')
|
||||
expect(currentLocale).toBe('pt')
|
||||
|
||||
locale.set('en-US')
|
||||
expect(currentLocale).toBe('en')
|
||||
})
|
||||
|
||||
it("should throw an error if locale doesn't exist", () => {
|
||||
expect(() => locale.set('FOO')).toThrow()
|
||||
})
|
||||
|
||||
it('should fallback to message id if id is not found', () => {
|
||||
locale.set('en')
|
||||
expect(_('batatinha')).toBe('batatinha')
|
||||
})
|
||||
|
||||
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', { locale: 'pt' })).toBe('Trocar idioma')
|
||||
expect(_('switch.lang', { locale: 'en' })).toBe('Switch language')
|
||||
})
|
||||
|
||||
it('should interpolate message with variables', () => {
|
||||
expect(_('greeting.message', { values: { name: 'Chris' } })).toBe(
|
||||
'Hello Chris, how are you?',
|
||||
)
|
||||
})
|
||||
|
||||
it('should interpolate message with variables according to passed locale', () => {
|
||||
expect(
|
||||
_('greeting.message', { values: { name: 'Chris' }, locale: 'pt' }),
|
||||
).toBe('Olá Chris, como vai?')
|
||||
})
|
||||
|
||||
describe('utilities', () => {
|
||||
describe('get locale', () => {
|
||||
beforeEach(() => {
|
||||
delete window.location
|
||||
window.location = {
|
||||
hash: '',
|
||||
search: '',
|
||||
}
|
||||
})
|
||||
|
||||
it('should get the locale based on the passed hash parameter', () => {
|
||||
window.location.hash = '#locale=en-US&lang=pt-BR'
|
||||
expect(getClientLocale({ hash: 'locale' })).toBe('en-US')
|
||||
expect(getClientLocale({ hash: 'lang' })).toBe('pt-BR')
|
||||
})
|
||||
|
||||
it('should get the locale based on the passed search parameter', () => {
|
||||
window.location.search = '?locale=en-US&lang=pt-BR'
|
||||
expect(getClientLocale({ search: 'locale' })).toBe('en-US')
|
||||
expect(getClientLocale({ search: 'lang' })).toBe('pt-BR')
|
||||
})
|
||||
|
||||
it('should get the locale based on the navigator language', () => {
|
||||
expect(getClientLocale({ navigator: true })).toBe(
|
||||
window.navigator.language,
|
||||
)
|
||||
})
|
||||
|
||||
it('should get the fallback locale', () => {
|
||||
expect(getClientLocale({ navigator: false, fallback: 'pt' })).toBe('pt')
|
||||
expect(getClientLocale({ hash: 'locale', fallback: 'pt' })).toBe('pt')
|
||||
})
|
||||
})
|
||||
|
||||
describe('format utils', () => {
|
||||
beforeAll(() => {
|
||||
locale.set('en')
|
||||
})
|
||||
|
||||
it('should capital a translated message', () => {
|
||||
expect(_.capital('hi')).toBe('Hi yo')
|
||||
})
|
||||
|
||||
it('should title a translated message', () => {
|
||||
expect(_.title('hi')).toBe('Hi Yo')
|
||||
})
|
||||
|
||||
it('should lowercase a translated message', () => {
|
||||
expect(_.lower('hi')).toBe('hi yo')
|
||||
})
|
||||
|
||||
it('should uppercase a translated message', () => {
|
||||
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, { format: 'medium' })).toBe('11:45:00 PM')
|
||||
})
|
||||
|
||||
it('should format a date value', () => {
|
||||
expect(_.date(date)).toBe('4/24/19')
|
||||
expect(_.date(date, { format: 'medium' })).toBe('Apr 24, 2019')
|
||||
})
|
||||
// number
|
||||
it('should format a date value', () => {
|
||||
expect(_.number(123123123)).toBe('123,123,123')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom formats', () => {
|
||||
beforeAll(() => {
|
||||
locale.set('pt-BR')
|
||||
})
|
||||
|
||||
it('should have default number custom formats', () => {
|
||||
expect(customFormats.number).toMatchObject({
|
||||
scientific: { notation: 'scientific' },
|
||||
engineering: { notation: 'engineering' },
|
||||
compactLong: { notation: 'compact', compactDisplay: 'long' },
|
||||
compactShort: { notation: 'compact', compactDisplay: 'short' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow to add custom formats', () => {
|
||||
addCustomFormats({
|
||||
number: {
|
||||
usd: { style: 'currency', currency: 'USD' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(customFormats.number).toMatchObject({
|
||||
usd: { style: 'currency', currency: 'USD' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should format messages with custom formats', () => {
|
||||
addCustomFormats({
|
||||
number: {
|
||||
usd: { style: 'currency', currency: 'USD' },
|
||||
brl: { style: 'currency', currency: 'BRL' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(_.number(123123123, { format: 'usd' })).toContain('US$')
|
||||
expect(_.number(123123123, { format: 'usd' })).toContain('123.123.123,00')
|
||||
|
||||
expect(_.number(123123123, { format: 'brl' })).toContain('R$')
|
||||
expect(_.number(123123123, { format: 'brl' })).toContain('123.123.123,00')
|
||||
})
|
||||
})
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": false,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "esnext",
|
||||
"lib": ["es2018", "dom", "esnext"],
|
||||
"outDir": "dist",
|
||||
"types": ["svelte", "jest"]
|
||||
},
|
||||
"exclude": ["node_modules/**/*", "dist"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user