mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-23 22:22:07 -05:00
feat(mobx): implement locale options
This commit is contained in:
parent
89748d7044
commit
87a9841885
5 changed files with 155 additions and 144 deletions
|
@ -1,23 +1,21 @@
|
|||
import { dispatch } from "../../redux";
|
||||
import { connectState } from "../../redux/connector";
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import { Language, Languages } from "../../context/Locale";
|
||||
|
||||
import ComboBox from "../ui/ComboBox";
|
||||
|
||||
type Props = {
|
||||
locale: string;
|
||||
};
|
||||
/**
|
||||
* Component providing a language selector combobox.
|
||||
* Note: this is not an observer but this is fine as we are just using a combobox.
|
||||
*/
|
||||
export default function LocaleSelector() {
|
||||
const locale = useApplicationState().locale;
|
||||
|
||||
export function LocaleSelector(props: Props) {
|
||||
return (
|
||||
<ComboBox
|
||||
value={props.locale}
|
||||
value={locale.getLanguage()}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "SET_LOCALE",
|
||||
locale: e.currentTarget.value as Language,
|
||||
})
|
||||
locale.setLanguage(e.currentTarget.value as Language)
|
||||
}>
|
||||
{Object.keys(Languages).map((x) => {
|
||||
const l = Languages[x as keyof typeof Languages];
|
||||
|
@ -30,9 +28,3 @@ export function LocaleSelector(props: Props) {
|
|||
</ComboBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectState(LocaleSelector, (state) => {
|
||||
return {
|
||||
locale: state.locale,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -3,11 +3,12 @@ import calendar from "dayjs/plugin/calendar";
|
|||
import format from "dayjs/plugin/localizedFormat";
|
||||
import update from "dayjs/plugin/updateLocale";
|
||||
import defaultsDeep from "lodash.defaultsdeep";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { IntlProvider } from "preact-i18n";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { connectState } from "../redux/connector";
|
||||
import { useApplicationState } from "../mobx/State";
|
||||
|
||||
import definition from "../../external/lang/en.json";
|
||||
|
||||
|
@ -222,59 +223,14 @@ export interface Dictionary {
|
|||
| undefined;
|
||||
}
|
||||
|
||||
function Locale({ children, locale }: Props) {
|
||||
const [defns, setDefinition] = useState<Dictionary>(
|
||||
export default observer(({ children }: Props) => {
|
||||
const locale = useApplicationState().locale;
|
||||
const [definitions, setDefinition] = useState<Dictionary>(
|
||||
definition as Dictionary,
|
||||
);
|
||||
|
||||
// Load relevant language information, fallback to English if invalid.
|
||||
const lang = Languages[locale] ?? Languages.en;
|
||||
|
||||
function transformLanguage(source: Dictionary) {
|
||||
// Fallback untranslated strings to English (UK)
|
||||
const obj = defaultsDeep(source, definition);
|
||||
|
||||
// Take relevant objects out, dayjs and defaults
|
||||
// should exist given we just took defaults above.
|
||||
const { dayjs } = obj;
|
||||
const { defaults } = dayjs;
|
||||
|
||||
// Determine whether we are using 12-hour clock.
|
||||
const twelvehour = defaults?.twelvehour
|
||||
? defaults.twelvehour === "yes"
|
||||
: false;
|
||||
|
||||
// Determine what date separator we are using.
|
||||
const separator: string = defaults?.date_separator ?? "/";
|
||||
|
||||
// Determine what date format we are using.
|
||||
const date: "traditional" | "simplified" | "ISO8601" =
|
||||
defaults?.date_format ?? "traditional";
|
||||
|
||||
// Available date formats.
|
||||
const DATE_FORMATS = {
|
||||
traditional: `DD${separator}MM${separator}YYYY`,
|
||||
simplified: `MM${separator}DD${separator}YYYY`,
|
||||
ISO8601: "YYYY-MM-DD",
|
||||
};
|
||||
|
||||
// Replace data in dayjs object, make sure to provide fallbacks.
|
||||
dayjs["sameElse"] = DATE_FORMATS[date] ?? DATE_FORMATS.traditional;
|
||||
dayjs["timeFormat"] = twelvehour ? "hh:mm A" : "HH:mm";
|
||||
|
||||
// Replace {{time}} format string in dayjs strings with the time format.
|
||||
Object.keys(dayjs)
|
||||
.filter((k) => typeof dayjs[k] === "string")
|
||||
.forEach(
|
||||
(k) =>
|
||||
(dayjs[k] = dayjs[k].replace(
|
||||
/{{time}}/g,
|
||||
dayjs["timeFormat"],
|
||||
)),
|
||||
);
|
||||
|
||||
return obj;
|
||||
}
|
||||
const lang = locale.getLanguage();
|
||||
const source = Languages[lang];
|
||||
|
||||
const loadLanguage = useCallback(
|
||||
(locale: string) => {
|
||||
|
@ -288,13 +244,13 @@ function Locale({ children, locale }: Props) {
|
|||
return;
|
||||
}
|
||||
|
||||
import(`../../external/lang/${lang.i18n}.json`).then(
|
||||
import(`../../external/lang/${source.i18n}.json`).then(
|
||||
async (lang_file) => {
|
||||
// Transform the definitions data.
|
||||
const defn = transformLanguage(lang_file.default);
|
||||
|
||||
// Determine and load dayjs locales.
|
||||
const target = lang.dayjs ?? lang.i18n;
|
||||
const target = source.dayjs ?? source.i18n;
|
||||
const dayjs_locale = await import(
|
||||
`../../node_modules/dayjs/esm/locale/${target}.js`
|
||||
);
|
||||
|
@ -312,25 +268,63 @@ function Locale({ children, locale }: Props) {
|
|||
},
|
||||
);
|
||||
},
|
||||
[lang.dayjs, lang.i18n],
|
||||
[source.dayjs, source.i18n],
|
||||
);
|
||||
|
||||
useEffect(() => loadLanguage(locale), [locale, lang, loadLanguage]);
|
||||
useEffect(() => loadLanguage(lang), [lang, source, loadLanguage]);
|
||||
|
||||
useEffect(() => {
|
||||
// Apply RTL language format.
|
||||
document.body.style.direction = lang.rtl ? "rtl" : "";
|
||||
}, [lang.rtl]);
|
||||
document.body.style.direction = source.rtl ? "rtl" : "";
|
||||
}, [source.rtl]);
|
||||
|
||||
return <IntlProvider definition={defns}>{children}</IntlProvider>;
|
||||
return <IntlProvider definition={definitions}>{children}</IntlProvider>;
|
||||
});
|
||||
|
||||
/**
|
||||
* Apply defaults and process dayjs entries for a langauge.
|
||||
* @param source Dictionary definition to transform
|
||||
* @returns Transformed dictionary definition
|
||||
*/
|
||||
function transformLanguage(source: Dictionary) {
|
||||
// Fallback untranslated strings to English (UK)
|
||||
const obj = defaultsDeep(source, definition);
|
||||
|
||||
// Take relevant objects out, dayjs and defaults
|
||||
// should exist given we just took defaults above.
|
||||
const { dayjs } = obj;
|
||||
const { defaults } = dayjs;
|
||||
|
||||
// Determine whether we are using 12-hour clock.
|
||||
const twelvehour = defaults?.twelvehour
|
||||
? defaults.twelvehour === "yes"
|
||||
: false;
|
||||
|
||||
// Determine what date separator we are using.
|
||||
const separator: string = defaults?.date_separator ?? "/";
|
||||
|
||||
// Determine what date format we are using.
|
||||
const date: "traditional" | "simplified" | "ISO8601" =
|
||||
defaults?.date_format ?? "traditional";
|
||||
|
||||
// Available date formats.
|
||||
const DATE_FORMATS = {
|
||||
traditional: `DD${separator}MM${separator}YYYY`,
|
||||
simplified: `MM${separator}DD${separator}YYYY`,
|
||||
ISO8601: "YYYY-MM-DD",
|
||||
};
|
||||
|
||||
// Replace data in dayjs object, make sure to provide fallbacks.
|
||||
dayjs["sameElse"] = DATE_FORMATS[date] ?? DATE_FORMATS.traditional;
|
||||
dayjs["timeFormat"] = twelvehour ? "hh:mm A" : "HH:mm";
|
||||
|
||||
// Replace {{time}} format string in dayjs strings with the time format.
|
||||
Object.keys(dayjs)
|
||||
.filter((k) => typeof dayjs[k] === "string")
|
||||
.forEach(
|
||||
(k) =>
|
||||
(dayjs[k] = dayjs[k].replace(/{{time}}/g, dayjs["timeFormat"])),
|
||||
);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
export default connectState<Omit<Props, "locale">>(
|
||||
Locale,
|
||||
(state) => {
|
||||
return {
|
||||
locale: state.locale,
|
||||
};
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useContext } from "preact/hooks";
|
|||
|
||||
import Auth from "./stores/Auth";
|
||||
import Draft from "./stores/Draft";
|
||||
import LocaleOptions from "./stores/LocaleOptions";
|
||||
|
||||
interface StoreDefinition {
|
||||
id: string;
|
||||
|
@ -20,6 +21,7 @@ interface StoreDefinition {
|
|||
export default class State {
|
||||
auth: Auth;
|
||||
draft: Draft;
|
||||
locale: LocaleOptions;
|
||||
|
||||
/**
|
||||
* Construct new State.
|
||||
|
@ -27,6 +29,7 @@ export default class State {
|
|||
constructor() {
|
||||
this.auth = new Auth();
|
||||
this.draft = new Draft();
|
||||
this.locale = new LocaleOptions();
|
||||
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
|
|
@ -72,13 +72,21 @@ export default class LocaleOptions implements Persistent<Data> {
|
|||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
@action hydrate(data: Data) {
|
||||
this.lang = data.lang;
|
||||
this.setLanguage(data.lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current language.
|
||||
*/
|
||||
@computed getLang() {
|
||||
@computed getLanguage() {
|
||||
return this.lang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current language.
|
||||
*/
|
||||
@action setLanguage(language: Language) {
|
||||
if (typeof Languages[language] === "undefined") return;
|
||||
this.lang = language;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import styles from "./Panes.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useMemo } from "preact/hooks";
|
||||
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import LocaleOptions from "../../../mobx/stores/LocaleOptions";
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
|
||||
|
@ -17,26 +24,25 @@ import enchantingTableWEBP from "../assets/enchanting_table.webp";
|
|||
import tamilFlagPNG from "../assets/tamil_nadu_flag.png";
|
||||
import tokiponaSVG from "../assets/toki_pona.svg";
|
||||
|
||||
type Props = {
|
||||
locale: Language;
|
||||
};
|
||||
type Key = [Language, LanguageEntry];
|
||||
|
||||
type Key = [string, LanguageEntry];
|
||||
interface Props {
|
||||
entry: Key;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
function Entry({ entry: [x, lang], locale }: { entry: Key } & Props) {
|
||||
/**
|
||||
* Component providing individual language entries.
|
||||
* @param param0 Entry data
|
||||
*/
|
||||
function Entry({ entry: [x, lang], selected, onSelect }: Props) {
|
||||
return (
|
||||
<Checkbox
|
||||
key={x}
|
||||
className={styles.entry}
|
||||
checked={locale === x}
|
||||
onChange={(v) => {
|
||||
if (v) {
|
||||
dispatch({
|
||||
type: "SET_LOCALE",
|
||||
locale: x as Language,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
checked={selected}
|
||||
onChange={onSelect}>
|
||||
<div className={styles.flag}>
|
||||
{lang.i18n === "ta" ? (
|
||||
<img
|
||||
|
@ -61,36 +67,58 @@ function Entry({ entry: [x, lang], locale }: { entry: Key } & Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export function Component(props: Props) {
|
||||
const languages = Object.keys(Langs).map((x) => [
|
||||
x,
|
||||
Langs[x as keyof typeof Langs],
|
||||
]) as Key[];
|
||||
/**
|
||||
* Component providing the language selection menu.
|
||||
*/
|
||||
export const Languages = observer(() => {
|
||||
const locale = useApplicationState().locale;
|
||||
const language = locale.getLanguage();
|
||||
|
||||
// Get the user's system language. Check for exact
|
||||
// matches first, otherwise check for partial matches
|
||||
const preferredLanguage =
|
||||
navigator.languages.filter((lang) =>
|
||||
languages.find((l) => l[0].replace(/_/g, "-") == lang),
|
||||
)?.[0] ||
|
||||
navigator.languages
|
||||
?.map((x) => x.split("-")[0])
|
||||
?.filter((lang) => languages.find((l) => l[0] == lang))?.[0]
|
||||
?.split("-")[0];
|
||||
// Generate languages array.
|
||||
const languages = useMemo(() => {
|
||||
const languages = Object.keys(Langs).map((x) => [
|
||||
x,
|
||||
Langs[x as keyof typeof Langs],
|
||||
]) as Key[];
|
||||
|
||||
if (preferredLanguage) {
|
||||
// This moves the user's system language to the top of the language list
|
||||
const prefLangKey = languages.find(
|
||||
(lang) => lang[0].replace(/_/g, "-") == preferredLanguage,
|
||||
);
|
||||
if (prefLangKey) {
|
||||
languages.splice(
|
||||
0,
|
||||
0,
|
||||
languages.splice(languages.indexOf(prefLangKey), 1)[0],
|
||||
// Get the user's system language. Check for exact
|
||||
// matches first, otherwise check for partial matches
|
||||
const preferredLanguage =
|
||||
navigator.languages.filter((lang) =>
|
||||
languages.find((l) => l[0].replace(/_/g, "-") == lang),
|
||||
)?.[0] ||
|
||||
navigator.languages
|
||||
?.map((x) => x.split("-")[0])
|
||||
?.filter((lang) => languages.find((l) => l[0] == lang))?.[0]
|
||||
?.split("-")[0];
|
||||
|
||||
if (preferredLanguage) {
|
||||
// This moves the user's system language to the top of the language list
|
||||
const prefLangKey = languages.find(
|
||||
(lang) => lang[0].replace(/_/g, "-") == preferredLanguage,
|
||||
);
|
||||
|
||||
if (prefLangKey) {
|
||||
languages.splice(
|
||||
0,
|
||||
0,
|
||||
languages.splice(languages.indexOf(prefLangKey), 1)[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return languages;
|
||||
}, []);
|
||||
|
||||
// Creates entries with given key.
|
||||
const EntryFactory = ([x, lang]: Key) => (
|
||||
<Entry
|
||||
key={x}
|
||||
entry={[x, lang]}
|
||||
selected={language === x}
|
||||
onSelect={() => locale.setLanguage(x)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.languages}>
|
||||
|
@ -98,11 +126,7 @@ export function Component(props: Props) {
|
|||
<Text id="app.settings.pages.language.select" />
|
||||
</h3>
|
||||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => !lang.cat)
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
{languages.filter(([, lang]) => !lang.cat).map(EntryFactory)}
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.language.const" />
|
||||
|
@ -110,9 +134,7 @@ export function Component(props: Props) {
|
|||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => lang.cat === "const")
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
.map(EntryFactory)}
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.language.other" />
|
||||
|
@ -120,9 +142,7 @@ export function Component(props: Props) {
|
|||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => lang.cat === "alt")
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
.map(EntryFactory)}
|
||||
</div>
|
||||
<Tip>
|
||||
<span>
|
||||
|
@ -137,10 +157,4 @@ export function Component(props: Props) {
|
|||
</Tip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Languages = connectState(Component, (state) => {
|
||||
return {
|
||||
locale: state.locale,
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue