aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--doc/i18n-technical.md47
-rw-r--r--doc/i18n.md71
-rw-r--r--doc/img/add_locale_github.pngbin0 -> 99036 bytes
-rw-r--r--doc/img/create_locale_github.pngbin0 -> 64118 bytes
-rw-r--r--doc/img/translate_github.pngbin0 -> 78506 bytes
-rw-r--r--package.json5
-rw-r--r--pnpm-lock.yaml80
-rw-r--r--src/components/LandingHeader.svelte37
-rw-r--r--src/components/LocaleSelector.svelte10
-rw-r--r--src/components/index.js1
-rw-r--r--src/lib/i18n.js48
-rw-r--r--src/locales/en.json10
-rw-r--r--src/locales/fr.json10
-rw-r--r--src/routes/[username]/settings.svelte7
-rw-r--r--src/routes/__layout.svelte14
16 files changed, 315 insertions, 29 deletions
diff --git a/README.md b/README.md
index 5ab2c1f6..f86424f0 100644
--- a/README.md
+++ b/README.md
@@ -9,3 +9,7 @@ this repository is the front-end code for the coinos web application, it uses `s
## commands
to get started clone the repo and run `pnpm install`, then `pnpm run dev` to get your dev environment setup, and `pnpm format` to format the codebase when making changes.
+
+## Internationalization
+
+CoinOS has a system for translation using [`svelte-i18n`](https://www.npmjs.com/package/svelte-i18n). For more information and instructions on how to translate CoinOS, click [here](./doc/i18n.md). For technical details, click [here](./doc/i18n-technical.md).
diff --git a/doc/i18n-technical.md b/doc/i18n-technical.md
new file mode 100644
index 00000000..6c45ee01
--- /dev/null
+++ b/doc/i18n-technical.md
@@ -0,0 +1,47 @@
+# CoinOS Internationalization - Technical Information
+
+_NOTE: This documentation was written for people with a technical background. If you do not have a technical background, consider reading [this guide](./i18n.md) instead._
+
+Last Updated: 2022-08-08
+
+## Dependencies
+
+CoinOS's internationalization system depends on the `npm` package [`svelte-i18n`](https://www.npmjs.com/package/svelte-i18n). Consider reading [its documentation](https://github.com/kaisermann/svelte-i18n/blob/HEAD/docs/Getting%20Started.md) in addition to this.
+
+## Internationalization Code
+
+The main code for CoinOS's internationalization is at `src/lib/i18n.js`. When imported, it does the following:
+
+1. Load all of the locales [asynchronously](https://github.com/kaisermann/svelte-i18n/blob/main/docs/Getting%20Started.md#32-asynchronous).
+2. Pick a locale intelligently, depending on the available locales and the navigator locale, using `pickLocale`. Specifically, if the user's locale (e.g. `en-US`) isn't available but its non-regional version (e.g. `en`) is, the system will use the non-regional locale.
+3. Initialize `svelte-i18n`.
+
+It also defines important constants, including the list of available locales and the default locale. This code is loaded by importing `$lib/i18n` in `src/routes/__layout.svelte`.
+
+## Locale Files
+
+Each locale in `coinos-ui-v2` is defined by a JSON file in `src/locales`. Its name is the locale's [ISO 639-1 code](https://en.wikipedia.org/wiki/ISO_639-1) plus `.json`. It is simply a file that maps string IDs to translated strings. Here is an example of `en.json`:
+
+```json
+{
+ "howItWorks": "How It Works",
+ "faq": "FAQ",
+ "about": "About",
+ "startInSeconds": "Start in Seconds",
+ "signIn": "Sign In"
+}
+```
+
+## Internationalizing Strings
+
+Strings are internationalized by simply using `svelte-i18n`'s [`$format`](https://github.com/kaisermann/svelte-i18n/blob/96a5adab7560249c8881e0612454e01c09273400/docs/Formatting.md) function. A localized string looks like this:
+
+```
+{$_('stringID')}
+```
+
+(`$_` is a convenient alias for `$format`.)
+
+## The Locale Selector
+
+CoinOS also has a `LocaleSelector` component, which should be used to select locales. It is a dropdown that allows you to select & update the locale.
diff --git a/doc/i18n.md b/doc/i18n.md
new file mode 100644
index 00000000..10f2995d
--- /dev/null
+++ b/doc/i18n.md
@@ -0,0 +1,71 @@
+# CoinOS Internationalization
+
+CoinOS can be translated into other languages by editing JSON files. You do not need programming knowledge to do this, or to use these instructions. Here are instructions on how to internationalize CoinOS:
+
+_NOTE: This guide was written for people without a technical background. If you want more technical details, click [here](./i18n-technical.md)._
+
+## Required Background Knowledge
+
+### Language Codes
+
+Before you can translate CoinOS, you need to know the [ISO 639-1 code](https://en.wikipedia.org/wiki/ISO_639-1) of your language. This is a standard two-letter code that your browser uses to refer to a language (for example, English is `en`, and French is `fr`). You can also add a dash and more letters to refer to a specific region (for example, American English is `en-US`, and British English is `en-GB`). A list of all codes can be found [here](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes), and Firefox-based browsers show you the code when you are selecting your browser's language.
+
+### Locale Files
+
+Each translation of CoinOS is stored in a special _locale file_. These files can be found in the `src/locales` folder in the CoinOS directory. The name of these files is the language code plus "`.json`". A file might look something like this:
+
+```JSON
+{
+ "howItWorks": "How It Works",
+ "faq": "FAQ",
+ "about": "About",
+ "startInSeconds": "Start in Seconds",
+ "signIn": "Sign In"
+}
+```
+
+Each line (other than the opening and closing braces) contains a **text name**, then a colon (`:`), then the **translated text**. All lines other than the last end in a comma. The text name is a word that is the same in all translations, and is used by the code to access that piece of text. **Both the text name and translated text must be inside quotation marks (`"`).**
+
+### The Language List
+
+At the top of the file `src/lib/i18n.js`, there is a list of translations that looks something like this:
+
+```js
+// The list of locales to look for
+// ADD ALL NEW LANGUAGE CODES TO THIS LIST
+const localeCodes = ['en', 'fr'];
+```
+
+In order for a translation to be recognized by the CoinOS system, you need to add it to this list. At the end of the list, before the second square bracket, add a comma, then a space, then the language code in quotation marks.
+
+## Creating a New Translation
+
+Follow these steps to create a new translation. You will need a GitHub account to do this:
+
+1. Determine the language code of your language, using [this list](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). For these instructions, I will be using the fictional code `xx` - replace that code with the code of whatever language you are translating to.
+2. Go to the `src` folder, then the `locales` folder, then create a file `xx.json` inside that folder using either a text editor (e.g. Notepad), or GitHub.
+ ![Creating a new translation in GitHub](./img/create_locale_github.png)
+3. Copy the text from `en.json` into this new file.
+4. Translate all of the pieces of text on the right. **Do not translate or alter in any way the pieces of text on the left, or any line that doesn't have two pieces of text!**
+ _NOTE: "CoinOS" is a feminine noun in languages with grammatical gender._
+ ![Translating in GitHub](./img/translate_github.png)
+5. Scroll down and use the green button to commit the file.
+6. Go to the `src` folder again, then `lib`, then `i18n.js`.
+7. Use the pencil icon on the top-right to edit the file, then add your language code (**in quotation marks**) to the list.
+ ![Adding a new translation in GitHub](./img/add_locale_github.png)
+8. Commit your changes like you did in step 5.
+
+## Adding to an Existing Translation
+
+Follow these steps to add to an existing translation. You will also need a GitHub account to do this:
+
+1. Determine the language code of your language, using [this list](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). For these instructions, I will be using the fictional code `xx` - replace that code with the code of whatever language you are translating to.
+2. Go to the `src` folder, then `locales`, then `en.json`.
+3. Find the line with the piece of text that you want to translate.
+4. Find your translation's file, at `src/locales/xx.json`.
+5. Use the pencil icon on the top-right to edit the file.
+6. Add the whole line (both left and right parts) from step 3 to this file, in between the `{}` braces. Make sure the previous line has a comma at the end.
+7. Translate the piece of text on the right, keeping the quotes surrounding it. **Do not translate or alter in any way the piece of text on the left, or any line that doesn't have two pieces of text!**
+ _NOTE: "CoinOS" is a feminine noun in languages with grammatical gender._
+8. Repeat steps 3-7 for every piece of text you want to translate (you should probably open `en.json` in another tab, so that your changes from step 6 aren't erased).
+9. Scroll down and use the green button to commit the file.
diff --git a/doc/img/add_locale_github.png b/doc/img/add_locale_github.png
new file mode 100644
index 00000000..1a75b223
--- /dev/null
+++ b/doc/img/add_locale_github.png
Binary files differ
diff --git a/doc/img/create_locale_github.png b/doc/img/create_locale_github.png
new file mode 100644
index 00000000..538124da
--- /dev/null
+++ b/doc/img/create_locale_github.png
Binary files differ
diff --git a/doc/img/translate_github.png b/doc/img/translate_github.png
new file mode 100644
index 00000000..22e33174
--- /dev/null
+++ b/doc/img/translate_github.png
Binary files differ
diff --git a/package.json b/package.json
index 8a70780f..cf50f209 100644
--- a/package.json
+++ b/package.json
@@ -25,5 +25,8 @@
"tailwindcss": "^3.1.6",
"vite": "^3.0.3"
},
- "type": "module"
+ "type": "module",
+ "dependencies": {
+ "svelte-i18n": "^3.4.0"
+ }
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 67db21dd..df638c61 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,14 +11,18 @@ specifiers:
prettier-plugin-svelte: ^2.7.0
qr-code-styling: 1.6.0-rc.1
svelte: ^3.49.0
+ svelte-i18n: ^3.4.0
svelte-outclick: ^3.1.0
svelte-preprocess: ^4.10.7
tailwindcss: ^3.1.6
vite: ^3.0.3
+dependencies:
+ svelte-i18n: [email protected]
+
devDependencies:
'@sveltejs/adapter-auto': 1.0.0-next.64
'@zerodevx/svelte-toast': 0.7.2
autoprefixer: [email protected]
cookie: 0.5.0
@@ -38,6 +42,40 @@ packages:
resolution: {integrity: sha512-B1/plF62pt+H2IJHvApK8fdOJAVsvojvacuac8x8s+JIyqbropMyqNqHTKLm3YD8ZFLGwYeFTudU+PQ7vGvBdA==}
dev: true
+ /@formatjs/ecma402-abstract/1.11.4:
+ resolution: {integrity: sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==}
+ dependencies:
+ '@formatjs/intl-localematcher': 0.2.25
+ tslib: 2.4.0
+ dev: false
+
+ /@formatjs/fast-memoize/1.2.1:
+ resolution: {integrity: sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==}
+ dependencies:
+ tslib: 2.4.0
+ dev: false
+
+ /@formatjs/icu-messageformat-parser/2.1.0:
+ resolution: {integrity: sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==}
+ dependencies:
+ '@formatjs/ecma402-abstract': 1.11.4
+ '@formatjs/icu-skeleton-parser': 1.3.6
+ tslib: 2.4.0
+ dev: false
+
+ /@formatjs/icu-skeleton-parser/1.3.6:
+ resolution: {integrity: sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==}
+ dependencies:
+ '@formatjs/ecma402-abstract': 1.11.4
+ tslib: 2.4.0
+ dev: false
+
+ /@formatjs/intl-localematcher/0.2.25:
+ resolution: {integrity: sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==}
+ dependencies:
+ tslib: 2.4.0
+ dev: false
+
/@iarna/toml/2.2.5:
resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==}
dev: true
@@ -127,8 +165,8 @@ packages:
- supports-color
dev: true
- resolution: {integrity: sha512-VrpFtSSu1ADRVWWDxLl218GKf9l+TA7tNCCAmVFAuJdj5zVxPgJflu4Dh1p5C4efKP+x1v6W5nvxmNkbZLPoag==}
+ resolution: {integrity: sha512-jHSa74F7k+hC+0fof75g/xm/+1M5sM66Qt6v8eLLMSgjkp36Lb5xOioBhbl6w0NYoE5xysLsBWuu+yHytfvCBA==}
engines: {node: '>=16.9'}
hasBin: true
requiresBuild: true
@@ -140,6 +178,7 @@ packages:
chokidar: 3.5.3
sade: 1.8.1
svelte: 3.49.0
+ tiny-glob: 0.2.9
vite: 3.0.3
transitivePeerDependencies:
- diff-match-patch
@@ -410,7 +449,6 @@ packages:
/deepmerge/4.2.2:
resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==}
engines: {node: '>=0.10.0'}
- dev: true
/defined/1.0.0:
resolution: {integrity: sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==}
@@ -679,7 +717,6 @@ packages:
/estree-walker/2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
- dev: true
/fast-glob/3.2.11:
resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==}
@@ -778,11 +815,9 @@ packages:
/globalyzer/0.1.0:
resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==}
- dev: true
/globrex/0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
- dev: true
/graceful-fs/4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
@@ -820,6 +855,15 @@ packages:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: true
+ /intl-messageformat/9.13.0:
+ resolution: {integrity: sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==}
+ dependencies:
+ '@formatjs/ecma402-abstract': 1.11.4
+ '@formatjs/fast-memoize': 1.2.1
+ '@formatjs/icu-messageformat-parser': 2.1.0
+ tslib: 2.4.0
+ dev: false
+
/is-binary-path/2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
@@ -951,7 +995,6 @@ packages:
/mri/1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
- dev: true
/mrmime/1.0.1:
resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
@@ -1245,7 +1288,6 @@ packages:
engines: {node: '>=6'}
dependencies:
mri: 1.2.0
- dev: true
/safe-buffer/5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@@ -1347,6 +1389,21 @@ packages:
svelte: 3.49.0
dev: true
+ /svelte-i18n/[email protected]:
+ resolution: {integrity: sha512-590N+YIRlebDT3fXmuAxd4guQZLR3vm4kCs5UhWYmw3SxOlJNZ7HwYYiw6d4jDr7P+Cx7DSopk1Z1K9wn8B6EA==}
+ engines: {node: '>= 11.15.0'}
+ hasBin: true
+ peerDependencies:
+ svelte: ^3.25.1
+ dependencies:
+ deepmerge: 4.2.2
+ estree-walker: 2.0.2
+ intl-messageformat: 9.13.0
+ sade: 1.8.1
+ svelte: 3.49.0
+ tiny-glob: 0.2.9
+ dev: false
+
/svelte-outclick/3.1.0:
resolution: {integrity: sha512-1p/4UAuBfgfltMByTXjE3wbNAYaPihsB9HkhYrKgkiUgZSUtQ+Tpr/gxLu0LIsgft1EKt1izWg/VfzO+f5lQUA==}
dev: true
@@ -1455,7 +1512,6 @@ packages:
dependencies:
globalyzer: 0.1.0
globrex: 0.1.2
- dev: true
/to-regex-range/5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
@@ -1468,6 +1524,10 @@ packages:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: true
+ /tslib/2.4.0:
+ resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
+ dev: false
+
/update-browserslist-db/[email protected]:
resolution: {integrity: sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==}
hasBin: true
diff --git a/src/components/LandingHeader.svelte b/src/components/LandingHeader.svelte
index a8abfaba..cc743b79 100644
--- a/src/components/LandingHeader.svelte
+++ b/src/components/LandingHeader.svelte
@@ -1,8 +1,9 @@
<script>
import { goto } from '$app/navigation';
import { scroll } from '$lib/utils';
- import { Icon } from '$comp';
+ import { Icon, LocaleSelector } from '$comp';
import { user } from '$lib/store';
+ import { _ } from 'svelte-i18n';
export let howItWorks;
export let faq;
@@ -17,25 +18,28 @@
<header class="w-full lg:w-5/6 py-5 mx-auto fixed md:sticky z-10 top-0 bg-white/90">
<nav class="block xl:flex flex-wrap justify-between items-center">
- <a href="/" class="hidden md:block">
- <Icon icon="logo" style="mx-auto mb-5 xl:mb-0" />
- </a>
- <Icon icon="logo" style="block md:hidden ml-5 w-48" />
+ <div class="flex justify-start md:justify-center items-center md:space-x-10">
+ <a href="/" class="hidden md:block">
+ <Icon icon="logo" style="mb-5 xl:mb-0" />
+ </a>
+ <Icon icon="logo" style="block md:hidden ml-5 w-48" />
+ <LocaleSelector style="hidden md:block py-1 mb-5 xl:mb-0" />
+ </div>
<!-- desktop nav -->
<div class="hidden space-x-10 md:flex flex-wrap justify-center items-center font-bold">
- <button on:click={() => scroll(howItWorks)}>How It Works</button>
- <button on:click={() => scroll(faq)}>FAQ</button>
- <button on:click={() => scroll(about)}>About</button>
+ <button on:click={() => scroll(howItWorks)}>{$_('nav.howItWorks')}</button>
+ <button on:click={() => scroll(faq)}>{$_('nav.faq')}</button>
+ <button on:click={() => scroll(about)}>{$_('nav.about')}</button>
{#if !$user}
<button class="border rounded-full px-6 py-2 font-bold" on:click={() => goto('/register')}
- >Start in Seconds
+ >{$_('nav.startInSeconds')}
</button>
<button
class="bg-black text-white border rounded-full px-6 py-2 font-bold"
on:click={() => goto('/login')}
>
- Sign in
+ {$_('nav.signIn')}
</button>
{:else}
<button
@@ -66,21 +70,23 @@
>
<div class="space-y-8 mt-24 ml-10 font-bold text-xl">
<button on:click={() => mobileMenuButtonClick(howItWorks)} class="block"
- >How It Works</button
+ >{$_('nav.howItWorks')}</button
+ >
+ <button on:click={() => mobileMenuButtonClick(faq)} class="block">{$_('nav.faq')}</button>
+ <button on:click={() => mobileMenuButtonClick(about)} class="block"
+ >{$_('nav.about')}</button
>
- <button on:click={() => mobileMenuButtonClick(faq)} class="block">FAQ</button>
- <button on:click={() => mobileMenuButtonClick(about)} class="block">About</button>
{#if !$user}
<button
class="border rounded-full px-6 py-2 font-bold block"
on:click={() => goto('/register')}
- >Start in Seconds
+ >{$_('nav.startInSeconds')}
</button>
<button
class="bg-black text-white border rounded-full px-6 py-2 font-bold block"
on:click={() => goto('/login')}
>
- Sign in
+ {$_('nav.signIn')}
</button>
{:else}
<button
@@ -95,6 +101,7 @@
Sign out
</button>
{/if}
+ <LocaleSelector style="py-1" />
</div>
</div>
</nav>
diff --git a/src/components/LocaleSelector.svelte b/src/components/LocaleSelector.svelte
new file mode 100644
index 00000000..2ae65986
--- /dev/null
+++ b/src/components/LocaleSelector.svelte
@@ -0,0 +1,10 @@
+<script>
+ import { _, locale, locales } from 'svelte-i18n';
+ export let style;
+</script>
+
+<select name="language" bind:value={$locale} class={style}>
+ {#each $locales as locale}
+ <option value={locale}>{locale}</option>
+ {/each}
+</select>
diff --git a/src/components/index.js b/src/components/index.js
index 1003a1b0..39a4e5e7 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -7,6 +7,7 @@ export { default as Icon } from './Icon.svelte';
export { default as Image } from './Image.svelte';
export { default as RegisterOrLogin } from './RegisterOrLogin.svelte';
export { default as AppHeader } from './AppHeader.svelte';
+export { default as LocaleSelector } from './LocaleSelector.svelte';
export { default as LoadingSplash } from './LoadingSplash.svelte';
export { default as PageNotFound } from './404.svelte';
export { default as About } from './About.svelte';
diff --git a/src/lib/i18n.js b/src/lib/i18n.js
new file mode 100644
index 00000000..3105c709
--- /dev/null
+++ b/src/lib/i18n.js
@@ -0,0 +1,48 @@
+import { register, init, getLocaleFromNavigator } from 'svelte-i18n';
+
+// The list of locales to look for
+// ADD ALL NEW LANGUAGE CODES TO THIS LIST
+const localeCodes = ['en', 'fr'];
+
+// the default locale to use if none was found
+const defaultLocale = 'en';
+
+// Adds a new locale based on its code
+function addLocale(localeCode) {
+ register(localeCode, () => import(`../locales/${localeCode}.json`));
+}
+
+// Picks the default locale based on navigator locale and the list of available locales
+function pickLocale(navigatorLocale, availableLocales) {
+ // locale can be null the first time you load this
+ // return the default locale in that case - reloading should fix this
+ if (!navigatorLocale) return defaultLocale;
+
+ // determine langugage part of locale
+ // (e.g. if navigatorLocale is 'en-US', languageCode is 'en')
+ let dashIndex = navigatorLocale ? navigatorLocale.indexOf('-') : -1;
+ let languageCode; // navigator locale without the regional part
+ if (dashIndex === -1) {
+ languageCode = navigatorLocale;
+ } else {
+ languageCode = navigatorLocale.slice(0, dashIndex);
+ }
+
+ // get locale
+ if (availableLocales.includes(navigatorLocale)) {
+ return navigatorLocale;
+ } else if (availableLocales.includes(languageCode)) {
+ return languageCode;
+ } else {
+ return navigatorLocale;
+ }
+}
+
+// add locales
+localeCodes.forEach(addLocale);
+
+let initialLocale = pickLocale(getLocaleFromNavigator(), localeCodes);
+init({
+ fallbackLocale: defaultLocale,
+ initialLocale: initialLocale
+});
diff --git a/src/locales/en.json b/src/locales/en.json
new file mode 100644
index 00000000..a44c202f
--- /dev/null
+++ b/src/locales/en.json
@@ -0,0 +1,10 @@
+{
+ "langname": "English",
+ "nav": {
+ "howItWorks": "How It Works",
+ "faq": "FAQ",
+ "about": "About",
+ "startInSeconds": "Start in Seconds",
+ "signIn": "Sign In"
+ }
+}
diff --git a/src/locales/fr.json b/src/locales/fr.json
new file mode 100644
index 00000000..9adc9488
--- /dev/null
+++ b/src/locales/fr.json
@@ -0,0 +1,10 @@
+{
+ "langname": "Français",
+ "nav": {
+ "howItWorks": "Comment Ça Fonctionne",
+ "faq": "FAQ",
+ "about": "À Propos",
+ "startInSeconds": "Commence en Secondes",
+ "signIn": "Connexion"
+ }
+}
diff --git a/src/routes/[username]/settings.svelte b/src/routes/[username]/settings.svelte
index 5abe58e0..e4d7b0bf 100644
--- a/src/routes/[username]/settings.svelte
+++ b/src/routes/[username]/settings.svelte
@@ -1,5 +1,5 @@
<script>
- import { AppHeader, Icon, Toggle } from '$comp';
+ import { AppHeader, Icon, Toggle, LocaleSelector } from '$comp';
import { user, colorTheme } from '$lib/store';
let setting = 'account';
@@ -45,6 +45,11 @@
{#if setting === 'account'}
<div>
+ <label for="language" class="font-bold block mb-1">Language</label>
+ <LocaleSelector style="block py-3 w-full" />
+ </div>
+
+ <div>
<label for="username" class="font-bold mb-1 block">Username</label>
<input
type="text"
diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte
index 5ffdc9a0..8ac6f20f 100644
--- a/src/routes/__layout.svelte
+++ b/src/routes/__layout.svelte
@@ -1,6 +1,8 @@
<script>
import { SvelteToast } from '@zerodevx/svelte-toast';
- import '../app.css';
+ import '$lib/i18n';
+ import { isLoading, locale } from 'svelte-i18n';
+ import '../app.css';
import { onMount } from 'svelte';
import { connect } from '$lib/socket';
import { user, token } from '$lib/store';
@@ -15,6 +17,14 @@
let ready = false;
onMount(() => {
+ // get locale from local storage
+ let localStorageLocale = localStorage.getItem('svelte-i18n-locale');
+ if (localStorageLocale) locale.set(localStorageLocale);
+
+ locale.subscribe((lng) => {
+ if (lng) localStorage.setItem('svelte-i18n-locale', lng);
+ });
+
if (protectedRoutes.find((p) => $page.url.pathname.match(p))) {
if (!$token) {
goto('/login');
@@ -29,7 +39,7 @@
});
</script>
-{#if ready}
+{#if ready && !$isLoading}
<SvelteToast options={{ reversed: true, intro: { y: 192 } }} />
<slot />
{:else}