Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Localize dates, bytes, and numbers #2146

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/classes/BtrixElement.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TailwindElement } from "./TailwindElement";

import { APIController } from "@/controllers/api";
import { LocalizeController } from "@/controllers/localize";
import { NavigateController } from "@/controllers/navigate";
import { NotifyController } from "@/controllers/notify";
import appState, { use } from "@/utils/state";
Expand All @@ -13,6 +14,7 @@ export class BtrixElement extends TailwindElement {
readonly api = new APIController(this);
readonly notify = new NotifyController(this);
readonly navigate = new NavigateController(this);
readonly localize = new LocalizeController(this);

protected get authState() {
return this.appState.auth;
Expand Down
13 changes: 10 additions & 3 deletions frontend/src/components/orgs-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { when } from "lit/directives/when.js";
import { BtrixElement } from "@/classes/BtrixElement";
import type { Dialog } from "@/components/ui/dialog";
import type { ProxiesAPIResponse, Proxy } from "@/types/crawler";
import { formatNumber, getLocale } from "@/utils/localization";
import { formatNumber } from "@/utils/localization";
import type { OrgData } from "@/utils/orgs";

/**
Expand Down Expand Up @@ -320,7 +320,10 @@ export class OrgsList extends BtrixElement {
${msg(
html`Deleting an org will delete all
<strong class="font-semibold">
<sl-format-bytes value=${org.bytesStored}></sl-format-bytes>
<sl-format-bytes
lang=${this.localize.activeLanguage}
value=${org.bytesStored}
></sl-format-bytes>
</strong>
of data associated with the org.`,
)}
Expand All @@ -330,6 +333,7 @@ export class OrgsList extends BtrixElement {
${msg(
html`Crawls:
<sl-format-bytes
lang=${this.localize.activeLanguage}
value=${org.bytesStoredCrawls}
></sl-format-bytes>`,
)}
Expand All @@ -338,6 +342,7 @@ export class OrgsList extends BtrixElement {
${msg(
html`Uploads:
<sl-format-bytes
lang=${this.localize.activeLanguage}
value=${org.bytesStoredUploads}
></sl-format-bytes>`,
)}
Expand All @@ -346,6 +351,7 @@ export class OrgsList extends BtrixElement {
${msg(
html`Profiles:
<sl-format-bytes
lang=${this.localize.activeLanguage}
value=${org.bytesStoredProfiles}
></sl-format-bytes>`,
)}
Expand Down Expand Up @@ -624,7 +630,7 @@ export class OrgsList extends BtrixElement {

<btrix-table-cell class="p-2">
<sl-format-date
lang=${getLocale()}
lang=${this.localize.activeLanguage}
class="truncate"
date=${org.created}
month="2-digit"
Expand All @@ -638,6 +644,7 @@ export class OrgsList extends BtrixElement {
<btrix-table-cell class="p-2">
${org.bytesStored
? html`<sl-format-bytes
lang=${this.localize.activeLanguage}
value=${org.bytesStored}
display="narrow"
></sl-format-bytes>`
Expand Down
13 changes: 7 additions & 6 deletions frontend/src/components/ui/config-details.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { localized, msg, str } from "@lit/localize";
import ISO6391 from "iso-639-1";
import { nothing } from "lit";
import { html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { when } from "lit/directives/when.js";
import { html as staticHtml, unsafeStatic } from "lit/static-html.js";
Expand All @@ -9,6 +9,7 @@ import RegexColorize from "regex-colorize";

import { RelativeDuration } from "./relative-duration";

import { BtrixElement } from "@/classes/BtrixElement";
import type { CrawlConfig, Seed, SeedConfig } from "@/pages/org/types";
import scopeTypeLabel from "@/strings/crawl-workflows/scopeType";
import sectionStrings from "@/strings/crawl-workflows/section";
Expand All @@ -18,7 +19,6 @@ import { isApiError } from "@/utils/api";
import { getAppSettings } from "@/utils/app";
import { DEPTH_SUPPORTED_SCOPES, isPageScopeType } from "@/utils/crawler";
import { humanizeSchedule } from "@/utils/cron";
import LiteElement, { html } from "@/utils/LiteElement";
import { formatNumber } from "@/utils/localization";
import { pluralOf } from "@/utils/pluralize";

Expand All @@ -32,7 +32,7 @@ import { pluralOf } from "@/utils/pluralize";
*/
@localized()
@customElement("btrix-config-details")
export class ConfigDetails extends LiteElement {
export class ConfigDetails extends BtrixElement {
@property({ type: Object })
crawlConfig?: CrawlConfig;

Expand Down Expand Up @@ -94,6 +94,7 @@ export class ConfigDetails extends LiteElement {
// Eventually we will want to set this to the selected locale
if (valueBytes) {
return html`<sl-format-bytes
lang=${this.localize.activeLanguage}
value=${valueBytes}
display="narrow"
></sl-format-bytes>`;
Expand Down Expand Up @@ -209,7 +210,7 @@ export class ConfigDetails extends LiteElement {
href=${`/orgs/${crawlConfig!.oid}/browser-profiles/profile/${
crawlConfig!.profileid
}`}
@click=${this.navLink}
@click=${this.navigate.link}
>
${crawlConfig?.profileName}
</a>`,
Expand Down Expand Up @@ -483,7 +484,7 @@ export class ConfigDetails extends LiteElement {
try {
await this.getCollections();
} catch (e) {
this.notify({
this.notify.toast({
message:
isApiError(e) && e.statusCode === 404
? msg("Collections not found.")
Expand All @@ -503,7 +504,7 @@ export class ConfigDetails extends LiteElement {

if (this.crawlConfig?.autoAddCollections && orgId) {
for (const collectionId of this.crawlConfig.autoAddCollections) {
const data = await this.apiFetch<Collection | undefined>(
const data = await this.api.fetch<Collection | undefined>(
`/orgs/${orgId}/collections/${collectionId}`,
);
if (data) {
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/components/ui/file-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "lit/decorators.js";

import { TailwindElement } from "@/classes/TailwindElement";
import { LocalizeController } from "@/controllers/localize";
import { truncate } from "@/utils/css";

type FileRemoveDetail = {
Expand Down Expand Up @@ -74,6 +75,8 @@ export class FileListItem extends TailwindElement {
@property({ type: Boolean })
progressIndeterminate?: boolean;

private readonly localize = new LocalizeController(this);

render() {
if (!this.file) return;
return html`<div class="item">
Expand All @@ -83,10 +86,14 @@ export class FileListItem extends TailwindElement {
<div class="size">
${this.progressValue !== undefined
? html`<sl-format-bytes
lang=${this.localize.activeLanguage}
value=${(this.progressValue / 100) * this.file.size}
></sl-format-bytes>
/ `
: ""}<sl-format-bytes value=${this.file.size}></sl-format-bytes>
: ""}<sl-format-bytes
lang=${this.localize.activeLanguage}
value=${this.file.size}
></sl-format-bytes>
</div>
</div>
<div class="actions">
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/ui/language-select.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { localized, msg } from "@lit/localize";
import type { SlSelect } from "@shoelace-style/shoelace";
import ISO6391, { type LanguageCode } from "iso-639-1";
import ISO6391 from "iso-639-1";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import sortBy from "lodash/fp/sortBy";

import { allLanguageCodes, type LanguageCode } from "@/types/localization";
import { getLang } from "@/utils/localization";

const languages = sortBy("name")(
ISO6391.getLanguages(ISO6391.getAllCodes()),
ISO6391.getLanguages(allLanguageCodes),
) as unknown as {
code: LanguageCode;
name: string;
Expand Down
27 changes: 10 additions & 17 deletions frontend/src/components/ui/user-language-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import type { SlSelectEvent } from "@shoelace-style/shoelace";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";

import { sourceLocale } from "@/__generated__/locale-codes";
import { BtrixElement } from "@/classes/BtrixElement";
import { allLocales, type LocaleCodeEnum } from "@/types/localization";
import { getLocale, setLocale } from "@/utils/localization";
import { AppStateService } from "@/utils/state";
import {
translatedLocales,
type TranslatedLocaleEnum,
} from "@/types/localization";

/**
* Select language that Browsertrix app will be shown in
Expand All @@ -23,9 +23,7 @@ export class LocalePicker extends BtrixElement {
private setLocaleNames() {
const localeNames: LocalePicker["localeNames"] = {};

// TODO Add browser-preferred languages
// https://github.com/webrecorder/browsertrix/issues/2143
allLocales.forEach((locale) => {
this.localize.languages.forEach((locale) => {
const name = new Intl.DisplayNames([locale], {
type: "language",
}).of(locale);
Expand All @@ -39,8 +37,7 @@ export class LocalePicker extends BtrixElement {
}

render() {
const selectedLocale =
this.appState.userPreferences?.locale || sourceLocale;
const selectedLocale = this.localize.activeLanguage;

return html`
<sl-dropdown
Expand All @@ -53,11 +50,11 @@ export class LocalePicker extends BtrixElement {
slot="trigger"
size="small"
caret
?disabled=${(allLocales as unknown as string[]).length < 2}
?disabled=${(translatedLocales as unknown as string[]).length < 2}
>
<sl-icon slot="prefix" name="translate"></sl-icon>
<span class="capitalize"
>${this.localeNames[selectedLocale as LocaleCodeEnum]}</span
>${this.localeNames[selectedLocale as TranslatedLocaleEnum]}</span
>
</sl-button>
<sl-menu>
Expand All @@ -80,12 +77,8 @@ export class LocalePicker extends BtrixElement {
}

async localeChanged(event: SlSelectEvent) {
const newLocale = event.detail.item.value as LocaleCodeEnum;
const newLocale = event.detail.item.value as TranslatedLocaleEnum;

AppStateService.partialUpdateUserPreferences({ locale: newLocale });

if (newLocale !== getLocale()) {
void setLocale(newLocale);
}
this.localize.setLanguage(newLocale);
}
}
100 changes: 100 additions & 0 deletions frontend/src/controllers/localize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { configureLocalization } from "@lit/localize";
import type { ReactiveController, ReactiveControllerHost } from "lit";
import uniq from "lodash/fp/uniq";

import { sourceLocale, targetLocales } from "@/__generated__/locale-codes";
import {
languageCodeSchema,
translatedLocales,
type AllLanguageCodes,
type LanguageCode,
} from "@/types/localization";
import { getLang, langShortCode } from "@/utils/localization";
import { numberFormatter } from "@/utils/number";
import appState from "@/utils/state";

const { getLocale, setLocale } = configureLocalization({
sourceLocale,
targetLocales,
loadLocale: async (locale: string) =>
import(`/src/__generated__/locales/${locale}.ts`),
});

// Shared throughout app:
let activeLanguage = sourceLocale;
const defaultNumberFormatter = numberFormatter(activeLanguage);
const numberFormatters = new Map([[activeLanguage, defaultNumberFormatter]]);

export const localizedNumberFormat = (
numberFormatters.get(activeLanguage) || defaultNumberFormatter
).format;

export function getActiveLanguage() {
return activeLanguage;
}

/**
* Manage app localization
*/
export class LocalizeController implements ReactiveController {
private readonly host: ReactiveControllerHost & EventTarget;

get activeLanguage() {
return activeLanguage;
}
set activeLanguage(val) {
activeLanguage = val;
}

get number() {
return (numberFormatters.get(this.activeLanguage) || defaultNumberFormatter)
.format;
}

get languages() {
return uniq([
...translatedLocales,
...window.navigator.languages.map(langShortCode),
]);
}

constructor(host: LocalizeController["host"]) {
this.host = host;
host.addController(this);
}

hostConnected() {}
hostDisconnected() {}

initLanguage() {
this.setLanguage(
appState.userPreferences?.language || getLang() || sourceLocale,
);
}

/**
* User-initiated language setting
*/
setLanguage(lang: LanguageCode) {
const { error } = languageCodeSchema.safeParse(lang);

if (error) {
console.debug("Error setting language:", error);
return;
}

numberFormatters.set(lang, numberFormatter(lang));

this.activeLanguage = lang;
this.setTranslation(lang);
}

private setTranslation(lang: LanguageCode) {
if (
lang !== getLocale() &&
(translatedLocales as AllLanguageCodes).includes(lang)
) {
void setLocale(lang);
}
}
}
Loading