mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-09-28 15:14:45 +02:00
refactor: 💡 rewrite to typescript
This commit is contained in:
parent
d483244a9f
commit
db21bb878a
@ -1 +1,2 @@
|
||||
/test/fixtures
|
||||
/test/fixtures
|
||||
dist
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": ["kaisermann"],
|
||||
"extends": ["kaisermann/typescript"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"jest": true
|
||||
|
225
README.md
225
README.md
@ -2,11 +2,26 @@
|
||||
|
||||
> Internationalization for Svelte.
|
||||
|
||||
[See Demo](https://svelte-i18n.netlify.com/)
|
||||
<!-- [See Demo](https://svelte-i18n.netlify.com/) -->
|
||||
|
||||
<!-- @import "[TOC]" {cmd="toc" depthFrom=2 depthTo=3 orderedList=false} -->
|
||||
|
||||
<!-- code_chunk_output -->
|
||||
|
||||
- [Usage](#usage)
|
||||
- [Locale](#locale)
|
||||
- [The dictionary](#the-dictionary)
|
||||
- [Formatting](#formatting)
|
||||
- [Formatting methods](#formatting-methods)
|
||||
- [Formats](#formats)
|
||||
- [CLI](#cli)
|
||||
- [Options](#options)
|
||||
|
||||
<!-- /code_chunk_output -->
|
||||
|
||||
## Usage
|
||||
|
||||
`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.
|
||||
`svelte-i18n` uses `stores` to keep 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.
|
||||
|
||||
---
|
||||
|
||||
@ -21,9 +36,7 @@ import { locale, dictionary, getClientLocale } from 'svelte-i18n'
|
||||
locale.set('en-US')
|
||||
|
||||
// This is a store, so we can subscribe to its changes
|
||||
locale.subscribe(() => {
|
||||
console.log('locale change')
|
||||
})
|
||||
locale.subscribe(() => console.log('locale change'))
|
||||
|
||||
// svelte-i18n exports a method to help getting the current client locale
|
||||
locale.set(
|
||||
@ -95,7 +108,7 @@ Each language message dictionary can be as deep as you want. Messages can also b
|
||||
|
||||
### Formatting
|
||||
|
||||
The `_`/`format` store is the actual formatter method. To use it, it's simple as any other svelte store.
|
||||
The `_`/`format` store is the actual formatter method. To use it it's simple as any other svelte store.
|
||||
|
||||
```html
|
||||
<script>
|
||||
@ -110,13 +123,13 @@ The `_`/`format` store is the actual formatter method. To use it, it's simple as
|
||||
|
||||
```html
|
||||
<div>
|
||||
{$_('greeting.message', { name: 'John' })}
|
||||
{$_('greeting.message', { values: { name: 'John' }})}
|
||||
<!-- Hello John, how are you? -->
|
||||
|
||||
{$_('photos', { n: 0 })}
|
||||
{$_('photos', { values: { n: 0 }})}
|
||||
<!-- You have no photos. -->
|
||||
|
||||
{$_('photos', { n: 12 })}
|
||||
{$_('photos', { values: { n: 12} })}
|
||||
<!-- You have 12 photos. -->
|
||||
</div>
|
||||
```
|
||||
@ -125,11 +138,28 @@ The `_`/`format` store is the actual formatter method. To use it, it's simple as
|
||||
|
||||
#### `_` / `format`
|
||||
|
||||
`function(messageId: string, locale:? string): string`
|
||||
Main formatting method that formats a localized message by its `id`.
|
||||
|
||||
`function(messageId: string, interpolations?: object, locale:? string): string`
|
||||
```ts
|
||||
function(messageId: string, options?: MessageObject): string
|
||||
function(options: MessageObject): string
|
||||
|
||||
Main formatting method that formats a localized message by its id.
|
||||
interface MessageObject {
|
||||
id?: string
|
||||
locale?: string
|
||||
format?: string
|
||||
default?: string
|
||||
values?: Record<string, string | number | Date>
|
||||
}
|
||||
```
|
||||
|
||||
- `id`: represents the path to a specific message;
|
||||
- `locale`: forces a specific locale;
|
||||
- `default`: the default value in case of message not found in the current locale;
|
||||
- `format`: the format to be used. See [#formats](#formats);
|
||||
- `values`: properties that should be interpolated in the message;
|
||||
|
||||
You can pass a `string` as the first parameter for a less verbose way of formatting a message.
|
||||
|
||||
```html
|
||||
<script>
|
||||
@ -138,110 +168,155 @@ Main formatting method that formats a localized message by its id.
|
||||
|
||||
<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>
|
||||
<div>{$_({ id: 'greeting.ask' })}</div>
|
||||
<!-- Please type your name -->
|
||||
```
|
||||
|
||||
#### `_.title`
|
||||
The formatter method also provides some casing utilities:
|
||||
|
||||
Transform the message into title case.
|
||||
- `_.upper` - transforms a localized message into uppercase;
|
||||
- `_.lower` - transforms a localized message into lowercase;
|
||||
- `_.capital` - capitalize a localized message;
|
||||
- `_.title` - transforms the message into title case;
|
||||
|
||||
```html
|
||||
<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
</script>
|
||||
<div>{$_.upper('greeting.ask')}</div>
|
||||
<!-- PLEASE TYPE YOUR NAME -->
|
||||
|
||||
<div>{$_.lower('greeting.ask')}</div>
|
||||
<!-- please type your name -->
|
||||
|
||||
<div>{$_.capital('greeting.ask')}</div>
|
||||
<!-- Please type your name -->
|
||||
|
||||
<div>{$_.title('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 [#formats](#formats) section to see available formats.
|
||||
|
||||
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
|
||||
```ts
|
||||
function(time: Date, options: MessageObject): string
|
||||
```
|
||||
|
||||
```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>
|
||||
<div>{$_.time(new Date(2019, 3, 24, 23, 45), { format: '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 [#formats](#formats) section to see available formats.
|
||||
|
||||
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
|
||||
```ts
|
||||
function(date: Date, options: MessageObject): string
|
||||
```
|
||||
|
||||
```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>
|
||||
<div>{$_.date(new Date(2019, 3, 24, 23, 45), { format: 'medium' } )}</div>
|
||||
<!-- Apr 24, 2019 -->
|
||||
```
|
||||
|
||||
#### `_.number`
|
||||
|
||||
`function(number: Number, locale?: string)`
|
||||
Formats a number with the specified locale and format. Please refer to the [#formats](#formats) section to see available formats.
|
||||
|
||||
Formats a number with the specified locale
|
||||
```ts
|
||||
function(number: number, options: MessageObject): string
|
||||
```
|
||||
|
||||
```html
|
||||
<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
</script>
|
||||
|
||||
<div>{$_.number(100000000)}</div>
|
||||
<!-- 100,000,000 -->
|
||||
|
||||
<div>{$_.number(100000000, 'pt')}</div>
|
||||
<div>{$_.number(100000000, { locale: 'pt' })}</div>
|
||||
<!-- 100.000.000 -->
|
||||
```
|
||||
|
||||
### Formats
|
||||
|
||||
`svelte-i18n` comes with a set of default `number`, `time` and `date` formats:
|
||||
|
||||
**Number:**
|
||||
|
||||
- `currency`: `{ style: 'currency' }`
|
||||
- `percent`: `{ style: 'percent' }`
|
||||
- `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' }`
|
||||
|
||||
It's possible to define custom format styles via the `addCustomFormats` method if you need to supply a set of options to the underlying `Intl` formatter.
|
||||
|
||||
```ts
|
||||
function addCustomFormats(formatsObject: Formats): void
|
||||
|
||||
interface Formats {
|
||||
number: Record<string, Intl.NumberFormatOptions>
|
||||
date: Record<string, Intl.DateTimeFormatOptions>
|
||||
time: Record<string, Intl.DateTimeFormatOptions>
|
||||
}
|
||||
```
|
||||
|
||||
Please refer to the [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat) and [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat) documentations to see available formatting options.
|
||||
|
||||
**Example**:
|
||||
|
||||
```js
|
||||
import { addCustomFormats } from 'svelte-i18n'
|
||||
|
||||
addCustomFormats({
|
||||
number: {
|
||||
EUR: {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
```html
|
||||
<div>
|
||||
{$_.number(123456.789, { format: 'EUR' })}
|
||||
</div>
|
||||
<!-- 123.456,79 € -->
|
||||
```
|
||||
|
||||
## CLI
|
||||
|
||||
`svelte-i18n` provides a command-line interface to extract all your messages to the `stdout` or to a specific JSON file.
|
||||
|
||||
```bash
|
||||
$ svelte-i18n extract [options] <glob-pattern> [output-file]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
- `-s, --shallow` - extract all messages to a shallow object, without creating nested objects. Default: `false`.
|
||||
|
||||
- `--overwrite` - overwrite the content of the `output` file instead of just appending missing properties. Default: `false`.
|
||||
|
||||
- `-c, --configDir` - define the directory of a [`svelte.config.js`](https://github.com/UnwrittenFun/svelte-vscode#generic-setup) in case your svelte components need to be preprocessed.
|
||||
|
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).
|
||||
|
4
example/cypress.json
Normal file
4
example/cypress.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"video": false
|
||||
}
|
5
example/cypress/fixtures/example.json
Normal file
5
example/cypress/fixtures/example.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
19
example/cypress/integration/spec.js
Normal file
19
example/cypress/integration/spec.js
Normal file
@ -0,0 +1,19 @@
|
||||
describe('Sapper template app', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
});
|
||||
|
||||
it('has the correct <h1>', () => {
|
||||
cy.contains('h1', 'Great success!')
|
||||
});
|
||||
|
||||
it('navigates to /about', () => {
|
||||
cy.get('nav a').contains('about').click();
|
||||
cy.url().should('include', '/about');
|
||||
});
|
||||
|
||||
it('navigates to /blog', () => {
|
||||
cy.get('nav a').contains('blog').click();
|
||||
cy.url().should('include', '/blog');
|
||||
});
|
||||
});
|
17
example/cypress/plugins/index.js
Normal file
17
example/cypress/plugins/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
25
example/cypress/support/commands.js
Normal file
25
example/cypress/support/commands.js
Normal file
@ -0,0 +1,25 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
20
example/cypress/support/index.js
Normal file
20
example/cypress/support/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
13
example/messages/default.json
Normal file
13
example/messages/default.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": {
|
||||
"about": "About",
|
||||
"index": "Sapper project template!"
|
||||
},
|
||||
"about_this_site": "About this site",
|
||||
"about_content": ["This is the 'about' page. There's not much here."],
|
||||
"messages": {
|
||||
"success": "Great success!",
|
||||
"high_five": "High five",
|
||||
"try_editing": "Try editing this file (src/routes/index.svelte) to test live reloading."
|
||||
}
|
||||
}
|
13
example/messages/en-US.json
Normal file
13
example/messages/en-US.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": {
|
||||
"about": "About",
|
||||
"index": "Sapper project template!"
|
||||
},
|
||||
"about_this_site": "About this site",
|
||||
"about_content": ["This is the 'about' page. There's not much here."],
|
||||
"messages": {
|
||||
"success": "Great success!",
|
||||
"high_five": "High five",
|
||||
"try_editing": "Try editing this file (src/routes/index.svelte) to test live reloading."
|
||||
}
|
||||
}
|
13
example/messages/es-ES.json
Normal file
13
example/messages/es-ES.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": {
|
||||
"about": "Acerca de",
|
||||
"index": " Plantilla de proyecto Sapper!"
|
||||
},
|
||||
"about_this_site": " Acerca de este sitio",
|
||||
"about_content": ["Esta es la página 'acerca de'. No hay mucho aquí."],
|
||||
"messages": {
|
||||
"success": "Gran éxito!",
|
||||
"high_five": "Cinco altos",
|
||||
"try_editing": " Intente editar este archivo (src/routes/index.svelte) para probar la recarga en vivo."
|
||||
}
|
||||
}
|
13
example/messages/pt-BR.json
Normal file
13
example/messages/pt-BR.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": {
|
||||
"about": "Sobre",
|
||||
"index": "Modelo de projeto em Sapper!"
|
||||
},
|
||||
"about_this_site": "Sobre este site",
|
||||
"about_content": ["Esta é a página 'sobre'. Não há muito aqui."],
|
||||
"messages": {
|
||||
"success": "Suuuucesso!",
|
||||
"high_five": "Toca aqui",
|
||||
"try_editing": "Tente editar este arquivo (src/routes/index.svelte) para testar o recarregamento ao vivo."
|
||||
}
|
||||
}
|
2716
example/package-lock.json
generated
2716
example/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,25 +1,38 @@
|
||||
{
|
||||
"name": "svelte-app",
|
||||
"version": "1.0.0",
|
||||
"name": "TODO",
|
||||
"description": "TODO",
|
||||
"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": "sapper export --legacy",
|
||||
"start": "node __sapper__/build",
|
||||
"cy:run": "cypress run",
|
||||
"cy:open": "cypress open",
|
||||
"test": "run-p --race dev cy:run"
|
||||
},
|
||||
"dependencies": {
|
||||
"svelte-i18n": "^1.1.0"
|
||||
"compression": "^1.7.1",
|
||||
"polka": "next",
|
||||
"rollup-plugin-json": "^4.0.0",
|
||||
"sirv": "^0.4.0",
|
||||
"svelte-i18n": "^2.0.0-alpha.2"
|
||||
},
|
||||
"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-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,118 @@
|
||||
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({
|
||||
dev,
|
||||
hydratable: true,
|
||||
emitCss: true,
|
||||
}),
|
||||
resolve({
|
||||
browser: true,
|
||||
dedupe,
|
||||
}),
|
||||
commonjs(),
|
||||
json(),
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
|
||||
!dev &&
|
||||
terser({
|
||||
module: true,
|
||||
}),
|
||||
],
|
||||
|
||||
onwarn,
|
||||
},
|
||||
plugins: [
|
||||
svelte({
|
||||
// enable run-time checks when not in production
|
||||
dev: !production,
|
||||
// we'll extract any component CSS out into
|
||||
// a separate file — better for performance
|
||||
css: css => {
|
||||
css.write('public/bundle.css')
|
||||
},
|
||||
}),
|
||||
|
||||
// If you have external dependencies installed from
|
||||
// npm, you'll most likely need these plugins. In
|
||||
// some cases you'll need additional configuration —
|
||||
// consult the documentation for details:
|
||||
// https://github.com/rollup/rollup-plugin-commonjs
|
||||
resolve(),
|
||||
commonjs(),
|
||||
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(),
|
||||
],
|
||||
external: Object.keys(pkg.dependencies).concat(
|
||||
require('module').builtinModules ||
|
||||
Object.keys(process.binding('natives')),
|
||||
),
|
||||
|
||||
// Watch the `public` directory and refresh the
|
||||
// browser on changes when not in production
|
||||
!production && livereload('public'),
|
||||
onwarn,
|
||||
},
|
||||
|
||||
// If we're building for production (npm run build
|
||||
// instead of npm run dev), minify
|
||||
production && terser(),
|
||||
],
|
||||
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'),
|
||||
})
|
60
example/src/components/Nav.svelte
Normal file
60
example/src/components/Nav.svelte
Normal file
@ -0,0 +1,60 @@
|
||||
<script>
|
||||
export let segment;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
border-bottom: 1px solid rgba(255,62,0,0.1);
|
||||
font-weight: 300;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
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 {
|
||||
text-decoration: none;
|
||||
padding: 1em 0.5em;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a class:selected='{segment === undefined}' href='.'>home</a></li>
|
||||
<li><a class:selected='{segment === "about"}' href='about'>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'>blog</a></li>
|
||||
</ul>
|
||||
</nav>
|
@ -1,51 +1,7 @@
|
||||
import {
|
||||
locale,
|
||||
dictionary,
|
||||
getClientLocale,
|
||||
addCustomFormats,
|
||||
} from '../../src/index.js'
|
||||
import { dictionary } 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)
|
||||
'pt-BR': () => import('../messages/pt-BR.json'),
|
||||
'en-US': () => import('../messages/en-US.json'),
|
||||
'es-ES': () => import('../messages/es-ES.json'),
|
||||
})
|
||||
|
@ -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}
|
41
example/src/routes/_layout.svelte
Normal file
41
example/src/routes/_layout.svelte
Normal file
@ -0,0 +1,41 @@
|
||||
<script context="module">
|
||||
import { locale, getClientLocale } from 'svelte-i18n'
|
||||
|
||||
export async function preload() {
|
||||
return locale.set(getClientLocale({ default: 'en-US', navigator: true }))
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Nav from '../components/Nav.svelte'
|
||||
|
||||
const locales = {
|
||||
'pt-BR': 'Português',
|
||||
'en-US': 'English',
|
||||
'es-ES': 'Espanõl',
|
||||
}
|
||||
|
||||
export let segment
|
||||
</script>
|
||||
|
||||
<style>
|
||||
main {
|
||||
position: relative;
|
||||
max-width: 56em;
|
||||
background-color: white;
|
||||
padding: 2em;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
<Nav {segment} />
|
||||
|
||||
<main>
|
||||
<select bind:value={$locale}>
|
||||
{#each Object.entries(locales) as [locale, label]}
|
||||
<option value={locale}>{label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<slot />
|
||||
</main>
|
15
example/src/routes/about.svelte
Normal file
15
example/src/routes/about.svelte
Normal file
@ -0,0 +1,15 @@
|
||||
<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_('title.about', { default: 'About' })}</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>{$_('about_this_site', { default: 'About this site' })}</h1>
|
||||
|
||||
<p>
|
||||
{$_('about_content[0]', {
|
||||
default: "This is the 'about' page. There's not much here.",
|
||||
})}
|
||||
</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)
|
||||
})
|
82
example/src/service-worker.js
Normal file
82
example/src/service-worker.js
Normal file
@ -0,0 +1,82 @@
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
1762
example/yarn.lock
Normal file
1762
example/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
6964
package-lock.json
generated
6964
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
70
package.json
70
package.json
@ -1,8 +1,11 @@
|
||||
{
|
||||
"name": "svelte-i18n",
|
||||
"version": "1.1.2-beta",
|
||||
"version": "2.0.0-alpha.2",
|
||||
"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>",
|
||||
@ -14,66 +17,67 @@
|
||||
"localization",
|
||||
"translation"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 11.15.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
"pretest": "npm run build",
|
||||
"test": "jest --no-cache --verbose",
|
||||
"test": "jest --no-cache",
|
||||
"test:watch": "jest --no-cache --verbose --watchAll",
|
||||
"lint": "eslint \"src/**/*.js\"",
|
||||
"format": "prettier --loglevel silent --write \"src/**/*.js\" && eslint --fix \"src/**/*.js\"",
|
||||
"lint": "eslint \"src/**/*.ts\"",
|
||||
"format": "prettier --loglevel silent --write \"src/**/*.ts\" && eslint --fix \"src/**/*.ts\"",
|
||||
"prepublishOnly": "npm run format && npm run test && npm run build"
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"collectCoverage": true
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-kaisermann": "0.0.3",
|
||||
"intl": "^1.2.5",
|
||||
"jest": "^24.9.0",
|
||||
"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",
|
||||
"estree-walker": "^0.9.0",
|
||||
"intl-messageformat": "^7.5.2",
|
||||
"micro-memoize": "^4.0.8",
|
||||
"object-resolve-path": "^1.1.1"
|
||||
"object-resolve-path": "^1.1.1",
|
||||
"tiny-glob": "^0.2.6"
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,48 @@
|
||||
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
|
||||
|
||||
// const externals = new Set([
|
||||
// ...Object.keys(pkg.dependencies),
|
||||
// ...Object.keys(pkg.peerDependencies),
|
||||
// 'fs',
|
||||
// 'path',
|
||||
// ])
|
||||
|
||||
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()],
|
||||
},
|
||||
]
|
||||
|
220
src/cli/extract.ts
Normal file
220
src/cli/extract.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import {
|
||||
Node,
|
||||
ObjectExpression,
|
||||
Property,
|
||||
ImportDeclaration,
|
||||
ImportSpecifier,
|
||||
CallExpression,
|
||||
Identifier,
|
||||
} from 'estree'
|
||||
import resolvePath from 'object-resolve-path'
|
||||
import { walk } from 'estree-walker'
|
||||
import { Ast } from 'svelte/types/compiler/interfaces'
|
||||
import { parse } from 'svelte/compiler'
|
||||
|
||||
const LIB_NAME = 'svelte-i18n'
|
||||
const DEFINE_MESSAGES_METHOD_NAME = 'defineMessages'
|
||||
const FORMAT_METHOD_NAMES = new Set(['format', '_'])
|
||||
|
||||
interface Message {
|
||||
node: Node
|
||||
meta: {
|
||||
id?: string
|
||||
default?: string
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
const isNumberString = (n: string) => !Number.isNaN(parseInt(n))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function isFormatCall(node: Node, imports: Set<string>) {
|
||||
if (node.type !== 'CallExpression') return false
|
||||
|
||||
let identifier: Identifier
|
||||
if (node.callee.type === 'MemberExpression') {
|
||||
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[]
|
||||
}
|
||||
|
||||
function getObjFromExpression(exprNode: Node | ObjectExpression) {
|
||||
if (exprNode.type !== 'ObjectExpression') return null
|
||||
return exprNode.properties.reduce<Message>(
|
||||
(acc, prop: Property) => {
|
||||
// we only want primitives
|
||||
if (prop.value.type !== 'Literal') return acc
|
||||
if (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: {} },
|
||||
)
|
||||
}
|
||||
|
||||
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 formatCallsWalker(node: Node) {
|
||||
if (isFormatCall(node, imports)) {
|
||||
calls.push(node as CallExpression)
|
||||
this.skip()
|
||||
}
|
||||
}
|
||||
walk(ast.instance as any, { enter: formatCallsWalker })
|
||||
walk(ast.html as any, { enter: formatCallsWalker })
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if (pathNode.type !== 'Literal' || typeof pathNode.value !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (options && options.type === 'ObjectExpression') {
|
||||
const messageObj = getObjFromExpression(options)
|
||||
messageObj.meta.id = pathNode.value
|
||||
return messageObj
|
||||
}
|
||||
|
||||
return {
|
||||
node: pathNode,
|
||||
meta: { id: pathNode.value },
|
||||
}
|
||||
}),
|
||||
].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 resolvePath(accumulator, message.meta.id) !== 'undefined'
|
||||
) {
|
||||
return
|
||||
}
|
||||
deepSet(accumulator, message.meta.id, defaultValue)
|
||||
}
|
||||
})
|
||||
return accumulator
|
||||
}
|
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)
|
80
src/client/formatters.ts
Normal file
80
src/client/formatters.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import IntlMessageFormat, { Formats } from 'intl-messageformat'
|
||||
import memoize from 'micro-memoize'
|
||||
|
||||
import { MemoizedIntlFormatter } from './types'
|
||||
|
||||
export const customFormats: any = {
|
||||
number: {
|
||||
scientific: { notation: 'scientific' },
|
||||
engineering: { notation: 'engineering' },
|
||||
compactLong: { notation: 'compact', compactDisplay: 'long' },
|
||||
compactShort: { notation: 'compact', compactDisplay: 'short' },
|
||||
},
|
||||
date: {},
|
||||
time: {},
|
||||
}
|
||||
|
||||
export function addCustomFormats(formats: Partial<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)
|
||||
}
|
||||
|
||||
const getIntlFormatterOptions = (
|
||||
type: 'time' | 'number' | 'date',
|
||||
name: string,
|
||||
): any => {
|
||||
if (type in customFormats && name in customFormats[type]) {
|
||||
return customFormats[type][name]
|
||||
}
|
||||
|
||||
if (
|
||||
type in IntlMessageFormat.formats &&
|
||||
name in IntlMessageFormat.formats[type]
|
||||
) {
|
||||
return (IntlMessageFormat.formats[type] as any)[name]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const getNumberFormatter: MemoizedIntlFormatter<
|
||||
Intl.NumberFormat,
|
||||
Intl.NumberFormatOptions
|
||||
> = memoize((locale, options = {}) => {
|
||||
if (options.locale) locale = options.locale
|
||||
if (options.format) {
|
||||
const format = getIntlFormatterOptions('number', options.format)
|
||||
if (format) options = format
|
||||
}
|
||||
return new Intl.NumberFormat(locale, options)
|
||||
})
|
||||
|
||||
export const getDateFormatter: MemoizedIntlFormatter<
|
||||
Intl.DateTimeFormat,
|
||||
Intl.DateTimeFormatOptions
|
||||
> = memoize((locale, options = { format: 'short' }) => {
|
||||
if (options.locale) locale = options.locale
|
||||
if (options.format) {
|
||||
const format = getIntlFormatterOptions('date', options.format)
|
||||
if (format) options = format
|
||||
}
|
||||
return new Intl.DateTimeFormat(locale, options)
|
||||
})
|
||||
|
||||
export const getTimeFormatter: MemoizedIntlFormatter<
|
||||
Intl.DateTimeFormat,
|
||||
Intl.DateTimeFormatOptions
|
||||
> = memoize((locale, options = { format: 'short' }) => {
|
||||
if (options.locale) locale = options.locale
|
||||
if (options.format) {
|
||||
const format = getIntlFormatterOptions('time', options.format)
|
||||
if (format) options = format
|
||||
}
|
||||
return new Intl.DateTimeFormat(locale, options)
|
||||
})
|
||||
|
||||
export const getMessageFormatter = memoize(
|
||||
(message: string, locale: string) =>
|
||||
new IntlMessageFormat(message, locale, customFormats),
|
||||
)
|
118
src/client/index.ts
Normal file
118
src/client/index.ts
Normal file
@ -0,0 +1,118 @@
|
||||
// todo maybe locale can be a promise so we can await for it on the template?
|
||||
|
||||
import { writable, derived } from 'svelte/store'
|
||||
import resolvePath from 'object-resolve-path'
|
||||
import memoize from 'micro-memoize'
|
||||
|
||||
import { capital, title, upper, lower, getClientLocale } from './utils'
|
||||
import { MessageObject, Formatter } from './types'
|
||||
import {
|
||||
getMessageFormatter,
|
||||
getDateFormatter,
|
||||
getNumberFormatter,
|
||||
getTimeFormatter,
|
||||
} from './formatters'
|
||||
|
||||
let currentLocale: string
|
||||
let currentDictionary: Record<string, any>
|
||||
|
||||
function getAvailableLocale(locale: string) {
|
||||
if (currentDictionary[locale]) {
|
||||
if (typeof currentDictionary[locale] === 'function') {
|
||||
return { locale, loader: currentDictionary[locale] }
|
||||
}
|
||||
return { locale }
|
||||
}
|
||||
|
||||
locale = locale.split('-').shift() //
|
||||
if (currentDictionary[locale]) {
|
||||
if (typeof currentDictionary[locale] === 'function') {
|
||||
return { locale, loader: currentDictionary[locale] }
|
||||
}
|
||||
return { locale }
|
||||
}
|
||||
|
||||
return { locale: null }
|
||||
}
|
||||
|
||||
const lookupMessage = memoize((path: string, locale: string) => {
|
||||
return (
|
||||
(currentDictionary[locale] as any)[path] ||
|
||||
resolvePath(currentDictionary[locale], path)
|
||||
)
|
||||
})
|
||||
|
||||
const formatMessage: Formatter = (id, options = {}) => {
|
||||
if (typeof id === 'object') {
|
||||
options = id as MessageObject
|
||||
id = options.id
|
||||
}
|
||||
|
||||
const { values, locale = currentLocale, default: defaultValue } = options
|
||||
const message = lookupMessage(id, locale)
|
||||
|
||||
if (!message) {
|
||||
console.warn(
|
||||
`[svelte-i18n] The message "${id}" was not found in the locale "${locale}".`,
|
||||
)
|
||||
if (defaultValue != null) return defaultValue
|
||||
return id
|
||||
}
|
||||
|
||||
if (!values) return message
|
||||
|
||||
return getMessageFormatter(message, locale).format(values)
|
||||
}
|
||||
|
||||
formatMessage.time = (t, options) =>
|
||||
getTimeFormatter(currentLocale, options).format(t)
|
||||
formatMessage.date = (d, options) =>
|
||||
getDateFormatter(currentLocale, options).format(d)
|
||||
formatMessage.number = (n, options) =>
|
||||
getNumberFormatter(currentLocale, options).format(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: any) => (currentDictionary = newDictionary),
|
||||
)
|
||||
|
||||
const locale = writable(null)
|
||||
const localeSet = locale.set
|
||||
locale.set = (newLocale: string) => {
|
||||
const { locale, loader } = getAvailableLocale(newLocale)
|
||||
if (typeof loader === 'function') {
|
||||
return loader()
|
||||
.then((dict: any) => {
|
||||
currentDictionary[locale] = dict.default || dict
|
||||
if (locale) return localeSet(locale)
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
throw e
|
||||
})
|
||||
}
|
||||
|
||||
if (locale) return localeSet(locale)
|
||||
|
||||
throw Error(`[svelte-i18n] Locale "${newLocale}" not found.`)
|
||||
}
|
||||
locale.update = (fn: (locale: string) => string) => localeSet(fn(currentLocale))
|
||||
locale.subscribe((newLocale: string) => (currentLocale = newLocale))
|
||||
|
||||
const format = derived([locale, dictionary], () => formatMessage)
|
||||
// defineMessages allow us to define and extract dynamic message ids
|
||||
const defineMessages = (i: Record<string, MessageObject>) => i
|
||||
|
||||
export { customFormats, addCustomFormats } from './formatters'
|
||||
export {
|
||||
locale,
|
||||
dictionary,
|
||||
getClientLocale,
|
||||
defineMessages,
|
||||
format as _,
|
||||
format,
|
||||
}
|
1
src/client/modules.d.ts
vendored
Normal file
1
src/client/modules.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'object-resolve-path'
|
39
src/client/types.ts
Normal file
39
src/client/types.ts
Normal file
@ -0,0 +1,39 @@
|
||||
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> {
|
||||
(locale: string, 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
|
||||
}
|
51
src/client/utils.ts
Normal file
51
src/client/utils.ts
Normal file
@ -0,0 +1,51 @@
|
||||
export const capital = (str: string) =>
|
||||
str.replace(/(^|\s)\S/, l => l.toLocaleUpperCase())
|
||||
export const title = (str: string) =>
|
||||
str.replace(/(^|\s)\S/g, l => l.toLocaleUpperCase())
|
||||
export const upper = (str: string) => str.toLocaleUpperCase()
|
||||
export const lower = (str: string) => str.toLocaleLowerCase()
|
||||
|
||||
export const getClientLocale = ({
|
||||
navigator,
|
||||
hash,
|
||||
search,
|
||||
default: defaultLocale,
|
||||
fallback = defaultLocale,
|
||||
}: {
|
||||
navigator?: boolean
|
||||
hash?: string
|
||||
search?: string
|
||||
fallback?: string
|
||||
default?: string
|
||||
}) => {
|
||||
let locale
|
||||
|
||||
const getFromURL = (urlPart: string, key: string) => {
|
||||
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) {
|
||||
locale = getFromURL(window.location.search, search)
|
||||
}
|
||||
|
||||
if (hash && !locale) {
|
||||
locale = getFromURL(window.location.hash, hash)
|
||||
}
|
||||
}
|
||||
|
||||
return locale || defaultLocale || fallback
|
||||
}
|
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
|
||||
}
|
227
test/cli/extraction.test.ts
Normal file
227
test/cli/extraction.test.ts
Normal file
@ -0,0 +1,227 @@
|
||||
// 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', () => {
|
||||
it('should return nothing if there are no imports from the library', () => {
|
||||
const ast = parse(`<script>const $_ = () => 0; $_();</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should collect 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' })
|
||||
})
|
||||
|
||||
it('should collect all format calls with renamed imports', () => {
|
||||
const ast = parse(`<script>
|
||||
import { format as _x, _ as intl } from 'svelte-i18n'
|
||||
$_x('foo')
|
||||
$intl({ id: 'bar' })
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(2)
|
||||
expect(calls[0]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
|
||||
})
|
||||
|
||||
it('should collect all format utility calls', () => {
|
||||
const ast = parse(`<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
$_.title('foo')
|
||||
$_.capitalize({ id: 'bar' })
|
||||
$_.number(10000)
|
||||
</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' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('collecting message definitions', () => {
|
||||
it('should return 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)
|
||||
})
|
||||
|
||||
it('should get 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', () => {
|
||||
it('should collect 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', () => {
|
||||
it('should return 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: '' })
|
||||
})
|
||||
|
||||
it('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: ['', '', ''],
|
||||
})
|
||||
})
|
||||
|
||||
it('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]': '',
|
||||
})
|
||||
})
|
||||
|
||||
it('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: ['', '', ''],
|
||||
})
|
||||
})
|
||||
|
||||
it('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': '',
|
||||
})
|
||||
})
|
||||
})
|
@ -1,3 +1,4 @@
|
||||
import { Formatter } from '../../src/client/types'
|
||||
import {
|
||||
dictionary,
|
||||
locale,
|
||||
@ -5,89 +6,101 @@ import {
|
||||
getClientLocale,
|
||||
addCustomFormats,
|
||||
customFormats,
|
||||
} from '../src/index.js'
|
||||
} from '../../src/client'
|
||||
|
||||
let _
|
||||
let currentLocale
|
||||
global.Intl = require('intl')
|
||||
|
||||
let _: Formatter
|
||||
let currentLocale: string
|
||||
|
||||
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}}',
|
||||
},
|
||||
pt: require('../fixtures/pt.json'),
|
||||
en: require('../fixtures/en.json'),
|
||||
}
|
||||
|
||||
format.subscribe(formatFn => {
|
||||
_ = formatFn
|
||||
})
|
||||
dictionary.set(dict)
|
||||
locale.subscribe(l => (currentLocale = l))
|
||||
locale.subscribe((l: string) => {
|
||||
currentLocale = l
|
||||
})
|
||||
locale.set('pt')
|
||||
|
||||
it('should change locale', () => {
|
||||
locale.set('pt')
|
||||
expect(currentLocale).toBe('pt')
|
||||
locale.set('en')
|
||||
expect(currentLocale).toBe('en')
|
||||
describe('locale', () => {
|
||||
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 existing locale', () => {
|
||||
locale.set('pt-BR')
|
||||
expect(currentLocale).toBe('pt')
|
||||
|
||||
locale.set('en-US')
|
||||
expect(currentLocale).toBe('en')
|
||||
describe('dictionary', () => {
|
||||
// todo test this better
|
||||
it('allows to dynamically import a dictionary', async () => {
|
||||
dictionary.update((dict: any) => {
|
||||
dict.es = () => import('../fixtures/es.json')
|
||||
return dict
|
||||
})
|
||||
await locale.set('es')
|
||||
expect(currentLocale).toBe('es')
|
||||
})
|
||||
})
|
||||
|
||||
it("should throw an error if locale doesn't exist", () => {
|
||||
expect(() => locale.set('FOO')).toThrow()
|
||||
})
|
||||
describe('formatting', () => {
|
||||
it('should fallback to message id if id is not found', () => {
|
||||
locale.set('en')
|
||||
expect(_('batatinha.quente')).toBe('batatinha.quente')
|
||||
})
|
||||
|
||||
it('should fallback to message id if id is not found', () => {
|
||||
locale.set('en')
|
||||
expect(_('batatinha')).toBe('batatinha')
|
||||
})
|
||||
it('should fallback to default value if id is not found', () => {
|
||||
locale.set('en')
|
||||
expect(_('batatinha.quente', { default: 'Hot Potato' })).toBe('Hot Potato')
|
||||
})
|
||||
|
||||
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 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 accept single object with id prop as the message path', () => {
|
||||
locale.set('pt')
|
||||
expect(_({ id: 'switch.lang' })).toBe('Trocar idioma')
|
||||
locale.set('en')
|
||||
expect(_({ id: 'switch.lang' })).toBe('Switch language')
|
||||
})
|
||||
|
||||
it('should interpolate message with variables', () => {
|
||||
expect(_('greeting.message', { values: { name: 'Chris' } })).toBe(
|
||||
'Hello Chris, how are you?',
|
||||
)
|
||||
})
|
||||
it('should translate to passed locale', () => {
|
||||
expect(_({ id: 'switch.lang', locale: 'pt' })).toBe('Trocar idioma')
|
||||
expect(_('switch.lang', { locale: 'en' })).toBe('Switch language')
|
||||
})
|
||||
|
||||
it('should interpolate message with variables according to passed locale', () => {
|
||||
expect(
|
||||
_('greeting.message', { values: { name: 'Chris' }, locale: 'pt' }),
|
||||
).toBe('Olá Chris, como vai?')
|
||||
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', () => {
|
||||
@ -97,7 +110,7 @@ describe('utilities', () => {
|
||||
window.location = {
|
||||
hash: '',
|
||||
search: '',
|
||||
}
|
||||
} as any
|
||||
})
|
||||
|
||||
it('should get the locale based on the passed hash parameter', () => {
|
||||
@ -150,6 +163,9 @@ describe('utilities', () => {
|
||||
locale.set('en')
|
||||
expect(_.time(date)).toBe('11:45 PM')
|
||||
expect(_.time(date, { format: 'medium' })).toBe('11:45:00 PM')
|
||||
expect(_.time(date, { format: 'medium', locale: 'pt-BR' })).toBe(
|
||||
'23:45:00',
|
||||
)
|
||||
})
|
||||
|
||||
it('should format a date value', () => {
|
||||
@ -195,12 +211,25 @@ describe('custom formats', () => {
|
||||
usd: { style: 'currency', currency: 'USD' },
|
||||
brl: { style: 'currency', currency: 'BRL' },
|
||||
},
|
||||
date: {
|
||||
customDate: { year: 'numeric', era: 'short' },
|
||||
},
|
||||
time: {
|
||||
customTime: { hour: '2-digit', month: 'narrow' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(_.number(123123123, { format: 'usd' })).toContain('US$')
|
||||
expect(_.number(123123123, { format: 'usd' })).toContain('123,123,123.00')
|
||||
locale.set('en-US')
|
||||
|
||||
expect(_.number(123123123, { format: 'brl' })).toContain('R$')
|
||||
expect(_.number(123123123, { format: 'brl' })).toContain('123,123,123.00')
|
||||
expect(_.number(123123123, { format: 'usd' })).toContain('$123,123,123.00')
|
||||
expect(_.number(123123123, { format: 'brl' })).toContain('R$123,123,123.00')
|
||||
|
||||
expect(_.date(new Date(2019, 0, 1), { format: 'customDate' })).toEqual(
|
||||
'2019 AD',
|
||||
)
|
||||
|
||||
expect(
|
||||
_.time(new Date(2019, 0, 1, 2, 0, 0), { format: 'customTime' }),
|
||||
).toEqual('Jan, 02')
|
||||
})
|
||||
})
|
10
test/fixtures/en.json
vendored
Normal file
10
test/fixtures/en.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"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}}"
|
||||
}
|
10
test/fixtures/es.json
vendored
Normal file
10
test/fixtures/es.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"hi": "hola yo",
|
||||
"switch.lang": "Cambiar de idioma",
|
||||
"greeting": {
|
||||
"ask": "Por favor escriba su nombre",
|
||||
"message": "Hola {name}, cómo estás?"
|
||||
},
|
||||
"photos": "Tienes {n, plural, =0 {0 fotos.} =1 {una foto.} other {# fotos.}}",
|
||||
"cats": "Yo tengo {n, number} {n,plural,one{gato}other{gatos}}"
|
||||
}
|
10
test/fixtures/pt.json
vendored
Normal file
10
test/fixtures/pt.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"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}}"
|
||||
}
|
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(),
|
||||
}
|
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