Feature/add localization #21
10 changed files with 183 additions and 3 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -1,10 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "calque",
|
"name": "calque",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@solid-primitives/i18n": "^2.1.1",
|
||||||
|
"@solid-primitives/storage": "^4.2.1",
|
||||||
"@solidjs/meta": "^0.29.4",
|
"@solidjs/meta": "^0.29.4",
|
||||||
"@solidjs/router": "^0.15.2",
|
"@solidjs/router": "^0.15.2",
|
||||||
"@solidjs/start": "^1.0.10",
|
"@solidjs/start": "^1.0.10",
|
||||||
"dexie": "^4.0.10",
|
"dexie": "^4.0.10",
|
||||||
|
"flag-icons": "^7.2.3",
|
||||||
"iterator-helpers-polyfill": "^3.0.1",
|
"iterator-helpers-polyfill": "^3.0.1",
|
||||||
"sitemap": "^8.0.0",
|
"sitemap": "^8.0.0",
|
||||||
"solid-icons": "^1.1.0",
|
"solid-icons": "^1.1.0",
|
||||||
|
|
|
@ -2,8 +2,9 @@ import { MetaProvider } from "@solidjs/meta";
|
||||||
import { Router } from "@solidjs/router";
|
import { Router } from "@solidjs/router";
|
||||||
import { FileRoutes } from "@solidjs/start/router";
|
import { FileRoutes } from "@solidjs/start/router";
|
||||||
import { Suspense } from "solid-js";
|
import { Suspense } from "solid-js";
|
||||||
import "./app.css";
|
|
||||||
import { ThemeProvider } from "./components/colorschemepicker";
|
import { ThemeProvider } from "./components/colorschemepicker";
|
||||||
|
import { I18nProvider } from "./features/i18n";
|
||||||
|
import "./app.css";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
@ -11,9 +12,11 @@ export default function App() {
|
||||||
root={props => (
|
root={props => (
|
||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Suspense>{props.children}</Suspense>
|
<I18nProvider>
|
||||||
|
<Suspense>{props.children}</Suspense>
|
||||||
|
</I18nProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MetaProvider>
|
</ MetaProvider>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FileRoutes />
|
<FileRoutes />
|
||||||
|
|
9
src/features/i18n/constants.ts
Normal file
9
src/features/i18n/constants.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Locale } from "./context";
|
||||||
|
|
||||||
|
import Flag_en_GB from 'flag-icons/flags/4x3/gb.svg';
|
||||||
|
import Flag_nl_NL from 'flag-icons/flags/4x3/nl.svg';
|
||||||
|
|
||||||
|
export const locales: Record<Locale, { label: string, flag: any }> = {
|
||||||
|
'en-GB': { label: 'English', flag: Flag_en_GB },
|
||||||
|
'nl-NL': { label: 'Nederlands', flag: Flag_nl_NL },
|
||||||
|
} as const;
|
60
src/features/i18n/context.tsx
Normal file
60
src/features/i18n/context.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import { Accessor, createContext, createMemo, createSignal, ParentComponent, Setter, useContext } from 'solid-js';
|
||||||
|
import { translator, flatten, Translator, Flatten } from "@solid-primitives/i18n";
|
||||||
|
import en from '~/i18n/en-GB.json';
|
||||||
|
import nl from '~/i18n/nl-NL.json';
|
||||||
|
import { makePersisted } from '@solid-primitives/storage';
|
||||||
|
|
||||||
|
type RawDictionary = typeof en;
|
||||||
|
type Dictionary = Flatten<RawDictionary>;
|
||||||
|
export type Locale = 'en-GB' | 'nl-NL';
|
||||||
|
|
||||||
|
const dictionaries = {
|
||||||
|
'en-GB': en,
|
||||||
|
'nl-NL': nl,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
interface I18nContextType {
|
||||||
|
readonly t: Translator<Dictionary>;
|
||||||
|
readonly locale: Accessor<Locale>;
|
||||||
|
readonly setLocale: Setter<Locale>;
|
||||||
|
readonly dictionaries: Accessor<Record<Locale, RawDictionary>>;
|
||||||
|
readonly availableLocales: Accessor<Locale[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const I18nContext = createContext<I18nContextType>();
|
||||||
|
|
||||||
|
export const I18nProvider: ParentComponent = (props) => {
|
||||||
|
const [locale, setLocale, initLocale] = makePersisted(createSignal<Locale>('en-GB'), { name: 'locale' });
|
||||||
|
const dictionary = createMemo(() => flatten(dictionaries[locale()]));
|
||||||
|
const t = translator(dictionary);
|
||||||
|
|
||||||
|
const ctx: I18nContextType = {
|
||||||
|
t,
|
||||||
|
locale,
|
||||||
|
setLocale,
|
||||||
|
dictionaries: createMemo(() => dictionaries),
|
||||||
|
availableLocales: createMemo(() => Object.keys(dictionaries) as Locale[]),
|
||||||
|
};
|
||||||
|
|
||||||
|
return <I18nContext.Provider value={ctx}>{props.children}</I18nContext.Provider>
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useI18n = () => {
|
||||||
|
const context = useContext(I18nContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(`'useI18n' is called outside the scope of an <I18nProvider />`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { t: context.t, locale: context.locale };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const internal_useI18n = () => {
|
||||||
|
const context = useContext(I18nContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(`'useI18n' is called outside the scope of an <I18nProvider />`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
2
src/features/i18n/index.tsx
Normal file
2
src/features/i18n/index.tsx
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { I18nProvider, useI18n } from './context';
|
||||||
|
export { LocalePicker } from './picker';
|
7
src/features/i18n/picker.module.css
Normal file
7
src/features/i18n/picker.module.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.box {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag {
|
||||||
|
inline-size: 1em;
|
||||||
|
}
|
22
src/features/i18n/picker.tsx
Normal file
22
src/features/i18n/picker.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { Component } from "solid-js";
|
||||||
|
import { internal_useI18n } from "./context";
|
||||||
|
import { locales } from "./constants";
|
||||||
|
import { ComboBox } from "~/components/combobox";
|
||||||
|
import { Dynamic } from "solid-js/web";
|
||||||
|
import css from './picker.module.css';
|
||||||
|
|
||||||
|
interface LocalePickerProps { }
|
||||||
|
|
||||||
|
export const LocalePicker: Component<LocalePickerProps> = (props) => {
|
||||||
|
const { locale, setLocale } = internal_useI18n();
|
||||||
|
|
||||||
|
return <ComboBox
|
||||||
|
id="locale-picker"
|
||||||
|
class={css.box}
|
||||||
|
value={locale()}
|
||||||
|
setValue={setLocale}
|
||||||
|
values={locales}
|
||||||
|
>
|
||||||
|
{(locale, { flag, label }) => <Dynamic component={flag} lang={locale} aria-label={label} class={css.flag} />}
|
||||||
|
</ComboBox>
|
||||||
|
};
|
37
src/i18n/en-GB.json
Normal file
37
src/i18n/en-GB.json
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"welcome": {
|
||||||
|
"title": "Hi, welcome!",
|
||||||
|
"subtitle": "Lets get started",
|
||||||
|
"edit": "Start editing",
|
||||||
|
"instructions": "Read the instructions",
|
||||||
|
"about": "Abut this app"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"menu": {
|
||||||
|
"file": "File",
|
||||||
|
"edit": "Edit",
|
||||||
|
"selection": "Selection"
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"open": "Open folder",
|
||||||
|
"close": "Close folder",
|
||||||
|
"closeTab": "Close tab",
|
||||||
|
"save": "Save",
|
||||||
|
"saveAs": "Save as ...",
|
||||||
|
"selectAll": "Select all",
|
||||||
|
"clearSelection": "Clear selection",
|
||||||
|
"insertKey": "Insert new key",
|
||||||
|
"insertLanguage": "Insert new language",
|
||||||
|
"delete": "Delete selected items"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feature": {
|
||||||
|
"file": {
|
||||||
|
"grid": {
|
||||||
|
"key": "Key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
src/i18n/nl-NL.json
Normal file
37
src/i18n/nl-NL.json
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"welcome": {
|
||||||
|
"title": "Hoi, welkom!",
|
||||||
|
"subtitle": "Laten we beginnen",
|
||||||
|
"edit": "Begin met bewerken",
|
||||||
|
"instructions": "Lees de instructies",
|
||||||
|
"about": "Over deze app"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"menu": {
|
||||||
|
"file": "Bestand",
|
||||||
|
"edit": "Bewerken",
|
||||||
|
"selection": "Selectie"
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"open": "Map openen",
|
||||||
|
"close": "Map sluiten",
|
||||||
|
"closeTab": "Tabblad sluiten",
|
||||||
|
"save": "Opslaan",
|
||||||
|
"saveAs": "Opslaan als ...",
|
||||||
|
"selectAll": "Selecteer alles",
|
||||||
|
"clearSelection": "Selectie leeg maken",
|
||||||
|
"insertKey": "Voeg nieuwe sleutel toe",
|
||||||
|
"insertLanguage": "Voeg nieuwe taal toe",
|
||||||
|
"delete": "Verwijder geselecteerde items"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feature": {
|
||||||
|
"file": {
|
||||||
|
"grid": {
|
||||||
|
"key": "Sleutel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue