Initial commit

This commit is contained in:
Christian Kaisermann 2020-12-03 10:41:29 -03:00
parent abbd396128
commit 639177b6e1
63 changed files with 282 additions and 4997 deletions

View File

@ -1,3 +0,0 @@
{
"presets": ["@babel/preset-env"]
}

View File

@ -1,20 +1,3 @@
{
"extends": ["@kiwi"],
"env": {
"browser": true,
"jest": true
},
"rules": {
"@typescript-eslint/no-explicit-any": "off"
},
"overrides": [
{
"files": ["test/**/*"],
"rules": {
"global-require": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-require-imports": "off"
}
}
]
"extends": ["vtex"]
}

View File

@ -1,27 +0,0 @@
---
Before filing an issue we'd appreciate it if you could take a moment to ensure
there isn't already an open issue or pull-request.
---
If there's an existing issue, please add a :+1: reaction to the description of
the issue. One way we prioritize issues is by the number of :+1: reactions on
their descriptions. Please DO NOT add `+1` or :+1: comments.
### Feature requests and proposals
We're excited to hear how we can make Svelte better. Please add as much detail
as you can on your use case.
### Bugs
If you're filing an issue about a bug please include as much information
as you can including the following.
- Your browser and the version: (e.x. Chrome 52.1, Firefox 48.0, IE 10)
- Your operating system: (e.x. OS X 10, Windows XP, etc)
- `svelte-i18n` version (Please check you can reproduce the issue with the latest release!)
- Whether your project uses Webpack or Rollup
- _Repeatable steps to reproduce the issue_
## Thanks for being part of Svelte!

View File

@ -1,47 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'Bug'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Logs**
Please include browser console and server logs around the time this bug occurred.
**To Reproduce**
To help us help you, if you've found a bug please consider the following:
- Please create a small repo that illustrates the problem.
- Reproductions should be small, self-contained, correct examples http://sscce.org.
Occasionally, this won't be possible, and that's fine we still appreciate you raising the issue. But please understand that `svelte-preprocess` is run by unpaid volunteers in their free time, and issues that follow these instructions will get fixed faster.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Stacktraces**
If you have a stack trace to include, we recommend putting inside a `<details>` block for the sake of the thread's readability:
<details>
<summary>Stack trace</summary>
Stack trace goes here...
</details>
**Information about your project:**
- Your browser and the version: (e.x. Chrome 52.1, Firefox 48.0, IE 10)
- Your operating system: (e.x. OS X 10, Ubuntu Linux 19.10, Windows XP, etc)
- `svelte-i18n` version (Please check you can reproduce the issue with the latest release!)
- Whether your project uses Webpack or Rollup
**Additional context**
Add any other context about the problem here.

View File

@ -1,22 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'New Feature'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. For example: I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**How important is this feature to you?**
Note: the more honest and specific you are here the more we will take you seriously.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,9 +0,0 @@
### Before submitting the PR, please make sure you do the following
- [ ] It's really useful if your PR relates to an outstanding issue, so please reference it in your PR, or create an explanatory one for discussion. In many cases features are absent for a reason.
- [ ] This message body should clearly illustrate what problems it solves. If there are related issues, remember to reference them.
- [ ] Ideally, include a test that fails without this PR but passes with it. PRs will only be merged once they pass CI. (Remember to `npm run lint`!)
### Tests
- [ ] Run the tests tests with `npm test` or `yarn test`

1
.nvmrc
View File

@ -1 +0,0 @@
12.0.0

View File

@ -1 +1 @@
"@kiwi/prettier-config"
"@vtex/prettier-config"

View File

@ -1,275 +1,12 @@
# [3.3.0](https://github.com/kaisermann/svelte-i18n/compare/v3.2.7...v3.3.0) (2020-11-24)
# Changelog
All notable changes to this project will be documented in this file.
### Features
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
* 🎸 add $json method to get raw dictionary values ([52400b5](https://github.com/kaisermann/svelte-i18n/commit/52400b5c51213b45270da101aab6e8ae2bda024c)), closes [#109](https://github.com/kaisermann/svelte-i18n/issues/109) [#83](https://github.com/kaisermann/svelte-i18n/issues/83)
## [Unreleased]
### Added
## [3.2.7](https://github.com/kaisermann/svelte-i18n/compare/v3.2.6...v3.2.7) (2020-11-23)
### Bug Fixes
* 🐛 message formatter type ([40e6dbe](https://github.com/kaisermann/svelte-i18n/commit/40e6dbe8f7490c57b70dc96f525530f046abcda1)), closes [#109](https://github.com/kaisermann/svelte-i18n/issues/109)
## [3.2.6](https://github.com/kaisermann/svelte-i18n/compare/v3.2.5...v3.2.6) (2020-11-20)
### Changed
- Don't minify CLI for better debugging.
## [3.2.5](https://github.com/kaisermann/svelte-i18n/compare/v3.2.4...v3.2.5) (2020-11-08)
### Bug Fixes
* 🐛 regression of flat keys separated by dot ([d87caef](https://github.com/kaisermann/svelte-i18n/commit/d87caef0600be10727222a2cfbe7ff391fb8ff4c))
## [3.2.4](https://github.com/kaisermann/svelte-i18n/compare/v3.2.3...v3.2.4) (2020-11-07)
### Bug Fixes
* 🐛 possible interpolation value types ([0caaead](https://github.com/kaisermann/svelte-i18n/commit/0caaead4789a62daef4ea73361506a9f135b80e7))
## [3.2.3](https://github.com/kaisermann/svelte-i18n/compare/v3.2.2...v3.2.3) (2020-11-06)
### Bug Fixes
* 🐛 prevent extraction of non-deterministic message ids ([9b6adb6](https://github.com/kaisermann/svelte-i18n/commit/9b6adb6538329ecba1e32e2acdca2c4761c1d99c)), closes [#89](https://github.com/kaisermann/svelte-i18n/issues/89)
## [3.2.2](https://github.com/kaisermann/svelte-i18n/compare/v3.2.1...v3.2.2) (2020-11-05)
### Bug Fixes
* 🐛 update estree-walker and intl-messageformat ([44e71d7](https://github.com/kaisermann/svelte-i18n/commit/44e71d72aba1cb3263ea009932df27dd39d86cb3))
## [3.2.1](https://github.com/kaisermann/svelte-i18n/compare/v3.2.0...v3.2.1) (2020-11-05)
### Bug Fixes
* 🐛 interpolate values for default values and missing keys ([330f20b](https://github.com/kaisermann/svelte-i18n/commit/330f20b7bd55af1e565de7ba0449a03cc24738aa)), closes [#101](https://github.com/kaisermann/svelte-i18n/issues/101)
# [3.2.0](https://github.com/kaisermann/svelte-i18n/compare/v3.1.0...v3.2.0) (2020-11-05)
### Features
* 🎸 Support getting deep localized objects/arrays ([ff54136](https://github.com/kaisermann/svelte-i18n/commit/ff541367f85a28ad69bb34beb145ce404b1a9240)), closes [#83](https://github.com/kaisermann/svelte-i18n/issues/83)
# [3.1.0](https://github.com/kaisermann/svelte-i18n/compare/v3.0.4...v3.1.0) (2020-09-20)
### Bug Fixes
* export correct configuration type ([68e8c51](https://github.com/kaisermann/svelte-i18n/commit/68e8c51a636910bbe0619350b7d8ad6fabe13c7d))
## [3.0.4](https://github.com/kaisermann/svelte-i18n/compare/v3.0.3...v3.0.4) (2020-05-31)
### Bug Fixes
* 🐛 also wait for loaders added while loading ([e560514](https://github.com/kaisermann/svelte-i18n/commit/e560514b1d957b2c4fc9b1a4f412ab93cf31d21a))
## [3.0.3](https://github.com/kaisermann/svelte-i18n/compare/v3.0.2...v3.0.3) (2020-03-29)
### Bug Fixes
* 🐛 prevent server from breaking on locale.set ([07ef1da](https://github.com/kaisermann/svelte-i18n/commit/07ef1da6d5177854b4707d5f038f5a14562e6bf5)), closes [#55](https://github.com/kaisermann/svelte-i18n/issues/55)
## [3.0.2](https://github.com/kaisermann/svelte-i18n/compare/v3.0.1...v3.0.2) (2020-03-06)
### Bug Fixes
* 🐛 ignore loadingDelay for the initialLocale ([9d82a98](https://github.com/kaisermann/svelte-i18n/commit/9d82a98e8d6ecf25dca12cce88183502e11133fe)), closes [#53](https://github.com/kaisermann/svelte-i18n/issues/53) [#53](https://github.com/kaisermann/svelte-i18n/issues/53)
## [3.0.1](https://github.com/kaisermann/svelte-i18n/compare/v2.3.1...v3.0.1) (2020-02-03)
### Features
* 🎸 add runtime typings ([7bf47d8](https://github.com/kaisermann/svelte-i18n/commit/7bf47d879006ffeec51ec112f20c74c72abe87ff)), closes [#43](https://github.com/kaisermann/svelte-i18n/issues/43)
* 🎸 make date,time and number formatters tree-shakeable ([6526245](https://github.com/kaisermann/svelte-i18n/commit/6526245bf9d40d25af14ec1e7acb34772a9f3f0e))
* 🎸 make getClientLocale tree-shakeable ([31b556b](https://github.com/kaisermann/svelte-i18n/commit/31b556bc3f77bc5b581541976a82f898a398c01a))
### BREAKING CHANGES
* It's now needed to explicitly import the `getClientLocale` method to use
its heuristics when setting the initial locale. This makes the method
and its helpers to be tree-shakeable.
```js
import { init, getClientLocale } from 'svelte-i18n'
init({
initialLocale: getClientLocale({ ... })
})
```
* Changes completely the API. Now, to format a number, date or time, the
developer must explicitly import the formatter store:
`import { time, date, number } from 'svelte-i18n'`
# [3.0.0](https://github.com/kaisermann/svelte-i18n/compare/v2.3.1...v3.0.0) (2020-02-03)
### Features
* 🎸 add runtime typings ([90bf171](https://github.com/kaisermann/svelte-i18n/commit/90bf171139ad6b55faa0e36b3d28d317de538985)), closes [#43](https://github.com/kaisermann/svelte-i18n/issues/43)
* 🎸 make date,time and number formatters tree-shakeable ([fb82a40](https://github.com/kaisermann/svelte-i18n/commit/fb82a400f349d8d997c1d14f8d16b1d5c8da7f3e))
* 🎸 make getClientLocale tree-shakeable ([4881acb](https://github.com/kaisermann/svelte-i18n/commit/4881acb7b3a9aacd64b0f00f3b85fd736aa53316))
### BREAKING CHANGES
* It's now needed to explicitly import the `getClientLocale` method to use
its heuristics when setting the initial locale. This makes the method
and its helpers to be tree-shakeable.
```js
import { init, getClientLocale } from 'svelte-i18n'
init({
initialLocale: getClientLocale({ ... })
})
```
* Changes completely the API. Now, to format a number, date or time, the
developer must explicitly import the formatter store:
`import { time, date, number } from 'svelte-i18n'`
## [2.3.1](https://github.com/kaisermann/svelte-i18n/compare/v2.3.0...v2.3.1) (2020-01-29)
### Bug Fixes
* 🐛 types from v3 branch leaking to master branch :shrug: ([88f7762](https://github.com/kaisermann/svelte-i18n/commit/88f7762e96c4eae963722bdedf601afbce4b2f17))
* memoizing of formatters when no locale is given. ([#47](https://github.com/kaisermann/svelte-i18n/issues/47)) ([27871f9](https://github.com/kaisermann/svelte-i18n/commit/27871f9775a96e0a2627a143635d4f4750b9f945))
# [2.3.0](https://github.com/kaisermann/svelte-i18n/compare/v2.2.4...v2.3.0) (2020-01-23)
### Features
* 🎸 add runtime typings ([dadeaa2](https://github.com/kaisermann/svelte-i18n/commit/dadeaa2e7fa0d0447135f76a5c70273238fc1da0)), closes [#43](https://github.com/kaisermann/svelte-i18n/issues/43)
## [2.2.4](https://github.com/kaisermann/svelte-i18n/compare/v2.2.2...v2.2.4) (2020-01-21)
## [2.2.3](https://github.com/kaisermann/svelte-i18n/compare/v2.2.2...v2.2.3) (2020-01-15)
### Refactor
* 💡 remove deepmerge and dlv dependencies ([270aefa](https://github.com/kaisermann/svelte-i18n/commit/270aefa1998d89215d8bdd1f813bdb9c690a5a2c))
## [2.2.2](https://github.com/kaisermann/svelte-i18n/compare/v2.2.0...v2.2.2) (2020-01-14)
### Bug Fixes
* 🐛 lookup message not caching correctly ([bb8c68f](https://github.com/kaisermann/svelte-i18n/commit/bb8c68f2eb7bbe658a40dc528b471ffadd5f92df))
* 🐛 mjs causing an elusive bug in webpack module resolution ([b2dc782](https://github.com/kaisermann/svelte-i18n/commit/b2dc7828c55b23be05adb0791816cc7bc9910af2)), closes [#36](https://github.com/kaisermann/svelte-i18n/issues/36)
## [2.2.1](https://github.com/kaisermann/svelte-i18n/compare/v2.2.0...v2.2.1) (2020-01-08)
### Bug Fixes
* 🐛 lookup message not caching correctly ([b9b6fa4](https://github.com/kaisermann/svelte-i18n/commit/b9b6fa41ffd99b89fc117c44a5bc636335c63632))
# [2.2.0](https://github.com/kaisermann/svelte-i18n/compare/v2.1.1...v2.2.0) (2020-01-07)
### Bug Fixes
* 🐛 make message formatter default to current locale ([0c57b9b](https://github.com/kaisermann/svelte-i18n/commit/0c57b9b568ba60216c4c96931da19dea97d998c4))
### Features
* add low level API to get access to the formatters ([#31](https://github.com/kaisermann/svelte-i18n/issues/31)) ([86cca99](https://github.com/kaisermann/svelte-i18n/commit/86cca992515809b1767d648293d395562dc2946a))
## [2.1.1](https://github.com/kaisermann/svelte-i18n/compare/v2.1.0...v2.1.1) (2019-12-02)
### Bug Fixes
- 🐛 fix conflict artifacts ([feb7ab9](https://github.com/kaisermann/svelte-i18n/commit/feb7ab9deadc97041e2d8a3364137f1fa13ed89b))
# [2.1.0](https://github.com/kaisermann/svelte-i18n/compare/v2.1.0-alpha.2...v2.1.0) (2019-11-30)
### Bug Fixes
- 🐛 allow to wait for initial locale load ([0b7f61c](https://github.com/kaisermann/svelte-i18n/commit/0b7f61c49a1c3206bbb5d9c77dfb5819a85d4bb5))
- 🐛 fallback behaviour and simplify API contact points ([64e69eb](https://github.com/kaisermann/svelte-i18n/commit/64e69eb3c0f62754570429a87450ff53eb29973a))
- 🐛 consider generic locales when registering loaders ([1b0138c](https://github.com/kaisermann/svelte-i18n/commit/1b0138c3f3458c4d8f0b30b4550652e8e0317fc7))
- 🐛 flush use the same promise if it wasn't resolved yet ([66972d4](https://github.com/kaisermann/svelte-i18n/commit/66972d4b1536b53d33c7974eb0fc059c0d0cc46c))
- client locale parameters typo ([#11](https://github.com/kaisermann/svelte-i18n/issues/11)) ([d1adf4c](https://github.com/kaisermann/svelte-i18n/commit/d1adf4c00a48ed679ae34a2bffc8ca9d709a2d5c))
### Features
- 🎸 add warnOnMissingMessages ([efbe793](https://github.com/kaisermann/svelte-i18n/commit/efbe793a0f3656b27d050886d85e06e9327ea681))
- 🐛 fallback behaviour and simplify API contact points ([6e0df2f](https://github.com/kaisermann/svelte-i18n/commit/6e0df2fb25e1bf9038eb4252ba993541a7fa2b4a)
- 🎸 `addMessagesTo` method ([d6b8664](https://github.com/kaisermann/svelte-i18n/commit/d6b8664009d738870aa3f0a4bd80e96abf6e6e59))
- 🎸 add \$loading indicator store ([bd2b350](https://github.com/kaisermann/svelte-i18n/commit/bd2b3501e9caa2e73f64835fedf93dc8939d41de))
- 🎸 add custom formats support ([d483244](https://github.com/kaisermann/svelte-i18n/commit/d483244a9f2bb5ba63ef8be95f0e87030b5cbc7e))
- 🎸 add pathname and hostname pattern matching ([b19b690](https://github.com/kaisermann/svelte-i18n/commit/b19b69050e252120016d47540e108f6eea193c37))
- 🎸 add preloadLocale method ([0a0e4b3](https://github.com/kaisermann/svelte-i18n/commit/0a0e4b3bab74499d684c86e17c949160762ae19b))
- 🎸 add waitInitialLocale helper ([6ee28e7](https://github.com/kaisermann/svelte-i18n/commit/6ee28e7d279c62060e834699714685567b6ab67c))
- 🎸 also look for message in generic locale ([e5d7b84](https://github.com/kaisermann/svelte-i18n/commit/e5d7b84241bd7e3fdd833e82dd8a9a8f251f023c)), closes [#19](https://github.com/kaisermann/svelte-i18n/issues/19)
- 🎸 export a store listing all locales available ([f58a20b](https://github.com/kaisermann/svelte-i18n/commit/f58a20b21eb58f891b3f9912cb6fff11eb329083))
- 🎸 locale change automatically updates the document lang ([64c8e55](https://github.com/kaisermann/svelte-i18n/commit/64c8e55f80636a1185a1797fe486b4189ff56944))
### Performance Improvements
- ⚡️ delay the \$loading state change for quick loadings ([6573f51](https://github.com/kaisermann/svelte-i18n/commit/6573f51e9b817db0c77f158945572f4ba14c71fc))
### BREAKING CHANGES
- This PR modifies the formatter method arguments.
- Initial release.

View File

@ -1,7 +0,0 @@
Copyright 2017 Christian Kaisermann <christian@kaisermann.me>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,50 +1,3 @@
[![npm version](https://badge.fury.io/js/svelte-i18n.svg)](https://badge.fury.io/js/svelte-i18n) ![](https://github.com/kaisermann/svelte-i18n/workflows/CI/badge.svg)
# @vtex/slugify
# svelte-i18n
> Internationalization for Svelte.
`svelte-i18n` helps you localize your app using the reactive tools Svelte provides. By using [stores](https://svelte.dev/docs#svelte_store) to keep track of the current `locale`, `dictionary` of messages and to `format` messages, we keep everything neat, in sync and easy to use on your svelte files.
**Requirements**
- Node: `>= 11.15.0`
- Browsers: `Chrome 38+`, `Edge 16+`, `Firefox 13+`, `Opera 25+`, `Safari 8+`.
```svelte
<script>
import { _ } from 'svelte-i18n'
</script>
<h1>{$_('page.home.title')}</h1>
<nav>
<a>{$_('page.home.nav', { default: 'Home' })}</a>
<a>{$_('page.about.nav', { default: 'About' })}</a>
<a>{$_('page.contact.nav', { default: 'Contact' })}</a>
</nav>
```
```jsonc
// en.json
{
"page": {
"home": {
"title": "Homepage",
"nav": "Home"
},
"about": {
"title": "About",
"nav": "About"
},
"contact": {
"title": "Contact",
"nav": "Contact Us"
}
}
}
```
- [Documentation](/docs/Getting%20Started.md)
- [Sapper Template](https://github.com/kaisermann/sapper-template-i18n)
- [i18n VSCode extension (3rd party)](https://github.com/antfu/i18n-ally)
> VTEX slug utilities

View File

@ -1,13 +0,0 @@
`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.

View File

@ -1,31 +0,0 @@
`import { dictionary } from 'svelte-i18n'`
### `$dictionary`
The `$dictionary` store is responsible for holding all loaded message definitions for each locale. A dictionary of messages can be a shallow or deep object:
###### `en-shallow.json`
```json
{
"title": "Sign up",
"field.name": "Name",
"field.birth": "Date of birth",
"field.genre": "Genre"
}
```
###### `en-deep.json`
```json
{
"title": "Sign up",
"field": {
"name": "Name",
"birth": "Date of birth",
"genre": "Genre"
}
}
```
It's recommended to use the [`addMessages()`](/docs/Methods.md#addmessages) and [`register()`](/docs/Methods.md#register) methods to add new message dictionaries to your app.

View File

@ -1,34 +0,0 @@
##### `'this' keyword is equivalent to 'undefined'`
When using `Rollup` as a bundler, you're possibly seeing this warning. It's related to the output of the typescript compiler used to transpile the `intl-messageformat` package. In this case, it's harmless and you can learn to live with it on your terminal or teach your rollup to ignore this kind of warning:
```js
// modified version of onwarn provided by sapper projects
const onwarn = (warning, onwarn) => {
if (
(warning.code === 'CIRCULAR_DEPENDENCY' &&
/[/\\]@sapper[/\\]/.test(warning.message))
) {
return
}
// ignores the annoying this is undefined warning
if(warning.code === 'THIS_IS_UNDEFINED') {
return
}
onwarn(warning)
}
export default {
client: {
...,
onwarn,
},
server: {
...,
onwarn,
},
}
```

View File

@ -1,212 +0,0 @@
<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->
<!-- code_chunk_output -->
- [Message syntax](#message-syntax)
- [`$format`, `$_` or `$t`](#format-_-or-t)
- [`$time(number: Date, options: MessageObject)`](#timenumber-date-options-messageobject)
- [`$date(date: Date, options: MessageObject)`](#datedate-date-options-messageobject)
- [`$number(number: number, options: MessageObject)`](#numbernumber-number-options-messageobject)
- [`$json(messageId: string)`](#jsonmessageid-string)
- [Formats](#formats)
- [Accessing formatters directly](#accessing-formatters-directly)
<!-- /code_chunk_output -->
### Message syntax
Under the hood, `formatjs` is used for localizing your messages. It allows `svelte-i18n` to support the ICU message syntax. It is strongly recommended to read their documentation about it.
- [Basic Internationalization Principles](https://formatjs.io/docs/core-concepts/basic-internationalization-principles)
- [Runtime Environments](https://formatjs.io/docs/guides/runtime-requirements/)
- [ICU Message Syntax](https://formatjs.io/docs/core-concepts/icu-syntax/)
### `$format`, `$_` or `$t`
`import { _, t, format } from 'svelte-i18n'`
The `$format` store is the actual formatter method. It's also aliased as `$_` and `$t` for convenience. To format a message is as simple as executing the `$format` method:
```svelte
<script>
import { _ } from 'svelte-i18n'
</script>
<h1>{$_('page_title')}</h1>
```
The formatter can be called with two different signatures:
- `format(messageId: string, options?: MessageObject): string`
- `format(options: MessageObject): string`
```ts
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. It is also possible to inject values into the translation like so:
```jsonc
// en.json
{
"awesome": "{name} is awesome!"
}
```
```svelte
<h1>{$_("awesome", { values: { name: "svelte-i18n" } })}</h1> <!-- "svelte-i18n is awesome" -->
```
If the message id literal value is not in the root of the dicitonary, `.` (dots) are interpreted as a path:
```jsonc
// en.json
{
"shallow.prop": "Shallow property",
"deep": {
"property": "Deep property"
}
}
```
```svelte
<div>{$_('shallow.prop')}</div> <!-- Shallow property -->
<div>{$_('deep.property')}</div> <!-- Deep property -->
```
### `$time(number: Date, options: MessageObject)`
`import { time } from 'svelte-i18n'`
Formats a date object into a time string with the specified format. Please refer to the [#formats](#formats) section to see available formats.
```html
<div>{$time(new Date(2019, 3, 24, 23, 45))}</div>
<!-- 11:45 PM -->
<div>{$time(new Date(2019, 3, 24, 23, 45), { format: 'medium' } )}</div>
<!-- 11:45:00 PM -->
```
### `$date(date: Date, options: MessageObject)`
`import { date } from 'svelte-i18n'`
Formats a date object into a string with the specified format. Please refer to the [#formats](#formats) section to see available formats.
```html
<div>{$date(new Date(2019, 3, 24, 23, 45))}</div>
<!-- 4/24/19 -->
<div>{$date(new Date(2019, 3, 24, 23, 45), { format: 'medium' } )}</div>
<!-- Apr 24, 2019 -->
```
### `$number(number: number, options: MessageObject)`
`import { number } from 'svelte-i18n'`
Formats a number with the specified locale and format. Please refer to the [#formats](#formats) section to see available formats.
```html
<div>{$number(100000000)}</div>
<!-- 100,000,000 -->
<div>{$number(100000000, { locale: 'pt' })}</div>
<!-- 100.000.000 -->
```
### `$json(messageId: string)`
`import { json } from 'svelte-i18n'`
Returns the raw JSON value of the specified `messageId` for the current locale. While [`$format`](#format-_-or-t) always returns a string, `$json` can be used to get an object relative to the current locale.
```html
<ul>
{#each $json('list.items') as item}
<li>{item.name}</li>
{/each}
</ul>
```
If you're using TypeScript, you can define the returned type as well:
```html
<ul>
{#each $json<Item[]>('list.items') as item}
<li>{item.name}</li>
{/each}
</ul>
```
### Formats
`svelte-i18n` comes with a default set of `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' }`
### Accessing formatters directly
`svelte-i18n` also provides a low-level API to access its formatter methods:
```js
import {
getDateFormatter,
getNumberFormatter,
getTimeFormatter,
getMessageFormatter,
} from 'svelte-i18n';
```
By using these methods, it's possible to manipulate values in a more specific way that fits your needs. For example, it's possible to create a method which receives a `date` and returns its relevant date related parts:
```js
import { getDateFormatter } from 'svelte-i18n';
const getDateParts = (date) =>
getDateFormatter()
.formatToParts(date)
.filter(({ type }) => type !== 'literal')
.reduce((acc, { type, value }) => {
acc[type] = value;
return acc;
}, {});
getDateParts(new Date(2020, 0, 1)); // { month: '1', day: '1', year: '2020' }
```
Check the [methods documentation](/docs/Methods.md#low-level-api) for more information.

View File

@ -1,152 +0,0 @@
### Getting started
<!-- @import "[TOC]" {cmd="toc" depthFrom=4 depthTo=6 orderedList=false} -->
<!-- code_chunk_output -->
- [1. Installing](#1-installing)
- [1.1 VSCode extension](#11-vscode-extension)
- [2. Locale dictionaries](#2-locale-dictionaries)
- [3. Adding locale dictionaries](#3-adding-locale-dictionaries)
- [3.1 Synchronous](#31-synchronous)
- [3.2 Asynchronous](#32-asynchronous)
- [4. Initializing](#4-initializing)
- [5. Localizing your app](#5-localizing-your-app)
<!-- /code_chunk_output -->
#### 1. Installing
First things first, let's install the necessary dependencies:
```sh
yarn add svelte-i18n
# if using rollup so we can import json files
yarn add -D @rollup/plugin-json
```
##### 1.1 VSCode extension
If you're using `VSCode` and want to have your messages previewed alongside your components, checkout the [i18n-ally](https://github.com/antfu/i18n-ally) and their [FAQ](https://github.com/antfu/i18n-ally/wiki/FAQ) to see how to set it up.
#### 2. Locale dictionaries
A locale dictionary is a regular JSON object which contains message definitions for a certain language.
```jsonc
// en.json
{
"page_title": "Page title",
"sign_in": "Sign in",
"sign_up": "Sign up"
}
// pt.json
{
"page_title": "Título da página",
"sign_in": "Entrar",
"sign_up": "Registrar"
}
```
#### 3. Adding locale dictionaries
There are two different ways of adding a new dictionary of messages to a certain locale:
##### 3.1 Synchronous
Just `import`/`require` your locale `.json` files and pass them to the [`addMessages(locale, dict)`](/docs/Methods.md#addmessage) method.
```js
// src/i18n.js
import { addMessages } from 'svelte-i18n';
import en from './en.json';
import enUS from './en-US.json';
import pt from './pt.json';
addMessages('en', en);
addMessages('en-US', enUS);
addMessages('pt', pt);
// en, en-US and pt are available
```
##### 3.2 Asynchronous
A more performant way to load your dictionaries is to register `loader` methods. This way, only the files registered to the current locale will be loaded. A `loader` is a method which must return a `Promise` that resolves to a `JSON` object. A [`$locale`](/docs/Locale.md#locale) value change will automatically load the registered loaders for the new locale.
```js
// src/i18n.js
import { register } from 'svelte-i18n';
register('en', () => import('./en.json'));
register('en-US', () => import('./en-US.json'));
register('pt', () => import('./pt.json'));
// en, en-US and pt are not available yet
```
#### 4. Initializing
After populating your [`$dictionary`](/docs/Dictionary.md) with [`addMessages()`](/docs/Methods.md#addmessages) or registering loaders via [`register()`](/docs/Methods.md#register), you are ready to bootstrap the library. You can use [`init()`](/docs/Methods.md#init) to define the fallback locale, initial locale and other options of your app.
```js
// src/i18n.js
import { register, init, getLocaleFromNavigator } from 'svelte-i18n';
register('en', () => import('./en.json'));
register('en-US', () => import('./en-US.json'));
register('pt', () => import('./pt.json'));
// en, en-US and pt are not available yet
init({
fallbackLocale: 'en',
initialLocale: getLocaleFromNavigator(),
});
// starts loading 'en-US' and 'en'
```
_Note_: Make sure to call your `i18n.js` file on your app's entry-point. If you're using Sapper, remember to also call `init()` on your server-side code (`server.js`).
Since we're using `register`, and not `addMessages`, we need to wait for it's loaders to finish before rendering your app.
In **Svelte**, the [`$isLoading`](/docs/Locale.md#loading) store can help to only show your app after the initial load as shown in [Locale](/docs/Locale.md#loading).
In **Sapper**, you can use the `preload` static method together with `waitLocale`:
```svelte
<!-- src/_layout.svelte -->
<script context="module">
import { waitLocale } from 'svelte-i18n'
export async function preload() {
// awaits for the loading of the 'en-US' and 'en' dictionaries
return waitLocale()
}
</script>
```
Please note that the `fallbackLocale` is always loaded, independent of the current locale, since only some messages can be missing.
#### 5. Localizing your app
After having the initial locale set, you're ready to start localizing your app. Import the [`$format`](/docs/Formatting.md) method, or any of its aliases, to any component that needs to be translated. Then, just call [`$format`](/docs/Formatting.md) passing the message `id` on your layout and voila! 🎉
```svelte
<script>
import { _ } from 'svelte-i18n'
</script>
<svelte:head>
<title>{$_('page_title')}</title>
</svelte:head>
<nav>
<a>{$_('sign_in')}</a>
<a>{$_('sign_up')}</a>
</nav>
```
See [Formatting](/docs/Formatting.md) to read about the supported message syntax and all the available formatters.

View File

@ -1,60 +0,0 @@
`import { locale } from 'svelte-i18n'`
<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->
### `$locale`
The `locale` store defines what is the current locale. When its value is changed, before updating the actual stored value, `svelte-i18n` sees if there's any message loaders registered for the new locale:
- If yes, changing the `locale` is an async operation.
- If no, the locale's dictionary is fully loaded and changing the locale is a sync operation.
The `<html lang>` attribute is automatically updated to the current locale.
#### Usage on component
To change the locale inside a component is as simple as assinging it a new value.
```svelte
<script>
import { locale, locales } from 'svelte-i18n'
</script>
<select bind:value={$locale}>
{#each $locales as locale}
<option value={locale}>{locale}</option>
{/each}
</select>
```
#### Usage on regular script
```js
import { locale } from 'svelte-i18n'
// Set the current locale to en-US
locale.set('en-US')
// This is a store, so we can subscribe to its changes
locale.subscribe(() => console.log('locale change'))
```
### `$isLoading`
While changing the `$locale`, the `$isLoading` store can be used to detect if the app is currently fetching any enqueued message definitions.
```svelte
<script>
import { isLoading } from 'svelte-i18n'
</script>
{#if $isLoading}
Please wait...
{:else}
<Nav />
<Main />
{/if}
```
> `$isLoading` will only be `true` if fetching takes more than 200ms.

View File

@ -1,295 +0,0 @@
<!-- @import "[TOC]" {cmd="toc" depthFrom=4 depthTo=4 orderedList=false} -->
<!-- code_chunk_output -->
- [`init`](#init)
- [`getLocaleFromHostname`](#getlocalefromhostname)
- [`getLocaleFromPathname`](#getlocalefrompathname)
- [`getLocaleFromNavigator`](#getlocalefromnavigator)
- [`getLocaleFromQueryString`](#getlocalefromquerystring)
- [`getLocaleFromHash`](#getlocalefromhash)
- [`addMessages`](#addmessages)
- [`register`](#register)
- [`waitLocale`](#waitlocale)
- [`getDateFormatter`/`getTimeFormatter`/`getNumberFormatter`](#getdateformattergettimeformattergetnumberformatter)
- [`getMessageFormatter`](#getmessageformatter)
<!-- /code_chunk_output -->
#### `init`
> `import { init } from 'svelte-i18n'`
`init(options: InitOptions): void`
Method responsible for configuring some of the library behaviours such as the global fallback and initial locales. Must be called before setting a locale and displaying your view.
```ts
interface InitOptions {
// the global fallback locale
fallbackLocale: string
// the app initial locale
initialLocale?: string
// custom time/date/number formats
formats?: Formats
// loading delay interval
loadingDelay?: number
}
```
**Example**:
```js
import { init } from 'svelte-i18n'
init({
// fallback to en if current locale is not in the dictionary
fallbackLocale: 'en',
initialLocale: 'pt-br',
})
```
##### Custom formats
It's possible to define custom format styles via the `formats` property if you want to quickly pass a set of options to the underlying `Intl` formatter.
```ts
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 { init } from 'svelte-i18n'
init({
// fallback to en if current locale is not in the dictionary
fallbackLocale: 'en',
formats: {
number: {
EUR: { style: 'currency', currency: 'EUR' },
},
},
})
```
```html
<div>
{$_.number(123456.789, { format: 'EUR' })}
</div>
<!-- 123.456,79 € -->
```
#### `getLocaleFromHostname`
> `import { getLocaleFromHostname } from 'svelte-i18n'
`getLocaleFromHostname(hostnamePattern: RegExp): string`
Utility method to help getting a initial locale based on a pattern of the current `hostname`.
**Example**:
```js
import { init, getLocaleFromHostname } from 'svelte-i18n'
init({
// fallback to en if current locale is not in the dictionary
fallbackLocale: 'en',
initialLocale: getLocaleFromHostname(/^(.*?)\./),
})
```
#### `getLocaleFromPathname`
> `import { getLocaleFromPathname } from 'svelte-i18n'
`getLocaleFromPathname(pathnamePattern: RegExp): string`
Utility method to help getting a initial locale based on a pattern of the current `pathname`.
**Example**:
```js
import { init, getLocaleFromPathname } from 'svelte-i18n'
init({
// fallback to en if current locale is not in the dictionary
fallbackLocale: 'en',
initialLocale: getLocaleFromPathname(/^\/(.*?)\//),
})
```
#### `getLocaleFromNavigator`
> `import { getLocaleFromNavigator } from 'svelte-i18n'
`getLocaleFromNavigator(): string`
Utility method to help getting a initial locale based on the browser's `navigator` settings.
**Example**:
```js
import { init, getLocaleFromNavigator } from 'svelte-i18n'
init({
// fallback to en if current locale is not in the dictionary
fallbackLocale: 'en',
initialLocale: getLocaleFromNavigator(),
})
```
#### `getLocaleFromQueryString`
> `import { getLocaleFromQueryString } from 'svelte-i18n'
`getLocaleFromQueryString(queryKey: string): string`
Utility method to help getting a initial locale based on a query string value.
```js
import { init, getLocaleFromQueryString } from 'svelte-i18n'
init({
// fallback to en if current locale is not in the dictionary
fallbackLocale: 'en',
initialLocale: getLocaleFromQueryString('lang'),
})
```
#### `getLocaleFromHash`
> `import { getLocaleFromHash } from 'svelte-i18n'
`getLocaleFromHash(hashKey: string): string`
Utility method to help getting a initial locale based on a hash `{key}={value}` string.
**Example**:
```js
import { init, getLocaleFromHash } from 'svelte-i18n'
init({
// fallback to en if current locale is not in the dictionary
fallbackLocale: 'en',
initialLocale: getLocaleFromHash('lang'),
})
```
#### `addMessages`
`import { addMessages } from 'svelte-i18n`
`addMessages(locale: string, ...dicts: Dictionary[]): void`
Merge one ore more dictionary of messages with the `locale` dictionary.
**Example**:
```js
addMessages('en', { field_1: 'Name' })
addMessages('en', { field_2: 'Last Name' })
addMessages('pt', { field_1: 'Nome' })
addMessages('pt', { field_2: 'Sobrenome' })
// Results in dictionary
{
en: {
field_1: 'Name',
field_2: 'Last Name'
},
pt: {
field_1: 'Nome',
field_2: 'Sobrenome'
}
}
```
#### `register`
> `import { register } from 'svelte-i18n'`
`register(locale: string, loader: () => Promise<object>): void`
Registers an async message `loader` for the specified `locale`. The loader queue is executed when changing to `locale` or when calling `waitLocale(locale)`.
**Example**:
```js
import { register } from 'svelte-i18n'
register('en', () => import('./_locales/en.json'))
register('pt', () => import('./_locales/pt.json'))
```
See [how to asynchronously load dictionaries](/svelte-i18n/blob/master/docs#22-asynchronous).
#### `waitLocale`
> `import { waitLocale } from 'svelte-i18n'`
`waitLocale(locale: string = $locale): Promise<void>`
Executes the queue of `locale`. If the queue isn't resolved yet, the same promise is returned. Great to use in the `preload` method of Sapper for awaiting [`loaders`](/svelte-i18n/blob/master/docs#22-asynchronous).
**Example**:
```svelte
<script context="module">
import { register, waitLocale, init } from 'svelte-i18n'
register('en', () => import('./_locales/en.json'))
register('pt-BR', () => import('./_locales/pt-BR.json'))
register('es-ES', () => import('./_locales/es-ES.json'))
init({ fallbackLocale: 'en' })
export async function preload() {
// awaits for 'en' loaders
return waitLocale()
}
</script>
```
### Low level API
#### `getDateFormatter`/`getTimeFormatter`/`getNumberFormatter`
> `import { getDateFormatter, getNumberFormatter, getTimeFormatter } from 'svelte-i18n'`
```ts
type FormatterOptions<T> = T & {
format?: string
locale?: string // defaults to current locale
}
getDateFormatter(
options: FormatterOptions<Intl.DateTimeFormatOptions>
): Intl.DateTimeFormat
getTimeFormatter(
options: FormatterOptions<Intl.DateTimeFormatOptions>
): Intl.DateTimeFormat
getNumberFormatter(
options: FormatterOptions<Intl.NumberFormatOptions>
): Intl.NumberFormat
```
Each of these methods return their respective [`Intl.xxxxFormatter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#Internationalization) variant. Click [here](/docs/formatting.md#accessing-formatters-directly) for an example of usage.
#### `getMessageFormatter`
> `import { getMessageFormatter } from 'svelte-i18n'`
`getMessageFormatter(messageId: string, locale: string): IntlMessageFormat`
Returns an instance of a [`IntlMessageFormat`](https://github.com/formatjs/formatjs/blob/master/packages/intl-messageformat/README.md).

View File

@ -1,210 +0,0 @@
<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->
<!-- code_chunk_output -->
- [From `v2` to `v3`](#from-v2-to-v3)
- [Formatting numbers, dates and times](#formatting-numbers-dates-and-times)
- [Casing utilities](#casing-utilities)
- [Getting the client locale](#getting-the-client-locale)
- [From `v1` to `v2`](#from-v1-to-v2)
- [Adding dictionaries](#adding-dictionaries)
- [Setting the initial and fallback locales](#setting-the-initial-and-fallback-locales)
- [Interpolating values](#interpolating-values)
- [Adding custom formats](#adding-custom-formats)
<!-- /code_chunk_output -->
#### From `v2` to `v3`
##### Formatting numbers, dates and times
In `v2`, to format numbers, dates and times you would access the `date`, `time` or `number` method of the main formatter method:
```js
$_.time(dateValue)
$_.date(dateValue)
$_.number(100000000)
```
In `v3`, these utilities are exported as standalone formatter stores, making them tree-shakeable:
```js
import { time, date, number } from 'svelte-i18n'
$time(someDateValue)
$date(someDateValue)
$number(100000000)
```
##### Casing utilities
The casing utilities `$_.title`, `$_.capital`, `$_.lower`, `$_.upper` were removed from the library.
In `v2`:
```js
$_.lower('message.id')
$_.upper('message.id')
$_.title('message.id')
$_.capital('message.id')
```
In `v3`:
```js
function capital(str: string) {
return str.replace(/(^|\s)\S/, l => l.toLocaleUpperCase())
}
function title(str: string) {
return str.replace(/(^|\s)\S/g, l => l.toLocaleUpperCase())
}
$_('message.id').toLocaleLowerCase()
$_('message.id').toLocaleUpperCase()
title($_('message.id'))
capital($_('message.id'))
```
##### Getting the client locale
In `v2`, the [`init`](/docs/Methods.md#init) method could automatically set the initial locale based on some heuristcs from the client:
```js
import { init } from 'svelte-i18n'
init({
initialLocale: {
navigator: true,
},
})
```
However, many people didn't need that kind of extra weight in their apps. So in `v3` you have to explicitly import the utility desired:
- [`getLocaleFromHostname`](/docs/Methods.md#getlocalefromhostname)
- [`getLocaleFromPathname`](/docs/Methods.md#getlocalefrompathname)
- [`getLocaleFromNavigator`](/docs/Methods.md#getlocalefromnavigator)
- [`getLocaleFromQueryString`](/docs/Methods.md#getlocalefromquerystring)
- [`getLocaleFromHash`](/docs/Methods.md#getlocalefromhash)
```js
import { init, getLocaleFromNavigator } from 'svelte-i18n'
init({
initialLocale: getLocaleFromNavigator(),
})
```
#### From `v1` to `v2`
##### Adding dictionaries
In `v1`, dictionaries were added through the store API of `$dictionary`.
```js
import { dictionary } from 'svelte-i18n'
dictionary.set({
en: { ... },
pt: { ... },
})
dictionary.update(d => {
d.fr = { ... }
return d
})
```
In `v2`, you can use [`addMessages(locale, messages)`](/docs/Methods.md#addmessages) to add new messages to the main dictionary.
```js
import { addMessages } from 'svelte-i18n'
addMessages('en', { ... })
addMessages('pt', { ... })
addMessages('fr', { ... })
// message dictionaries are merged together
addMessages('en', { ... })
```
_It's also possible to asynchronously load your locale dictionary, see [register()](/docs/Methods.md#register)._
##### Setting the initial and fallback locales
In `v1`, to set the initial and fallback locales you could use `getClientLocale()` together with `$locale = ...` or `locale.set(...)`.
```js
import { getClientLocale, locale } from 'svelte-i18n'
locale.set(
getClientLocale({
fallback: 'en',
navigator: true,
})
)
```
In `v2`, both locales can be defined very similarly with [`init()`](/docs/Methods.md#init).
```js
import { init } from 'svelte-i18n'
init({
fallbackLocale: 'en',
initialLocale: {
navigator: true,
},
})
```
##### Interpolating values
In `v1`, interpolated values were the whole object passed as the second argument of the `$format` method.
```svelte
<h1>
{$_('navigation.pagination', { current: 2, max: 10 })}
</h1>
<!-- Page: 2/10 -->
```
In `v2`, the interpolated values are passed in the `values` property.
```svelte
<h1>
{$_('navigation.pagination', { values: { current: 2, max: 10 }})}
</h1>
<!-- Page: 2/10 -->
```
##### Adding custom formats
In `v1`, custom formats could be added with `addCustomFormats()`.
```js
import { addCustomFormats } from 'svelte-i18n'
addCustomFormats({
number: {
EUR: { style: 'currency', currency: 'EUR' },
},
})
```
In `v2`, custom formats are added through [`init()`](/docs/Methods.md#init).
```js
import { init } from 'svelte-i18n'
init({
fallbackLocale: ...,
initialLocale, ...,
formats:{
number: {
EUR: { style: 'currency', currency: 'EUR' },
},
}
})
```

View File

@ -1,22 +1,15 @@
{
"name": "svelte-i18n",
"version": "3.3.0",
"main": "dist/runtime.cjs.js",
"module": "dist/runtime.esm.js",
"types": "types/runtime/index.d.ts",
"bin": {
"svelte-i18n": "dist/cli.js"
},
"name": "@vtex/slugify",
"version": "0.0.1",
"main": "dist/main.cjs.js",
"module": "dist/main.esm.js",
"types": "types/index.d.ts",
"license": "MIT",
"description": "Internationalization library for Svelte",
"description": "VTEX slug utilities",
"author": "Christian Kaisermann <christian@kaisermann.me>",
"repository": "https://github.com/kaisermann/svelte-i18n",
"repository": "https://github.com/vtex/slugify",
"keywords": [
"svelte",
"i18n",
"internationalization",
"localization",
"translation"
"slug"
],
"engines": {
"node": ">= 11.15.0"
@ -24,16 +17,13 @@
"scripts": {
"clean": "rm -rf dist/ types/",
"build": "rollup -c",
"build:types": "tsc -p src/runtime --emitDeclarationOnly",
"dev": "rollup -c -w",
"test": "cross-env NODE_ICU_DATA=node_modules/full-icu jest",
"test:ci": "npm run test -- --silent",
"lint": "eslint \"{src,test}/**/*.ts\"",
"format": "prettier --loglevel silent --write \"{src,test}/**/*.ts\"",
"release": " git add package.json && git commit -m \"chore(release): v$npm_package_version :tada:\"",
"build:types": "tsc -p src/ --emitDeclarationOnly",
"prebuild": "yarn clean",
"postbuild": "yarn build:types",
"version": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1 && git add CHANGELOG.md",
"dev": "rollup -c -w",
"test": "jest",
"lint": "eslint \"{src,test}/**/*.ts\"",
"format": "prettier --loglevel silent --write \"{src,test}/**/*.ts\"",
"prepublishOnly": "npm run test && npm run build"
},
"files": [
@ -57,7 +47,7 @@
"jest": {
"collectCoverage": true,
"testMatch": [
"<rootDir>/test/**/*.test.ts"
"<rootDir>/**/*.test.ts"
],
"collectCoverageFrom": [
"<rootDir>/src/**/*.ts"
@ -66,46 +56,22 @@
"^.+\\.tsx?$": "ts-jest"
}
},
"peerDependencies": {
"svelte": "^3.25.1"
},
"devDependencies": {
"@babel/core": "^7.11.6",
"@babel/preset-env": "^7.11.5",
"@kiwi/eslint-config": "^1.2.0",
"@kiwi/prettier-config": "^1.1.0",
"@types/dlv": "^1.1.2",
"@types/estree": "0.0.45",
"@types/intl": "^1.2.0",
"@types/jest": "^26.0.14",
"@types/sade": "^1.7.2",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^26.3.0",
"@vtex/prettier-config": "^0.3.5",
"conventional-changelog-cli": "^2.1.0",
"cross-env": "^7.0.2",
"eslint": "^7.9.0",
"full-icu": "^1.3.1",
"eslint-config-vtex": "^12.8.11",
"husky": "^4.3.0",
"jest": "^26.4.2",
"lint-staged": "^10.4.0",
"prettier": "^2.1.2",
"rollup": "^2.27.1",
"rollup-plugin-auto-external": "^2.0.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.27.2",
"sass": "^1.26.11",
"svelte": "^3.25.1",
"svelte-preprocess": "^4.3.0",
"ts-jest": "^26.4.0",
"typescript": "^4.0.3"
},
"dependencies": {
"deepmerge": "^4.2.2",
"dlv": "^1.1.3",
"estree-walker": "^2.0.1",
"intl-messageformat": "^9.3.15",
"sade": "^1.7.4",
"tiny-glob": "^0.2.6"
}
}

View File

@ -1,41 +1,18 @@
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 commonjs from 'rollup-plugin-commonjs'
import ts from 'rollup-plugin-typescript2'
import { terser } from 'rollup-plugin-terser'
import pkg from './package.json';
import pkg from './package.json'
const PROD = !process.env.ROLLUP_WATCH;
const PROD = !process.env.ROLLUP_WATCH
export default [
{
input: 'src/runtime/index.ts',
external: [
...Object.keys(pkg.dependencies),
...Object.keys(pkg.peerDependencies),
'svelte/store',
],
input: 'src/index.ts',
output: [
{ file: pkg.module, format: 'es' },
{ file: pkg.main, format: 'cjs' },
],
plugins: [commonjs(), autoExternal(), ts(), PROD && terser()],
plugins: [commonjs(), 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()],
},
];
]

View File

@ -1,202 +0,0 @@
import {
Node,
ObjectExpression,
ImportDeclaration,
ImportSpecifier,
CallExpression,
Identifier,
Literal,
} from 'estree';
import { walk } from 'estree-walker';
import { Ast } from 'svelte/types/compiler/interfaces';
import { parse } from 'svelte/compiler';
import dlv from 'dlv';
import { deepSet } from './includes/deepSet';
import { getObjFromExpression } from './includes/getObjFromExpression';
import { Message } from './types';
const LIB_NAME = 'svelte-i18n';
const DEFINE_MESSAGES_METHOD_NAME = 'defineMessages';
const FORMAT_METHOD_NAMES = new Set(['format', '_', 't']);
function isFormatCall(node: Node, imports: Set<string>) {
if (node.type !== 'CallExpression') return false;
let identifier: Identifier;
if (node.callee.type === 'Identifier') {
identifier = node.callee;
}
if (!identifier || identifier.type !== 'Identifier') {
return false;
}
const methodName = identifier.name.slice(1);
return imports.has(methodName);
}
function isMessagesDefinitionCall(node: Node, methodName: string) {
if (node.type !== 'CallExpression') return false;
return (
node.callee &&
node.callee.type === 'Identifier' &&
node.callee.name === methodName
);
}
function getLibImportDeclarations(ast: Ast) {
return (ast.instance
? ast.instance.content.body.filter(
(node) =>
node.type === 'ImportDeclaration' && node.source.value === LIB_NAME,
)
: []) as ImportDeclaration[];
}
function getDefineMessagesSpecifier(decl: ImportDeclaration) {
return decl.specifiers.find(
(spec) =>
'imported' in spec && spec.imported.name === DEFINE_MESSAGES_METHOD_NAME,
) as ImportSpecifier;
}
function getFormatSpecifiers(decl: ImportDeclaration) {
return decl.specifiers.filter(
(spec) => 'imported' in spec && FORMAT_METHOD_NAMES.has(spec.imported.name),
) as ImportSpecifier[];
}
export function collectFormatCalls(ast: Ast) {
const importDecls = getLibImportDeclarations(ast);
if (importDecls.length === 0) return [];
const imports = new Set(
importDecls.flatMap((decl) =>
getFormatSpecifiers(decl).map((n) => n.local.name),
),
);
if (imports.size === 0) return [];
const calls: CallExpression[] = [];
function enter(node: Node) {
if (isFormatCall(node, imports)) {
calls.push(node as CallExpression);
this.skip();
}
}
walk(ast.instance as any, { enter });
walk(ast.html as any, { enter });
return calls;
}
export function collectMessageDefinitions(ast: Ast) {
const definitions: ObjectExpression[] = [];
const defineImportDecl = getLibImportDeclarations(ast).find(
getDefineMessagesSpecifier,
);
if (defineImportDecl == null) return [];
const defineMethodName = getDefineMessagesSpecifier(defineImportDecl).local
.name;
walk(ast.instance as any, {
enter(node: Node) {
if (isMessagesDefinitionCall(node, defineMethodName) === false) return;
const [arg] = (node as CallExpression).arguments;
if (arg.type === 'ObjectExpression') {
definitions.push(arg);
this.skip();
}
},
});
return definitions.flatMap((definitionDict) =>
definitionDict.properties.map((propNode) => {
if (propNode.type !== 'Property') {
throw new Error(
`Found invalid '${propNode.type}' at L${propNode.loc.start.line}:${propNode.loc.start.column}`,
);
}
return propNode.value as ObjectExpression;
}),
);
}
export function collectMessages(markup: string): Message[] {
const ast = parse(markup);
const calls = collectFormatCalls(ast);
const definitions = collectMessageDefinitions(ast);
return [
...definitions.map((definition) => getObjFromExpression(definition)),
...calls.map((call) => {
const [pathNode, options] = call.arguments;
let messageObj;
if (pathNode.type === 'ObjectExpression') {
// _({ ...opts })
messageObj = getObjFromExpression(pathNode);
} else {
const node = pathNode as Literal;
const id = node.value as string;
if (options && options.type === 'ObjectExpression') {
// _(id, { ...opts })
messageObj = getObjFromExpression(options);
messageObj.id = id;
} else {
// _(id)
messageObj = { id };
}
}
if (messageObj?.id == null) return null;
return messageObj;
}),
].filter(Boolean);
}
export function extractMessages(
markup: string,
{ accumulator = {}, shallow = false, overwrite = false } = {} as any,
) {
collectMessages(markup).forEach((messageObj) => {
let defaultValue = messageObj.default;
if (typeof defaultValue === 'undefined') {
defaultValue = '';
}
if (shallow) {
if (overwrite === false && messageObj.id in accumulator) {
return;
}
accumulator[messageObj.id] = defaultValue;
} else {
if (
overwrite === false &&
typeof dlv(accumulator, messageObj.id) !== 'undefined'
) {
return;
}
deepSet(accumulator, messageObj.id, defaultValue);
}
});
return accumulator;
}

View File

@ -1,21 +0,0 @@
/* eslint-disable no-multi-assign */
/* eslint-disable no-return-assign */
const isNumberString = (n: string) => !Number.isNaN(parseInt(n, 10));
export function deepSet(obj: any, path: string, value: any) {
const parts = path.replace(/\[(\w+)\]/gi, '.$1').split('.');
return parts.reduce((ref, part, i) => {
if (part in ref) return (ref = ref[part]);
if (i < parts.length - 1) {
if (isNumberString(parts[i + 1])) {
return (ref = ref[part] = []);
}
return (ref = ref[part] = {});
}
return (ref[part] = value);
}, obj);
}

View File

@ -1,19 +0,0 @@
import { ObjectExpression, Property, Identifier } from 'estree';
import { Message } from '../types';
export function getObjFromExpression(exprNode: ObjectExpression) {
return exprNode.properties.reduce<Message>((acc, prop: Property) => {
// we only want primitives
if (
prop.value.type === 'Literal' &&
prop.value.value !== Object(prop.value.value)
) {
const key = (prop.key as Identifier).name as string;
acc[key] = prop.value.value;
}
return acc;
}, {});
}

View File

@ -1,80 +0,0 @@
import fs from 'fs';
import { dirname, resolve } from 'path';
import sade from 'sade';
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);
const program = sade('svelte-i18n');
program
.command('extract <glob> [output]')
.describe('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?.preprocess) {
const processed = await preprocess(content, svelteConfig.preprocess, {
filename: filePath,
});
content = processed.code;
}
extractMessages(content, { filePath, 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);

View File

@ -1,5 +0,0 @@
export interface Message {
id?: string;
default?: string;
[key: string]: any;
}

24
src/index.test.ts Normal file
View File

@ -0,0 +1,24 @@
import { slugify } from './index'
test('replaces invalid characters with -', () => {
const input = '*+~.()\'"!:@&[]`,/ %$#?{}|><=_^'
const output = '-'.repeat(input.length)
expect(slugify(input)).toBe(output)
})
test('lowercases the input', () => {
const input = `I'M NOT LOWERCASE`
const output = 'i-m-not-lowercase'
expect(slugify(input)).toBe(output)
})
test('replaces diactrics characters with their counterparts without diactrics', () => {
const input =
'ÁÄÂÀÃÅČÇĆĎÉĚËÈÊẼĔȆÍÌÎÏŇÑÓÖÒÔÕØŘŔŠŤÚŮÜÙÛÝŸŽáäâàãåčçćďéěëèêẽĕȇíìîïňñóöòôõøðřŕšťúůüùûýÿžþÞĐđßÆa'
const output = 'AAAAAACCCDEEEEEEEEIIIINNOOOOOORRSTUUUUUYYZaaaaaacccdeeeeeeeeiiiinnooooooorrstuuuuuyyzbBDdBAa'.toLowerCase()
expect(slugify(input)).toBe(output)
})

25
src/index.ts Normal file
View File

@ -0,0 +1,25 @@
const from =
'ÁÄÂÀÃÅČÇĆĎÉĚËÈÊẼĔȆÍÌÎÏŇÑÓÖÒÔÕØŘŔŠŤÚŮÜÙÛÝŸŽáäâàãåčçćďéěëèêẽĕȇíìîïňñóöòôõøðřŕšťúůüùûýÿžþÞĐđßÆa·/_,:;'
const to =
'AAAAAACCCDEEEEEEEEIIIINNOOOOOORRSTUUUUUYYZaaaaaacccdeeeeeeeeiiiinnooooooorrstuuuuuyyzbBDdBAa------'
const removeAccents = (str: string) => {
let newStr = str.slice(0)
for (let i = 0; i < from.length; i++) {
newStr = newStr.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
}
return newStr
}
// Based on: https://github.com/vtex-apps/search-result/blob/master/react/utils/slug.ts
// Parameter "S. Coifman" should output "s--coifman"
export function slugify(str: string) {
// According to Bacelar, the search API uses a legacy method for slugifying strings.
// replaces special characters with dashes, remove accents and lower cases everything
const replaced = str.replace(/[*+~.()'"!:@&[\]`,/ %$#?{}|><=_^]/g, '-')
return removeAccents(replaced).toLowerCase()
}

View File

@ -1,76 +0,0 @@
import { ConfigureOptions } from './types';
import { $locale } from './stores/locale';
interface Formats {
number: Record<string, any>;
date: Record<string, any>;
time: Record<string, any>;
}
export const defaultFormats: Formats = {
number: {
scientific: { notation: 'scientific' },
engineering: { notation: 'engineering' },
compactLong: { notation: 'compact', compactDisplay: 'long' },
compactShort: { notation: 'compact', compactDisplay: 'short' },
},
date: {
short: { month: 'numeric', day: 'numeric', year: '2-digit' },
medium: { month: 'short', day: 'numeric', year: 'numeric' },
long: { month: 'long', day: 'numeric', year: 'numeric' },
full: { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' },
},
time: {
short: { hour: 'numeric', minute: 'numeric' },
medium: { hour: 'numeric', minute: 'numeric', second: 'numeric' },
long: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZoneName: 'short',
},
full: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZoneName: 'short',
},
},
};
export const defaultOptions: ConfigureOptions = {
fallbackLocale: null,
initialLocale: null,
loadingDelay: 200,
formats: defaultFormats,
warnOnMissingMessages: true,
};
const options: ConfigureOptions = defaultOptions;
export function getOptions() {
return options;
}
export function init(opts: ConfigureOptions) {
const { formats, ...rest } = opts;
const initialLocale = opts.initialLocale || opts.fallbackLocale;
Object.assign(options, rest, { initialLocale });
if (formats) {
if ('number' in formats) {
Object.assign(options.formats.number, formats.number);
}
if ('date' in formats) {
Object.assign(options.formats.date, formats.date);
}
if ('time' in formats) {
Object.assign(options.formats.time, formats.time);
}
}
return $locale.set(initialLocale);
}

View File

@ -1,95 +0,0 @@
import IntlMessageFormat from 'intl-messageformat';
import { MemoizedIntlFormatter } from '../types';
import { getCurrentLocale } from '../stores/locale';
import { getOptions } from '../configs';
import { monadicMemoize } from './memoize';
type MemoizedNumberFormatterFactory = MemoizedIntlFormatter<
Intl.NumberFormat,
Intl.NumberFormatOptions
>;
type MemoizedDateTimeFormatterFactory = MemoizedIntlFormatter<
Intl.DateTimeFormat,
Intl.DateTimeFormatOptions
>;
const getIntlFormatterOptions = (
type: 'time' | 'number' | 'date',
name: string,
): any => {
const { formats } = getOptions();
if (type in formats && name in formats[type]) {
return formats[type][name];
}
throw new Error(`[svelte-i18n] Unknown "${name}" ${type} format.`);
};
const createNumberFormatter: MemoizedNumberFormatterFactory = monadicMemoize(
({ locale, format, ...options }) => {
if (locale == null) {
throw new Error('[svelte-i18n] A "locale" must be set to format numbers');
}
if (format) {
options = getIntlFormatterOptions('number', format);
}
return new Intl.NumberFormat(locale, options);
},
);
const createDateFormatter: MemoizedDateTimeFormatterFactory = monadicMemoize(
({ locale, format, ...options }) => {
if (locale == null) {
throw new Error('[svelte-i18n] A "locale" must be set to format dates');
}
if (format) options = getIntlFormatterOptions('date', format);
else if (Object.keys(options).length === 0) {
options = getIntlFormatterOptions('date', 'short');
}
return new Intl.DateTimeFormat(locale, options);
},
);
const createTimeFormatter: MemoizedDateTimeFormatterFactory = monadicMemoize(
({ locale, format, ...options }) => {
if (locale == null) {
throw new Error(
'[svelte-i18n] A "locale" must be set to format time values',
);
}
if (format) options = getIntlFormatterOptions('time', format);
else if (Object.keys(options).length === 0) {
options = getIntlFormatterOptions('time', 'short');
}
return new Intl.DateTimeFormat(locale, options);
},
);
export const getNumberFormatter: MemoizedNumberFormatterFactory = ({
locale = getCurrentLocale(),
...args
} = {}) => createNumberFormatter({ locale, ...args });
export const getDateFormatter: MemoizedDateTimeFormatterFactory = ({
locale = getCurrentLocale(),
...args
} = {}) => createDateFormatter({ locale, ...args });
export const getTimeFormatter: MemoizedDateTimeFormatterFactory = ({
locale = getCurrentLocale(),
...args
} = {}) => createTimeFormatter({ locale, ...args });
export const getMessageFormatter = monadicMemoize(
(message: string, locale: string = getCurrentLocale()) =>
new IntlMessageFormat(message, locale, getOptions().formats),
);

View File

@ -1,111 +0,0 @@
import { MessagesLoader } from '../types';
import {
hasLocaleDictionary,
$dictionary,
addMessages,
} from '../stores/dictionary';
import { getRelatedLocalesOf } from '../stores/locale';
type Queue = Set<MessagesLoader>;
const queue: Record<string, Queue> = {};
export function resetQueues() {
Object.keys(queue).forEach((key) => {
delete queue[key];
});
}
function createLocaleQueue(locale: string) {
queue[locale] = new Set();
}
function removeLoaderFromQueue(locale: string, loader: MessagesLoader) {
queue[locale].delete(loader);
if (queue[locale].size === 0) {
delete queue[locale];
}
}
function getLocaleQueue(locale: string) {
return queue[locale];
}
function getLocalesQueues(locale: string) {
return getRelatedLocalesOf(locale)
.reverse()
.map<[string, MessagesLoader[]]>((localeItem) => {
const localeQueue = getLocaleQueue(localeItem);
return [localeItem, localeQueue ? [...localeQueue] : []];
})
.filter(([, localeQueue]) => localeQueue.length > 0);
}
export function hasLocaleQueue(locale: string) {
return getRelatedLocalesOf(locale)
.reverse()
.some((localeQueue) => getLocaleQueue(localeQueue)?.size);
}
function loadLocaleQueue(locale: string, localeQueue: MessagesLoader[]) {
const allLoadersPromise = Promise.all(
localeQueue.map((loader) => {
// todo: maybe don't just remove, but add to a `loading` set?
removeLoaderFromQueue(locale, loader);
return loader().then((partial) => partial.default || partial);
}),
);
return allLoadersPromise.then((partials) => addMessages(locale, ...partials));
}
const activeFlushes: { [key: string]: Promise<void> } = {};
export function flush(locale: string): Promise<void> {
if (!hasLocaleQueue(locale)) {
if (locale in activeFlushes) {
return activeFlushes[locale];
}
return;
}
// get queue of XX-YY and XX locales
const queues = getLocalesQueues(locale);
// todo: what happens if some loader fails?
activeFlushes[locale] = Promise.all(
queues.map(([localeName, localeQueue]) =>
loadLocaleQueue(localeName, localeQueue),
),
).then(() => {
if (hasLocaleQueue(locale)) {
return flush(locale);
}
delete activeFlushes[locale];
});
return activeFlushes[locale];
}
export function registerLocaleLoader(locale: string, loader: MessagesLoader) {
if (!getLocaleQueue(locale)) createLocaleQueue(locale);
const localeQueue = getLocaleQueue(locale);
// istanbul ignore if
if (getLocaleQueue(locale).has(loader)) return;
if (!hasLocaleDictionary(locale)) {
$dictionary.update((d) => {
d[locale] = {};
return d;
});
}
localeQueue.add(loader);
}

View File

@ -1,54 +0,0 @@
const getFromQueryString = (queryString: string, key: string) => {
const keyVal = queryString.split('&').find((i) => i.indexOf(`${key}=`) === 0);
if (keyVal) {
return keyVal.split('=').pop();
}
return null;
};
const getFirstMatch = (base: string, pattern: RegExp) => {
const match = pattern.exec(base);
// istanbul ignore if
if (!match) return null;
// istanbul ignore else
return match[1] || null;
};
export const getLocaleFromHostname = (hostname: RegExp) => {
// istanbul ignore next
if (typeof window === 'undefined') return null;
return getFirstMatch(window.location.hostname, hostname);
};
export const getLocaleFromPathname = (pathname: RegExp) => {
// istanbul ignore next
if (typeof window === 'undefined') return null;
return getFirstMatch(window.location.pathname, pathname);
};
export const getLocaleFromNavigator = () => {
// istanbul ignore next
if (typeof window === 'undefined') return null;
return window.navigator.language || window.navigator.languages[0];
};
export const getLocaleFromQueryString = (search: string) => {
// istanbul ignore next
if (typeof window === 'undefined') return null;
return getFromQueryString(window.location.search.substr(1), search);
};
export const getLocaleFromHash = (hash: string) => {
// istanbul ignore next
if (typeof window === 'undefined') return null;
return getFromQueryString(window.location.hash.substr(1), hash);
};

View File

@ -1,40 +0,0 @@
import { getMessageFromDictionary } from '../stores/dictionary';
import { getFallbackOf } from '../stores/locale';
export const lookupCache: {
[locale: string]: {
[messageId: string]: any;
};
} = {};
const addToCache = (path: string, locale: string, message: string) => {
if (!message) return message;
if (!(locale in lookupCache)) lookupCache[locale] = {};
if (!(path in lookupCache[locale])) lookupCache[locale][path] = message;
return message;
};
const searchForMessage = (path: string, locale: string): any => {
if (locale == null) return undefined;
const message = getMessageFromDictionary(locale, path);
if (message) return message;
return searchForMessage(path, getFallbackOf(locale));
};
export const lookup = (path: string, locale: string) => {
if (locale in lookupCache && path in lookupCache[locale]) {
return lookupCache[locale][path];
}
const message = searchForMessage(path, locale);
if (message) {
return addToCache(path, locale, message);
}
return undefined;
};

View File

@ -1,19 +0,0 @@
// eslint-disable-next-line @typescript-eslint/ban-types
type MemoizedFunction = <F extends Function>(fn: F) => F;
const monadicMemoize: MemoizedFunction = (fn) => {
const cache = Object.create(null);
const memoizedFn: any = (arg: unknown) => {
const cacheKey = JSON.stringify(arg);
if (cacheKey in cache) {
return cache[cacheKey];
}
return (cache[cacheKey] = fn(arg));
};
return memoizedFn;
};
export { monadicMemoize };

View File

@ -1,66 +0,0 @@
import { MessageObject } from './types';
import { getCurrentLocale, $locale } from './stores/locale';
import { getOptions, init } from './configs';
import { flush, registerLocaleLoader } from './includes/loaderQueue';
import {
getLocaleFromHostname,
getLocaleFromPathname,
getLocaleFromNavigator,
getLocaleFromQueryString,
getLocaleFromHash,
} from './includes/localeGetters';
import { $dictionary, $locales, addMessages } from './stores/dictionary';
import { $isLoading } from './stores/loading';
import {
$format,
$formatDate,
$formatNumber,
$formatTime,
$getJSON,
} from './stores/formatters';
import {
getDateFormatter,
getNumberFormatter,
getTimeFormatter,
getMessageFormatter,
} from './includes/formatters';
// defineMessages allow us to define and extract dynamic message ids
export function defineMessages(i: Record<string, MessageObject>) {
return i;
}
export function waitLocale(locale?: string) {
return flush(locale || getCurrentLocale() || getOptions().initialLocale);
}
export {
// setup
init,
addMessages,
registerLocaleLoader as register,
// stores
$locale as locale,
$dictionary as dictionary,
$locales as locales,
$isLoading as isLoading,
// reactive methods
$format as format,
$format as _,
$format as t,
$formatDate as date,
$formatNumber as number,
$formatTime as time,
$getJSON as json,
// low-level
getDateFormatter,
getNumberFormatter,
getTimeFormatter,
getMessageFormatter,
// utils
getLocaleFromHostname,
getLocaleFromPathname,
getLocaleFromNavigator,
getLocaleFromQueryString,
getLocaleFromHash,
};

View File

@ -1,62 +0,0 @@
import { writable, derived } from 'svelte/store';
import deepmerge from 'deepmerge';
import dlv from 'dlv';
import { LocaleDictionary, LocalesDictionary } from '../types/index';
import { getFallbackOf } from './locale';
let dictionary: LocalesDictionary;
const $dictionary = writable<LocalesDictionary>({});
export function getLocaleDictionary(locale: string) {
return (dictionary[locale] as LocaleDictionary) || null;
}
export function getDictionary() {
return dictionary;
}
export function hasLocaleDictionary(locale: string) {
return locale in dictionary;
}
export function getMessageFromDictionary(locale: string, id: string) {
if (!hasLocaleDictionary(locale)) {
return null;
}
const localeDictionary = getLocaleDictionary(locale);
// flat ids
if (id in localeDictionary) {
return localeDictionary[id];
}
// deep ids
const match = dlv(localeDictionary, id);
return match;
}
export function getClosestAvailableLocale(locale: string): string | null {
if (locale == null || hasLocaleDictionary(locale)) return locale;
return getClosestAvailableLocale(getFallbackOf(locale));
}
export function addMessages(locale: string, ...partials: LocaleDictionary[]) {
$dictionary.update((d) => {
d[locale] = deepmerge.all<LocaleDictionary>([d[locale] || {}, ...partials]);
return d;
});
}
// eslint-disable-next-line no-shadow
const $locales = derived([$dictionary], ([dictionary]) =>
Object.keys(dictionary),
);
$dictionary.subscribe((newDictionary) => (dictionary = newDictionary));
export { $dictionary, $locales };

View File

@ -1,93 +0,0 @@
import { derived } from 'svelte/store';
import {
MessageFormatter,
MessageObject,
TimeFormatter,
DateFormatter,
NumberFormatter,
JSONGetter,
} from '../types';
import { lookup } from '../includes/lookup';
import { hasLocaleQueue } from '../includes/loaderQueue';
import {
getMessageFormatter,
getTimeFormatter,
getDateFormatter,
getNumberFormatter,
} from '../includes/formatters';
import { getOptions } from '../configs';
import { $dictionary } from './dictionary';
import { getCurrentLocale, getRelatedLocalesOf, $locale } from './locale';
const formatMessage: MessageFormatter = (id, options = {}) => {
if (typeof id === 'object') {
options = id as MessageObject;
id = options.id;
}
const {
values,
locale = getCurrentLocale(),
default: defaultValue,
} = options;
if (locale == null) {
throw new Error(
'[svelte-i18n] Cannot format a message without first setting the initial locale.',
);
}
let message = lookup(id, locale);
if (!message) {
if (getOptions().warnOnMissingMessages) {
// istanbul ignore next
console.warn(
`[svelte-i18n] The message "${id}" was not found in "${getRelatedLocalesOf(
locale,
).join('", "')}".${
hasLocaleQueue(getCurrentLocale())
? `\n\nNote: there are at least one loader still registered to this locale that wasn't executed.`
: ''
}`,
);
}
message = defaultValue || id;
} else if (typeof message !== 'string') {
console.warn(
`[svelte-i18n] Message with id "${id}" must be of type "string", found: "${typeof message}". Gettin its value through the "$format" method is deprecated; use the "json" method instead.`,
);
return message;
}
if (!values) {
return message;
}
return getMessageFormatter(message, locale).format(values) as string;
};
const formatTime: TimeFormatter = (t, options) => {
return getTimeFormatter(options).format(t);
};
const formatDate: DateFormatter = (d, options) => {
return getDateFormatter(options).format(d);
};
const formatNumber: NumberFormatter = (n, options) => {
return getNumberFormatter(options).format(n);
};
const getJSON: JSONGetter = <T>(id: string, locale = getCurrentLocale()) => {
return lookup(id, locale) as T;
};
export const $format = derived([$locale, $dictionary], () => formatMessage);
export const $formatTime = derived([$locale], () => formatTime);
export const $formatDate = derived([$locale], () => formatDate);
export const $formatNumber = derived([$locale], () => formatNumber);
export const $getJSON = derived([$locale, $dictionary], () => getJSON);

View File

@ -1,3 +0,0 @@
import { writable } from 'svelte/store';
export const $isLoading = writable(false);

View File

@ -1,103 +0,0 @@
import { writable } from 'svelte/store';
import { flush, hasLocaleQueue } from '../includes/loaderQueue';
import { getOptions } from '../configs';
import { getClosestAvailableLocale } from './dictionary';
import { $isLoading } from './loading';
let current: string;
const $locale = writable(null);
export function isFallbackLocaleOf(localeA: string, localeB: string) {
return localeB.indexOf(localeA) === 0 && localeA !== localeB;
}
export function isRelatedLocale(localeA: string, localeB: string) {
return (
localeA === localeB ||
isFallbackLocaleOf(localeA, localeB) ||
isFallbackLocaleOf(localeB, localeA)
);
}
export function getFallbackOf(locale: string) {
const index = locale.lastIndexOf('-');
if (index > 0) return locale.slice(0, index);
const { fallbackLocale } = getOptions();
if (fallbackLocale && !isRelatedLocale(locale, fallbackLocale)) {
return fallbackLocale;
}
return null;
}
export function getRelatedLocalesOf(locale: string): string[] {
const locales = locale
.split('-')
.map((_, i, arr) => arr.slice(0, i + 1).join('-'));
const { fallbackLocale } = getOptions();
if (fallbackLocale && !isRelatedLocale(locale, fallbackLocale)) {
return locales.concat(getRelatedLocalesOf(fallbackLocale));
}
return locales;
}
export function getCurrentLocale() {
return current;
}
$locale.subscribe((newLocale: string) => {
current = newLocale;
if (typeof window !== 'undefined') {
document.documentElement.setAttribute('lang', newLocale);
}
});
const localeSet = $locale.set;
$locale.set = (newLocale: string): void | Promise<void> => {
if (getClosestAvailableLocale(newLocale) && hasLocaleQueue(newLocale)) {
const { loadingDelay } = getOptions();
let loadingTimer: number;
// if there's no current locale, we don't wait to set isLoading to true
// because it would break pages when loading the initial locale
if (
typeof window !== 'undefined' &&
getCurrentLocale() != null &&
loadingDelay
) {
loadingTimer = window.setTimeout(
() => $isLoading.set(true),
loadingDelay,
);
} else {
$isLoading.set(true);
}
return flush(newLocale)
.then(() => {
localeSet(newLocale);
})
.finally(() => {
clearTimeout(loadingTimer);
$isLoading.set(false);
});
}
return localeSet(newLocale);
};
// istanbul ignore next
$locale.update = (fn: (locale: string) => void | Promise<void>) =>
localeSet(fn(current));
export { $locale };

View File

@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"declarationDir": "../../types/runtime"
},
"include": ["."]
}

View File

@ -1,73 +0,0 @@
import type { FormatXMLElementFn, Formats } from 'intl-messageformat';
export interface LocaleDictionary {
[key: string]: LocaleDictionary | string | Array<string | LocaleDictionary>;
}
export type LocalesDictionary = {
[key: string]: LocaleDictionary;
};
export type InterpolationValues =
| Record<
string,
| string
| number
| boolean
| Date
| FormatXMLElementFn<unknown>
| null
| undefined
>
| undefined;
export interface MessageObject {
id?: string;
locale?: string;
format?: string;
default?: string;
values?: InterpolationValues;
}
export type MessageFormatter = (
id: string | MessageObject,
options?: MessageObject,
) => string;
export type TimeFormatter = (
d: Date | number,
options?: IntlFormatterOptions<Intl.DateTimeFormatOptions>,
) => string;
export type DateFormatter = (
d: Date | number,
options?: IntlFormatterOptions<Intl.DateTimeFormatOptions>,
) => string;
export type NumberFormatter = (
d: number,
options?: IntlFormatterOptions<Intl.NumberFormatOptions>,
) => string;
export type JSONGetter = <T extends any>(id: string, locale?: string) => T;
type IntlFormatterOptions<T> = T & {
format?: string;
locale?: string;
};
export interface MemoizedIntlFormatter<T, U> {
(options?: IntlFormatterOptions<U>): T;
}
export interface MessagesLoader {
(): Promise<any>;
}
export interface ConfigureOptions {
fallbackLocale: string;
formats?: Partial<Formats>;
initialLocale?: string;
loadingDelay?: number;
warnOnMissingMessages?: boolean;
}

8
src/tsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"declaration": true,
"declarationDir": "../types/runtime"
},
"include": ["."]
}

View File

@ -1,290 +0,0 @@
import { parse } from 'svelte/compiler';
import {
collectFormatCalls,
collectMessageDefinitions,
collectMessages,
extractMessages,
} from '../../src/cli/extract';
describe('collecting format calls', () => {
it('returns nothing if there are no script tag', () => {
const ast = parse(`<div>Hey</div>`);
const calls = collectFormatCalls(ast);
expect(calls).toHaveLength(0);
});
it('returns nothing if there are no imports', () => {
const ast = parse(`<script>
import Foo from 'foo';
const $_ = () => 0; $_();
</script>`);
const calls = collectFormatCalls(ast);
expect(calls).toHaveLength(0);
});
it('returns nothing if there are no format imports', () => {
const ast = parse(
`<script>
import { init } from 'svelte-i18n';
init({})
</script>`,
);
const calls = collectFormatCalls(ast);
expect(calls).toHaveLength(0);
});
it('collects all format calls in the instance script', () => {
const ast = parse(`<script>
import { format, _ } from 'svelte-i18n'
$format('foo')
format('bar')
let label = $_({id:'bar'})
const a = { b: () => 0}
</script>`);
const calls = collectFormatCalls(ast);
expect(calls).toHaveLength(2);
expect(calls[0]).toMatchObject({ type: 'CallExpression' });
expect(calls[1]).toMatchObject({ type: 'CallExpression' });
});
it('collects all format calls with renamed imports', () => {
const ast = parse(`<script>
import { format as _x, _ as intl, t as f } from 'svelte-i18n'
$_x('foo')
$intl({ id: 'bar' })
$f({ id: 'bar' })
</script>`);
const calls = collectFormatCalls(ast);
expect(calls).toHaveLength(3);
expect(calls[0]).toMatchObject({ type: 'CallExpression' });
expect(calls[1]).toMatchObject({ type: 'CallExpression' });
expect(calls[2]).toMatchObject({ type: 'CallExpression' });
});
});
describe('collecting message definitions', () => {
it('returns nothing if there are no imports from the library', () => {
const ast = parse(
`<script>
import foo from 'foo';
import { dictionary } from 'svelte-i18n';
</script>`,
);
expect(collectMessageDefinitions(ast)).toHaveLength(0);
});
it('gets all message definition objects', () => {
const ast = parse(`<script>
import { defineMessages } from 'svelte-i18n';
defineMessages({ foo: { id: 'foo' }, bar: { id: 'bar' } })
defineMessages({ baz: { id: 'baz' }, quix: { id: 'qux' } })
</script>`);
const definitions = collectMessageDefinitions(ast);
expect(definitions).toHaveLength(4);
expect(definitions[0]).toMatchObject({ type: 'ObjectExpression' });
expect(definitions[1]).toMatchObject({ type: 'ObjectExpression' });
expect(definitions[2]).toMatchObject({ type: 'ObjectExpression' });
expect(definitions[3]).toMatchObject({ type: 'ObjectExpression' });
});
it('throws an error if an spread is found', () => {
const ast = parse(`<script>
import { defineMessages } from 'svelte-i18n';
const potato = { foo: { id: 'foo' }, bar: { id: 'bar' } }
defineMessages({ ...potato })
</script>`);
expect(() =>
collectMessageDefinitions(ast),
).toThrowErrorMatchingInlineSnapshot(
`"Found invalid 'SpreadElement' at L4:23"`,
);
});
});
describe('collecting messages', () => {
it('collects all messages in both instance and html ASTs', () => {
const markup = `
<script>
import { _, defineMessages } from 'svelte-i18n';
console.log($_({ id: 'foo' }))
console.log($_({ 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({ id: 'foo' }),
expect.objectContaining({ id: 'msg_1' }),
expect.objectContaining({ id: 'msg_2' }),
expect.objectContaining({ id: 'msg_3', default: 'Message' }),
expect.objectContaining({ id: 'page.title' }),
expect.objectContaining({
id: 'disabled',
default: 'Disabled',
}),
expect.objectContaining({
id: 'enabled',
default: 'Enabled',
}),
]),
);
});
it('ignores non-static message ids', () => {
const markup = `
<script>
import { _, defineMessages } from 'svelte-i18n';
$_({ id: 'foo' + i })
$_('bar' + i)
</script>`;
const messages = collectMessages(markup);
expect(messages).toHaveLength(0);
});
});
describe('messages extraction', () => {
it('returns a object built based on all found message paths', () => {
const markup = `<script>
import { _ } from 'svelte-i18n';
</script>
<h1>{$_('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>{$_('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>{$_('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>{$_('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>{$_('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': '',
});
});
});

View File

@ -1,3 +0,0 @@
{
"sneakers": "trainers"
}

View File

@ -1,9 +0,0 @@
{
"form": {
"field_1_name": "Name",
"field_2_name": "Lastname"
},
"photos": "You have {n, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}",
"title": "Page title",
"sneakers": "sneakers"
}

View File

@ -1,8 +0,0 @@
{
"form": {
"field_1_name": "Nombre",
"field_2_name": "Apellido"
},
"title": "Título de la página",
"sneakers": "zapatillas"
}

View File

@ -1,16 +0,0 @@
{
"number": {
"usd": { "style": "currency", "currency": "USD" },
"brl": { "style": "currency", "currency": "BRL" }
},
"date": {
"customDate": { "year": "numeric", "era": "short" }
},
"time": {
"customTime": {
"hour": "2-digit",
"minute": "2-digit",
"second": "2-digit"
}
}
}

View File

@ -1,5 +0,0 @@
{
"page": {
"title_about": "About"
}
}

View File

@ -1,3 +0,0 @@
{
"french_bread": "pão francês"
}

View File

@ -1,3 +0,0 @@
{
"french_bread": "cacetinhos"
}

View File

@ -1,9 +0,0 @@
{
"form": {
"field_1_name": "Nome",
"field_2_name": "Sobrenome"
},
"photos": "Você {n, plural, =0 {não tem fotos.} =1 {tem uma foto.} other {tem # fotos.}}",
"title": "Título da página",
"sneakers": "tênis"
}

View File

@ -1,5 +0,0 @@
const preprocess = require('svelte-preprocess')
module.exports = {
preprocess: preprocess(),
}

View File

@ -1,46 +0,0 @@
import { get } from 'svelte/store';
import {
init,
getOptions,
defaultOptions,
defaultFormats,
} from '../../src/runtime/configs';
import { $locale } from '../../src/runtime/stores/locale';
beforeEach(() => {
init(defaultOptions);
});
test('inits the fallback locale', () => {
expect(getOptions().fallbackLocale).toBeNull();
init({
fallbackLocale: 'en',
});
expect(getOptions().fallbackLocale).toBe('en');
});
test('inits the initial locale by string', () => {
init({
fallbackLocale: 'pt',
initialLocale: 'en',
});
expect(getOptions().initialLocale).toBe('en');
expect(get($locale)).toBe('en');
});
test('adds custom formats for time, date and number values', () => {
const customFormats = require('../fixtures/formats.json');
init({
fallbackLocale: 'en',
formats: customFormats,
});
expect(getOptions().formats).toMatchObject(defaultFormats);
expect(getOptions().formats).toMatchObject(customFormats);
});
test('sets the minimum delay to set the loading store value', () => {
init({ fallbackLocale: 'en', loadingDelay: 300 });
expect(getOptions().loadingDelay).toBe(300);
});

View File

@ -1,198 +0,0 @@
import {
getNumberFormatter,
getDateFormatter,
getTimeFormatter,
getMessageFormatter,
init,
locale,
} from '../../../src/runtime';
beforeEach(() => {
init({ fallbackLocale: undefined });
});
describe('number formatter', () => {
const number = 123123;
it('throws if no locale is set', () => {
expect(() => getNumberFormatter().format(number)).toThrow(
'[svelte-i18n] A "locale" must be set to format numbers',
);
});
it('formats a number according to the current locale', () => {
init({ fallbackLocale: 'en' });
expect(getNumberFormatter().format(number)).toBe('123,123');
});
it('formats a number according to a locale', () => {
init({ fallbackLocale: 'en' });
expect(getNumberFormatter({ locale: 'pt-BR' }).format(number)).toBe(
'123.123',
);
});
it('formats a number with a custom format', () => {
init({
fallbackLocale: 'en',
formats: require('../../fixtures/formats.json'),
});
expect(getNumberFormatter({ format: 'brl' }).format(number)).toBe(
'R$123,123.00',
);
});
it('formats a number with inline options', () => {
init({ fallbackLocale: 'en' });
expect(
getNumberFormatter({ style: 'currency', currency: 'BRL' }).format(number),
).toBe('R$123,123.00');
});
it('formats a number according to the currently set locale', () => {
locale.set('en');
expect(getNumberFormatter().format(number)).toBe('123,123');
locale.set('nl');
expect(getNumberFormatter().format(number)).toBe('123.123');
});
});
describe('date formatter', () => {
const date = new Date(2019, 1, 1);
it('throws if no locale is set', () => {
expect(() => getDateFormatter().format(date)).toThrow(
'[svelte-i18n] A "locale" must be set to format dates',
);
});
it('formats a date according to the current locale', () => {
init({ fallbackLocale: 'en' });
expect(getDateFormatter().format(date)).toBe('2/1/19');
});
it('formats a date according to a locale', () => {
expect(getDateFormatter({ locale: 'pt-BR' }).format(date)).toBe('01/02/19');
});
it('throws if passed a non-existing format', () => {
init({
fallbackLocale: 'en',
formats: require('../../fixtures/formats.json'),
});
expect(() =>
getDateFormatter({ locale: 'pt-BR', format: 'foo' }).format(date),
).toThrowError(`[svelte-i18n] Unknown "foo" date format.`);
});
it('formats a date with a custom format', () => {
init({
fallbackLocale: 'en',
formats: require('../../fixtures/formats.json'),
});
expect(getDateFormatter({ format: 'customDate' }).format(date)).toBe(
'2019 AD',
);
});
it('formats a date with inline options', () => {
init({ fallbackLocale: 'en' });
expect(
getDateFormatter({ year: 'numeric', era: 'short' }).format(date),
).toBe('2019 AD');
});
it('formats a date according to the currently set locale', () => {
locale.set('en');
expect(getDateFormatter().format(date)).toBe('2/1/19');
locale.set('nl');
expect(getDateFormatter().format(date)).toBe('1-2-19');
});
});
describe('time formatter', () => {
const time = new Date(2019, 1, 1, 20, 37, 32);
it('throws if no locale is set', () => {
expect(() => getTimeFormatter().format(time)).toThrow(
'[svelte-i18n] A "locale" must be set to format time',
);
});
it('formats a time according to the current locale', () => {
init({ fallbackLocale: 'en' });
expect(getTimeFormatter().format(time)).toBe('8:37 PM');
});
it('formats a time according to a locale', () => {
expect(getTimeFormatter({ locale: 'pt-BR' }).format(time)).toBe('20:37');
});
it('formats a time with a custom format', () => {
init({
fallbackLocale: 'en',
formats: require('../../fixtures/formats.json'),
});
expect(getTimeFormatter({ format: 'customTime' }).format(time)).toBe(
'08:37:32 PM',
);
});
it('throws if passed a non-existing format', () => {
init({
fallbackLocale: 'en',
formats: require('../../fixtures/formats.json'),
});
expect(() =>
getTimeFormatter({ locale: 'pt-BR', format: 'foo' }).format(time),
).toThrowError(`[svelte-i18n] Unknown "foo" time format.`);
});
it('formats a time with inline options', () => {
init({ fallbackLocale: 'en' });
expect(
getTimeFormatter({
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}).format(time),
).toBe('08:37:32 PM');
});
it('formats time according to the currently set locale', () => {
locale.set('en');
expect(getTimeFormatter().format(time)).toBe('8:37 PM');
locale.set('nl');
expect(getTimeFormatter().format(time)).toBe('20:37');
});
});
describe('message formatter', () => {
it('formats a message with interpolated values', () => {
expect(
getMessageFormatter('Page: {current,number}/{max,number}', 'en').format({
current: 2,
max: 10,
}),
).toBe('Page: 2/10');
});
it('formats number with custom formats', () => {
expect(
getMessageFormatter('Number: {n, number, compactShort}', 'en').format({
n: 2000000,
}),
).toBe('Number: 2M');
});
});

View File

@ -1,71 +0,0 @@
import {
hasLocaleQueue,
flush,
registerLocaleLoader,
resetQueues,
} from '../../../src/runtime/includes/loaderQueue';
import { getMessageFromDictionary } from '../../../src/runtime/stores/dictionary';
beforeEach(() => {
resetQueues();
});
const loader = (content: any) => () => new Promise((res) => res(content));
test('registers a locale loader', () => {
expect(hasLocaleQueue('pt-BR')).toBe(false);
registerLocaleLoader('pt-BR', loader({ message: 'Mensagem' }));
expect(hasLocaleQueue('pt-BR')).toBe(true);
});
test('checks if exist queues of locale and its fallbacks', () => {
registerLocaleLoader('en', loader({ field: 'Name' }));
expect(hasLocaleQueue('en-US')).toBe(true);
});
test("does nothing if there's no queue for a locale", () => {
expect(flush('foo')).toBeUndefined();
});
test('flushes the queue of a locale and its fallbacks and merge the result with the dictionary', async () => {
registerLocaleLoader('en', loader({ field: 'Name' }));
registerLocaleLoader('en-US', loader({ field_2: 'Lastname' }));
await flush('en-US');
expect(getMessageFromDictionary('en', 'field')).toBe('Name');
expect(getMessageFromDictionary('en-US', 'field_2')).toBe('Lastname');
expect(hasLocaleQueue('en')).toBe(false);
expect(hasLocaleQueue('en-US')).toBe(false);
});
test('consecutive flushes return the same promise', async () => {
registerLocaleLoader('en', async () => ({}));
const flushA = flush('en');
const flushB = flush('en');
const flushC = flush('en');
expect(flushB).toStrictEqual(flushA);
expect(flushC).toStrictEqual(flushA);
});
test('waits for loaders added while already flushing', async () => {
registerLocaleLoader(
'en',
() => new Promise((res) => setTimeout(() => res({ foo: 'foo' }), 300)),
);
const flushPromise = flush('en');
registerLocaleLoader(
'en',
() => new Promise((res) => setTimeout(() => res({ bar: 'bar' }))),
);
await flushPromise;
expect(getMessageFromDictionary('en', 'foo')).toBe('foo');
expect(getMessageFromDictionary('en', 'bar')).toBe('bar');
});

View File

@ -1,85 +0,0 @@
import { lookup, lookupCache } from '../../../src/runtime/includes/lookup';
import {
$dictionary,
addMessages,
} from '../../../src/runtime/stores/dictionary';
beforeEach(() => {
$dictionary.set({});
});
test('returns null if no locale was passed', () => {
expect(lookup('message.id', undefined)).toBeUndefined();
expect(lookup('message.id', null)).toBeUndefined();
});
test('gets a shallow message of a locale dictionary', () => {
addMessages('en', { field: 'name' });
expect(lookup('field', 'en')).toBe('name');
});
test('gets a deep message of a locale dictionary', () => {
addMessages('en', { deep: { field: 'lastname' } });
expect(lookup('deep.field', 'en')).toBe('lastname');
});
test('gets a message from the fallback dictionary', () => {
addMessages('en', { field: 'name' });
expect(lookup('field', 'en-US')).toBe('name');
});
test('gets an array ', () => {
addMessages('en', {
careers: [
{
role: 'Role 1',
description: 'Description 1',
},
{
role: 'Role 2',
description: 'Description 2',
},
],
});
expect(lookup('careers', 'en-US')).toMatchInlineSnapshot(`
Array [
Object {
"description": "Description 1",
"role": "Role 1",
},
Object {
"description": "Description 2",
"role": "Role 2",
},
]
`);
});
test('caches found messages by locale', () => {
addMessages('en', { field: 'name' });
addMessages('pt', { field: 'nome' });
lookup('field', 'en-US');
lookup('field', 'pt');
expect(lookupCache).toMatchObject({
'en-US': { field: 'name' },
pt: { field: 'nome' },
});
});
test("doesn't cache falsy messages", () => {
addMessages('en', { field: 'name' });
addMessages('pt', { field: 'nome' });
lookup('field_2', 'en-US');
lookup('field_2', 'pt');
expect(lookupCache).not.toMatchObject({
'en-US': { field_2: 'name' },
pt: { field_2: 'nome' },
});
});

View File

@ -1,47 +0,0 @@
import {
getLocaleFromQueryString,
getLocaleFromHash,
getLocaleFromNavigator,
getLocaleFromPathname,
getLocaleFromHostname,
} from '../../../src/runtime/includes/localeGetters';
describe('getting client locale', () => {
beforeEach(() => {
delete window.location;
window.location = {
pathname: '/',
hostname: 'example.com',
hash: '',
search: '',
} as any;
});
it('gets the locale based on the passed hash parameter', () => {
window.location.hash = '#locale=en-US&lang=pt-BR';
expect(getLocaleFromHash('lang')).toBe('pt-BR');
});
it('gets the locale based on the passed search parameter', () => {
window.location.search = '?locale=en-US&lang=pt-BR';
expect(getLocaleFromQueryString('lang')).toBe('pt-BR');
});
it('gets the locale based on the navigator language', () => {
expect(getLocaleFromNavigator()).toBe(window.navigator.language);
});
it('gets the locale based on the pathname', () => {
window.location.pathname = '/en-US/foo/';
expect(getLocaleFromPathname(/^\/(.*?)\//)).toBe('en-US');
});
it('gets the locale base on the hostname', () => {
window.location.hostname = 'pt.example.com';
expect(getLocaleFromHostname(/^(.*?)\./)).toBe('pt');
});
it('returns null if no locale was found', () => {
expect(getLocaleFromQueryString('lang')).toBeNull();
});
});

View File

@ -1,75 +0,0 @@
import { defineMessages, waitLocale, register, init } from '../../src/runtime';
import { $locale } from '../../src/runtime/stores/locale';
import { hasLocaleQueue } from '../../src/runtime/includes/loaderQueue';
import {
getLocaleDictionary,
$dictionary,
} from '../../src/runtime/stores/dictionary';
import { $format } from '../../src/runtime/stores/formatters';
test('defineMessages returns the identity of its first argument', () => {
const obj = {};
expect(obj).toBe(defineMessages(obj));
});
describe('waiting for a locale to load', () => {
beforeEach(() => {
$dictionary.set({});
$locale.set(undefined);
});
it('should wait for a locale queue to be flushed', async () => {
register('en', () => Promise.resolve({ foo: 'foo' }));
$locale.set('en');
await waitLocale('en');
expect(hasLocaleQueue('en')).toBe(false);
expect(getLocaleDictionary('en')).toMatchObject({ foo: 'foo' });
});
it('should wait for the current locale queue to be flushed', async () => {
register('en', () => Promise.resolve({ foo: 'foo' }));
init({ fallbackLocale: 'pt', initialLocale: 'en' });
await waitLocale();
expect(hasLocaleQueue('en')).toBe(false);
expect(getLocaleDictionary('en')).toMatchObject({ foo: 'foo' });
});
it('should wait for the fallback locale queue to be flushed if initial not set', async () => {
register('pt', () => Promise.resolve({ foo: 'foo' }));
init({ fallbackLocale: 'pt' });
await waitLocale();
expect(hasLocaleQueue('pt')).toBe(false);
expect(getLocaleDictionary('pt')).toMatchObject({ foo: 'foo' });
});
});
describe('format updates', () => {
beforeEach(() => {
init({ fallbackLocale: 'en' });
});
it('format store is updated when locale changes', () => {
const fn = jest.fn();
const cancel = $format.subscribe(fn);
$locale.set('pt');
expect(fn).toHaveBeenCalledTimes(2);
cancel();
});
it('format store is updated when dictionary changes', () => {
const fn = jest.fn();
const cancel = $format.subscribe(fn);
$dictionary.set({});
expect(fn).toHaveBeenCalledTimes(2);
cancel();
});
});

View File

@ -1,113 +0,0 @@
import { get } from 'svelte/store';
import {
getDictionary,
hasLocaleDictionary,
getClosestAvailableLocale,
getMessageFromDictionary,
addMessages,
$dictionary,
$locales,
getLocaleDictionary,
} from '../../../src/runtime/stores/dictionary';
beforeEach(() => {
$dictionary.set({});
});
test('adds a new dictionary to a locale', () => {
addMessages('en', { field_1: 'name' });
addMessages('pt', { field_1: 'nome' });
expect(get($dictionary)).toMatchObject({
en: { field_1: 'name' },
pt: { field_1: 'nome' },
});
});
test('gets the whole current dictionary', () => {
addMessages('en', { field_1: 'name' });
expect(getDictionary()).toMatchObject(get($dictionary));
});
test('merges the existing dictionaries with new ones', () => {
addMessages('en', { field_1: 'name', deep: { prop1: 'foo' } });
addMessages('en', { field_2: 'lastname', deep: { prop2: 'foo' } });
addMessages('pt', { field_1: 'nome', deep: { prop1: 'foo' } });
addMessages('pt', { field_2: 'sobrenome', deep: { prop2: 'foo' } });
expect(get($dictionary)).toMatchObject({
en: {
field_1: 'name',
field_2: 'lastname',
deep: { prop1: 'foo', prop2: 'foo' },
},
pt: {
field_1: 'nome',
field_2: 'sobrenome',
deep: { prop1: 'foo', prop2: 'foo' },
},
});
});
test('gets the dictionary of a locale', () => {
addMessages('en', { field_1: 'name' });
expect(getLocaleDictionary('en')).toMatchObject({ field_1: 'name' });
});
test('checks if a locale dictionary exists', () => {
addMessages('pt', { field_1: 'name' });
expect(hasLocaleDictionary('en')).toBe(false);
expect(hasLocaleDictionary('pt')).toBe(true);
});
test('gets the closest available locale', () => {
addMessages('pt', { field_1: 'name' });
expect(getClosestAvailableLocale('pt-BR')).toBe('pt');
});
test("returns null if there's no closest locale available", () => {
addMessages('pt', { field_1: 'name' });
expect(getClosestAvailableLocale('it-IT')).toBeNull();
});
test('lists all locales in the dictionary', () => {
addMessages('en', {});
addMessages('pt', {});
addMessages('pt-BR', {});
expect(get($locales)).toEqual(['en', 'pt', 'pt-BR']);
});
describe('getting messages', () => {
it('gets a message from a shallow dictionary', () => {
addMessages('en', { message: 'Some message' });
expect(getMessageFromDictionary('en', 'message')).toBe('Some message');
});
it('gets a message from a deep object in the dictionary', () => {
addMessages('en', { messages: { message_1: 'Some message' } });
expect(getMessageFromDictionary('en', 'messages.message_1')).toBe(
'Some message',
);
});
it('gets a message from an array in the dictionary', () => {
addMessages('en', { messages: ['Some message', 'Other message'] });
expect(getMessageFromDictionary('en', 'messages.0')).toBe('Some message');
expect(getMessageFromDictionary('en', 'messages.1')).toBe('Other message');
});
it('accepts english in dictionary keys', () => {
addMessages('pt', {
'Hey man. How are you today?': 'E ai cara, como você vai hoje?',
});
expect(getMessageFromDictionary('pt', 'Hey man. How are you today?')).toBe(
'E ai cara, como você vai hoje?',
);
});
it('returns undefined for missing messages', () => {
addMessages('en', {});
expect(getMessageFromDictionary('en', 'foo')).toBeUndefined();
});
});

View File

@ -1,154 +0,0 @@
import { get } from 'svelte/store';
import {
JSONGetter,
MessageFormatter,
TimeFormatter,
DateFormatter,
NumberFormatter,
} from '../../../src/runtime/types/index';
import {
$format,
$formatTime,
$formatDate,
$formatNumber,
$getJSON,
} from '../../../src/runtime/stores/formatters';
import { init } from '../../../src/runtime/configs';
import { addMessages } from '../../../src/runtime/stores/dictionary';
import { $locale } from '../../../src/runtime/stores/locale';
let formatMessage: MessageFormatter;
let formatTime: TimeFormatter;
let formatDate: DateFormatter;
let formatNumber: NumberFormatter;
let getJSON: JSONGetter;
$locale.subscribe(() => {
formatMessage = get($format);
formatTime = get($formatTime);
formatDate = get($formatDate);
formatNumber = get($formatNumber);
getJSON = get($getJSON);
});
addMessages('en', require('../../fixtures/en.json'));
addMessages('en-GB', require('../../fixtures/en-GB.json'));
addMessages('pt', require('../../fixtures/pt.json'));
addMessages('pt-BR', require('../../fixtures/pt-BR.json'));
addMessages('pt-PT', require('../../fixtures/pt-PT.json'));
beforeEach(() => {
init({ fallbackLocale: 'en' });
});
describe('format message', () => {
it('formats a message by its id and the current locale', () => {
expect(formatMessage({ id: 'form.field_1_name' })).toBe('Name');
});
it('formats a message by its id and the a passed locale', () => {
expect(formatMessage({ id: 'form.field_1_name', locale: 'pt' })).toBe(
'Nome',
);
});
it('formats a message with interpolated values', () => {
expect(formatMessage({ id: 'photos', values: { n: 0 } })).toBe(
'You have no photos.',
);
expect(formatMessage({ id: 'photos', values: { n: 1 } })).toBe(
'You have one photo.',
);
expect(formatMessage({ id: 'photos', values: { n: 21 } })).toBe(
'You have 21 photos.',
);
});
it('formats the default value with interpolated values', () => {
expect(
formatMessage({
id: 'non-existent',
default: '{food}',
values: { food: 'potato' },
}),
).toBe('potato');
});
it('formats the key with interpolated values', () => {
expect(
formatMessage({
id: '{food}',
values: { food: 'potato' },
}),
).toBe('potato');
});
it('accepts a message id as first argument', () => {
expect(formatMessage('form.field_1_name')).toBe('Name');
});
it('accepts a message id as first argument and formatting options as second', () => {
expect(formatMessage('form.field_1_name', { locale: 'pt' })).toBe('Nome');
});
it('throws if no locale is set', () => {
$locale.set(null);
expect(() => formatMessage('form.field_1_name')).toThrow(
'[svelte-i18n] Cannot format a message without first setting the initial locale.',
);
});
it('uses a missing message default value', () => {
expect(formatMessage('missing', { default: 'Missing Default' })).toBe(
'Missing Default',
);
});
it('errors out when value found is not string', () => {
const { warn } = global.console;
jest.spyOn(global.console, 'warn').mockImplementation();
expect(typeof formatMessage('form')).toBe('object');
expect(console.warn).toBeCalledWith(
`[svelte-i18n] Message with id "form" must be of type "string", found: "object". Gettin its value through the "$format" method is deprecated; use the "json" method instead.`,
);
global.console.warn = warn;
});
it('warn on missing messages', () => {
const { warn } = global.console;
jest.spyOn(global.console, 'warn').mockImplementation();
formatMessage('missing');
expect(console.warn).toBeCalledWith(
`[svelte-i18n] The message "missing" was not found in "en".`,
);
global.console.warn = warn;
});
});
test('format time', () => {
expect(formatTime(new Date(2019, 0, 1, 20, 37))).toBe('8:37 PM');
});
test('format date', () => {
expect(formatDate(new Date(2019, 0, 1, 20, 37))).toBe('1/1/19');
});
test('format number', () => {
expect(formatNumber(123123123)).toBe('123,123,123');
});
test('get raw JSON data from the current locale dictionary', () => {
expect(getJSON('form')).toMatchObject({
field_1_name: 'Name',
field_2_name: 'Lastname',
});
expect(getJSON('non-existing')).toBeUndefined();
});

View File

@ -1,169 +0,0 @@
import { get } from 'svelte/store';
import { lookup } from '../../../src/runtime/includes/lookup';
import {
isFallbackLocaleOf,
getFallbackOf,
getRelatedLocalesOf,
getCurrentLocale,
$locale,
isRelatedLocale,
} from '../../../src/runtime/stores/locale';
import { getOptions, init } from '../../../src/runtime/configs';
import { register, isLoading } from '../../../src/runtime';
import { hasLocaleQueue } from '../../../src/runtime/includes/loaderQueue';
beforeEach(() => {
init({ fallbackLocale: undefined });
$locale.set(undefined);
});
test('sets and gets the fallback locale', () => {
init({ fallbackLocale: 'en' });
expect(getOptions().fallbackLocale).toBe('en');
});
test('checks if a locale is a fallback locale of another locale', () => {
expect(isFallbackLocaleOf('en', 'en-US')).toBe(true);
expect(isFallbackLocaleOf('en', 'en')).toBe(false);
expect(isFallbackLocaleOf('it', 'en-US')).toBe(false);
});
test('checks if a locale is a related locale of another locale', () => {
expect(isRelatedLocale('en', 'en-US')).toBe(true);
expect(isRelatedLocale('pt-BR', 'pt')).toBe(true);
expect(isRelatedLocale('en', 'en')).toBe(true);
expect(isRelatedLocale('en', 'it-IT')).toBe(false);
expect(isRelatedLocale('en-US', 'it')).toBe(false);
});
test('gets the next fallback locale of a locale', () => {
expect(getFallbackOf('az-Cyrl-AZ')).toBe('az-Cyrl');
expect(getFallbackOf('en-US')).toBe('en');
expect(getFallbackOf('en')).toBeNull();
});
test('gets the global fallback locale if set', () => {
init({ fallbackLocale: 'en' });
expect(getFallbackOf('it')).toBe('en');
});
test('should not get the global fallback as the fallback of itself', () => {
init({ fallbackLocale: 'en' });
expect(getFallbackOf('en')).toBeNull();
});
test('if global fallback locale has a fallback, it should return it', () => {
init({ fallbackLocale: 'en-US' });
expect(getFallbackOf('en-US')).toBe('en');
});
test('gets all fallback locales of a locale', () => {
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US']);
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US']);
expect(getRelatedLocalesOf('az-Cyrl-AZ')).toEqual([
'az',
'az-Cyrl',
'az-Cyrl-AZ',
]);
});
test('gets all fallback locales of a locale including the global fallback locale', () => {
init({ fallbackLocale: 'pt' });
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt']);
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt']);
expect(getRelatedLocalesOf('az-Cyrl-AZ')).toEqual([
'az',
'az-Cyrl',
'az-Cyrl-AZ',
'pt',
]);
});
test('gets all fallback locales of a locale including the global fallback locale and its fallbacks', () => {
init({ fallbackLocale: 'pt-BR' });
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt', 'pt-BR']);
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt', 'pt-BR']);
expect(getRelatedLocalesOf('az-Cyrl-AZ')).toEqual([
'az',
'az-Cyrl',
'az-Cyrl-AZ',
'pt',
'pt-BR',
]);
});
test("don't list fallback locale twice", () => {
init({ fallbackLocale: 'pt-BR' });
expect(getRelatedLocalesOf('pt-BR')).toEqual(['pt', 'pt-BR']);
expect(getRelatedLocalesOf('pt')).toEqual(['pt']);
});
test('gets the current locale', () => {
expect(getCurrentLocale()).toBeUndefined();
$locale.set('es-ES');
expect(getCurrentLocale()).toBe('es-ES');
});
test('if no initial locale is set, set the locale to the fallback', () => {
init({ fallbackLocale: 'pt' });
expect(get($locale)).toBe('pt');
expect(getOptions().fallbackLocale).toBe('pt');
});
test('if no initial locale was found, set to the fallback locale', () => {
init({
fallbackLocale: 'en',
initialLocale: null,
});
expect(get($locale)).toBe('en');
expect(getOptions().fallbackLocale).toBe('en');
});
test('should flush the queue of the locale when changing the store value', async () => {
register(
'en',
() => new Promise((res) => setTimeout(() => res({ foo: 'Foo' }), 50)),
);
expect(hasLocaleQueue('en')).toBe(true);
await $locale.set('en');
expect(hasLocaleQueue('en')).toBe(false);
expect(lookup('foo', 'en')).toBe('Foo');
});
test('if no locale is set, ignore the loading delay', async () => {
register(
'en',
() => new Promise((res) => setTimeout(() => res({ foo: 'Foo' }), 50)),
);
const promise = $locale.set('en');
expect(get(isLoading)).toBe(true);
await promise;
expect(get(isLoading)).toBe(false);
});
test("if a locale is set, don't ignore the loading delay", async () => {
register(
'en',
() => new Promise((res) => setTimeout(() => res({ foo: 'Foo' }), 50)),
);
register(
'pt',
() => new Promise((res) => setTimeout(() => res({ foo: 'Foo' }), 50)),
);
await $locale.set('en');
const promise = $locale.set('pt');
expect(get(isLoading)).toBe(false);
await promise;
expect(get(isLoading)).toBe(false);
});

View File

@ -9,7 +9,6 @@
"resolveJsonModule": true,
"target": "es2017",
"lib": ["es2018", "dom", "esnext"],
"outDir": "dist",
"types": ["svelte", "jest"]
"outDir": "dist"
}
}

1203
yarn.lock

File diff suppressed because it is too large Load Diff