Skip to content

Commit

Permalink
User interface translations
Browse files Browse the repository at this point in the history
  • Loading branch information
kubk committed Dec 23, 2023
1 parent d99861c commit 4dfd13f
Show file tree
Hide file tree
Showing 32 changed files with 513 additions and 159 deletions.
3 changes: 2 additions & 1 deletion src/lib/mobx-form/persistable-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { makePersistable } from "mobx-persist-store";
export const persistableField = <T>(
field: TextField<T>,
storageKey: string,
expireIn?: number,
): TextField<T> => {
makePersistable(field, {
name: storageKey,
properties: ["value"],
storage: window.localStorage,
expireIn: 86400000, // One day in milliseconds
expireIn: expireIn,
});

return field;
Expand Down
7 changes: 4 additions & 3 deletions src/lib/mobx-form/validator.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// @ts-nocheck
// https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/field-level-validation?file=/index.js
import { t } from "../../translations/t.ts";

// https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/field-level-validation?file=/index.js
export const validators = {
required:
(errorMessage = "Required") =>
(errorMessage = t("validation_required")) =>
(value) =>
value ? undefined : errorMessage,
number: (value) => (isNaN(value) ? "Must be a number" : undefined),
number: (value) => (isNaN(value) ? t("validation_number") : undefined),
all:
(...validators) =>
(value) =>
Expand Down
27 changes: 27 additions & 0 deletions src/lib/translator/translator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect, test } from "vitest";
import { Translator } from "./translator";

test("Translator - allows to set and retrieve current lang", () => {
type Translation = {
hello: string;
};
const en: Translation = {
hello: "Hello!",
};
const ru: Translation = {
hello: "Привет",
};

const translations = { en, ru };
type Language = keyof typeof translations;

const ts = new Translator<Language, Translation>(translations, "en");

expect(ts.getLang()).toEqual("en");
expect(ts.translate("hello")).toBe("Hello!");

ts.setLang("ru");

expect(ts.getLang()).toEqual("ru");
expect(ts.translate("hello")).toBe("Привет");
});
27 changes: 27 additions & 0 deletions src/lib/translator/translator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
type Storage<Language extends string, Resource> = {
[key in Language]: Resource;
};

type DefaultResource = { [key in string]: string };

export class Translator<
Language extends string,
Translation extends DefaultResource,
> {
constructor(
private storage: Storage<Language, Translation>,
private lang: Language,
) {}

setLang(lang: Language) {
this.lang = lang;
}

getLang() {
return this.lang;
}

translate(key: keyof Translation, defaultValue?: string): string {
return this.storage[this.lang][key] ?? defaultValue;
}
}
3 changes: 2 additions & 1 deletion src/screens/deck-catalog/deck-added-label.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { css, cx } from "@emotion/css";
import { theme } from "../../ui/theme.tsx";
import React from "react";
import { t } from "../../translations/t.ts";

export const DeckAddedLabel = () => {
return (
<div
title={"This deck is on your list"}
title={t("deck_has_been_added")}
className={css({
position: "absolute",
right: 0,
Expand Down
43 changes: 25 additions & 18 deletions src/screens/deck-catalog/deck-catalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ import { useDeckCatalogStore } from "../../store/deck-catalog-store-context.tsx"
import { useMount } from "../../lib/react/use-mount.ts";
import { theme } from "../../ui/theme.tsx";
import { Select } from "../../ui/select.tsx";
import { enumEntries } from "../../lib/typescript/enum-values.ts";
import { LanguageFilter } from "../../store/deck-catalog-store.ts";
import { camelCaseToHuman } from "../../lib/string/camel-case-to-human.ts";
import {
LanguageFilter,
languageFilterToNativeName,
} from "../../store/deck-catalog-store.ts";
import { DeckListItemWithDescription } from "../../ui/deck-list-item-with-description.tsx";
import { range } from "../../lib/array/range.ts";
import { DeckLoading } from "../deck-list/deck-loading.tsx";
import { NoDecksMatchingFilters } from "./no-decks-matching-filters.tsx";
import { deckListStore } from "../../store/deck-list-store.ts";
import { DeckAddedLabel } from "./deck-added-label.tsx";
import { t, translateCategory } from "../../translations/t.ts";
import { util } from "zod";
import objectValues = util.objectValues;

export const DeckCatalog = observer(() => {
const store = useDeckCatalogStore();
Expand All @@ -37,38 +41,41 @@ export const DeckCatalog = observer(() => {
marginBottom: 16,
})}
>
<h3 className={css({ textAlign: "center" })}>Deck Catalog</h3>
<div className={css({ display: "flex", gap: 4 })}>
<div className={css({ color: theme.hintColor })}>Available in</div>
<Select<LanguageFilter>
value={store.filters.language.value}
onChange={store.filters.language.onChange}
options={enumEntries(LanguageFilter).map(([name, key]) => ({
value: key,
label: name === "Any" ? "Any language" : camelCaseToHuman(name),
}))}
/>
</div>
<h3 className={css({ textAlign: "center" })}>{t("deck_catalog")}</h3>

<div className={css({ display: "flex", gap: 4 })}>
<div className={css({ color: theme.hintColor })}>Category</div>
<div className={css({ color: theme.hintColor })}>{t("category")}</div>
<Select
value={store.filters.categoryId.value}
onChange={store.filters.categoryId.onChange}
isLoading={store.categories?.state === "pending"}
options={
store.categories?.state === "fulfilled"
? [{ value: "", label: "Any" }].concat(
? [{ value: "", label: t("any_category") }].concat(
store.categories.value.categories.map((category) => ({
value: category.id,
label: category.name,
label: translateCategory(category.name),
})),
)
: []
}
/>
</div>

<div className={css({ display: "flex", gap: 4 })}>
<div className={css({ color: theme.hintColor })}>
{t("i_understand")}
</div>
<Select<LanguageFilter>
value={store.filters.language.value}
onChange={store.filters.language.onChange}
options={objectValues(LanguageFilter).map((key) => ({
value: key,
label: languageFilterToNativeName(key),
}))}
/>
</div>

{(() => {
if (store.decks?.state === "pending") {
return range(5).map((i) => <DeckLoading key={i} />);
Expand Down
7 changes: 5 additions & 2 deletions src/screens/deck-catalog/no-decks-matching-filters.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { css } from "@emotion/css";
import { theme } from "../../ui/theme.tsx";
import React from "react";
import { t } from "../../translations/t.ts";

export const NoDecksMatchingFilters = () => {
return (
Expand All @@ -11,9 +12,11 @@ export const NoDecksMatchingFilters = () => {
textAlign: "center",
})}
>
<div className={css({ fontWeight: 500 })}>No decks found</div>
<div className={css({ fontWeight: 500 })}>
{t("deck_search_not_found")}
</div>
<div className={css({ fontSize: 14, color: theme.hintColor })}>
Try updating filters to see more decks
{t("deck_search_not_found_description")}
</div>
</div>
);
Expand Down
15 changes: 8 additions & 7 deletions src/screens/deck-form/card-form-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Input } from "../../ui/input.tsx";
import React from "react";
import { CardFormType } from "../../store/deck-form-store.ts";
import { HintTransparent } from "../../ui/hint-transparent.tsx";
import { t } from "../../translations/t.ts";

type Props = {
cardForm: CardFormType;
Expand All @@ -23,20 +24,20 @@ export const CardFormView = observer((props: Props) => {
position: "relative",
})}
>
<h3 className={css({ textAlign: "center" })}>Add card</h3>
<Label text={"Front side"} isRequired>
<h3 className={css({ textAlign: "center" })}>{t("add_card")}</h3>
<Label text={t("card_front_title")} isRequired>
<Input field={cardForm.front} rows={3} type={"textarea"} />
<HintTransparent>The prompt or question you'll see</HintTransparent>
<HintTransparent>{t("card_front_side_hint")}</HintTransparent>
</Label>

<Label text={"Back side"} isRequired>
<Label text={t("card_back_title")} isRequired>
<Input field={cardForm.back} rows={3} type={"textarea"} />
<HintTransparent>The response you need to provide</HintTransparent>
<HintTransparent>{t("card_back_side_hint")}</HintTransparent>
</Label>

<Label text={"Example"}>
<Label text={t("card_field_example_title")}>
<Input field={cardForm.example} rows={2} type={"textarea"} />
<HintTransparent>Optional additional information</HintTransparent>
<HintTransparent>{t("card_field_example_hint")}</HintTransparent>
</Label>
</div>
);
Expand Down
3 changes: 2 additions & 1 deletion src/screens/deck-form/card-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import { useDeckFormStore } from "../../store/deck-form-store-context.tsx";
import { useBackButton } from "../../lib/telegram/use-back-button.tsx";
import { CardFormView } from "./card-form-view.tsx";
import { useTelegramProgress } from "../../lib/telegram/use-telegram-progress.tsx";
import { t } from "../../translations/t.ts";

export const CardForm = observer(() => {
const deckFormStore = useDeckFormStore();
const cardForm = deckFormStore.cardForm;
assert(cardForm, "Card should not be empty before editing");

useMainButton(
"Save",
t("save"),
() => {
deckFormStore.saveCardForm();
},
Expand Down
15 changes: 8 additions & 7 deletions src/screens/deck-form/card-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { theme } from "../../ui/theme.tsx";
import { Button } from "../../ui/button.tsx";
import React from "react";
import { reset } from "../../ui/reset.ts";
import { t } from "../../translations/t.ts";

export const CardList = observer(() => {
const deckFormStore = useDeckFormStore();
Expand All @@ -32,12 +33,12 @@ export const CardList = observer(() => {
marginBottom: 16,
})}
>
<h3 className={css({ textAlign: "center" })}>Cards</h3>
<h3 className={css({ textAlign: "center" })}>{t("cards")}</h3>
{deckFormStore.form.cards.length > 1 && (
<>
<Input
field={deckFormStore.cardFilter.text}
placeholder={"Search card"}
placeholder={t("search_card")}
/>
<div
className={css({
Expand All @@ -46,18 +47,18 @@ export const CardList = observer(() => {
gap: 8,
})}
>
<span>Sort by</span>
<span>{t("sort_by")}</span>
{[
{
label: "Date",
label: t("card_sort_by_date"),
fieldName: "createdAt" as const,
},
{
label: "Front",
label: t("card_sort_by_front"),
fieldName: "frontAlpha" as const,
},
{
label: "Back",
label: t("card_sort_by_back"),
fieldName: "backAlpha" as const,
},
].map((item, i) => {
Expand Down Expand Up @@ -111,7 +112,7 @@ export const CardList = observer(() => {
deckFormStore.openNewCardForm();
}}
>
Add card
{t("add_card")}
</Button>
</div>
);
Expand Down
Loading

0 comments on commit 4dfd13f

Please sign in to comment.