Skip to content

Commit

Permalink
Allow configuring available languages from helm chart (#2230)
Browse files Browse the repository at this point in the history
Closes #2223 

- [x] Adds `localesAvailable` to `/api/settings` endpoint, and uses that
list if available, rather than the full list of translated locales, to
determine which options to display to users
- [x] ~~Uses the user's browser locales, filtered to the current
language setting, for formatting numbers, dates, and durations~~
- [x] Adds & persists checkbox for "use same language for formatting
dates and numbers" in user settings
- [x] Replaces uses of `sl-format-bytes` with `localize.bytes(...)`, and
`sl-format-date` with replacement `btrix-format-date` that properly
handles fallback locales
- [x] Caches all number/duration/datetime formatters by a combined key
consisting of app language, browser language, browser setting, and
formatter options so that all formatters can be reused if needed
(previously any formatter with non-default options would be recreated
every render)
- [x] Splits out ordinal formatting from number formatter, as it didn't
make much sense in some non-English locales
- [x] Adds a little demo of date/time/duration/number formatting so you
can see what effect your language settings have
  


https://github.com/user-attachments/assets/724858cb-b140-4d72-a38d-83f602c71bc7

---------

Signed-off-by: emma <[email protected]>
Co-authored-by: Ilya Kreymer <[email protected]>
Co-authored-by: Ilya Kreymer <[email protected]>
  • Loading branch information
3 people authored Dec 14, 2024
1 parent 46aab91 commit b650762
Show file tree
Hide file tree
Showing 46 changed files with 704 additions and 403 deletions.
7 changes: 5 additions & 2 deletions .github/workflows/ui-tests-playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- 'frontend/*.json'
- 'frontend/*.js'
- 'frontend/*.ts'
- '.github/workflows/ui-tests-playwright.yml'

jobs:
test:
Expand Down Expand Up @@ -69,11 +70,13 @@ jobs:
cat .env
- name: Build frontend
run: cd frontend && yarn build
working-directory: frontend
run: yarn build
id: build-frontend

- name: Run Playwright tests
run: cd frontend && yarn playwright test
working-directory: frontend
run: yarn playwright test

- uses: actions/upload-artifact@v4
if: always()
Expand Down
8 changes: 8 additions & 0 deletions backend/btrixcloud/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import asyncio
import sys
from typing import List, Optional

from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
Expand Down Expand Up @@ -121,6 +122,8 @@ class SettingsResponse(BaseModel):
salesEmail: str = ""
supportEmail: str = ""

localesEnabled: Optional[List[str]]


# ============================================================================
# pylint: disable=too-many-locals, duplicate-code
Expand Down Expand Up @@ -150,6 +153,11 @@ def main() -> None:
signUpUrl=os.environ.get("SIGN_UP_URL", ""),
salesEmail=os.environ.get("SALES_EMAIL", ""),
supportEmail=os.environ.get("EMAIL_SUPPORT", ""),
localesEnabled=(
[lang.strip() for lang in os.environ.get("LOCALES_ENABLED", "").split(",")]
if os.environ.get("LOCALES_ENABLED")
else None
),
)

invites = init_invites(mdb, email)
Expand Down
1 change: 1 addition & 0 deletions backend/test/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ def test_api_settings():
"signUpUrl": "",
"salesEmail": "",
"supportEmail": "",
"localesEnabled": ["en", "es"],
}
2 changes: 2 additions & 0 deletions chart/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ data:

BACKEND_IMAGE_PULL_POLICY: "{{ .Values.backend_pull_policy }}"

LOCALES_ENABLED: "{{ .Values.locales_enabled }}"


---
apiVersion: v1
Expand Down
6 changes: 6 additions & 0 deletions chart/values.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Global Settings
# =========================================

# locales available to choose from in the front end
locales_enabled: "en,es"


# Crawler Settings
# =========================================
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dependencies": {
"@cheap-glitch/mi-cron": "^1.0.1",
"@formatjs/intl-durationformat": "^0.6.4",
"@formatjs/intl-localematcher": "^0.5.9",
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
"@lit/localize": "^0.12.1",
"@lit/task": "^1.0.0",
Expand Down
1 change: 1 addition & 0 deletions frontend/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,6 @@ export default defineConfig({
command: "yarn serve",
port: 9871,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
});
25 changes: 20 additions & 5 deletions frontend/scripts/serve.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
// Serve app locally without building with webpack, e.g. for e2e
const fs = require("fs");
const path = require("path");

const connectHistoryApiFallback = require("connect-history-api-fallback");
const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware");

const distPath = path.join(process.cwd(), "dist");
if (!fs.existsSync(path.join(distPath, "index.html"))) {
throw new Error("dist folder is missing");
}

const dotEnvPath = path.resolve(process.cwd(), ".env.local");
require("dotenv").config({
path: dotEnvPath,
Expand All @@ -18,10 +23,20 @@ const { devServer } = devConfig;

devServer.setupMiddlewares([], { app });

app.use("/", express.static("dist"));
Object.keys(devServer.proxy).forEach((path) => {
app.use(path, createProxyMiddleware(devServer.proxy[path]));
app.use(
path,
createProxyMiddleware({
target: devServer.proxy[path],
changeOrigin: true,
}),
);
});
app.use("/", express.static(distPath));
app.get("/*", (req, res) => {
res.sendFile(path.join(distPath, "index.html"));
});
app.use(connectHistoryApiFallback());

app.listen(9871);
app.listen(9871, () => {
console.log("Server listening on http://localhost:9871");
});
33 changes: 9 additions & 24 deletions frontend/src/components/orgs-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,34 +319,28 @@ 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>
${this.localize.bytes(org.bytesStored)}
</strong>
of data associated with the org.`,
)}
</p>
<ul class="mb-3 text-neutral-600">
<li>
${msg(
html`Crawls:
<sl-format-bytes
value=${org.bytesStoredCrawls}
></sl-format-bytes>`,
html`${msg("Crawls")}:
${this.localize.bytes(org.bytesStoredCrawls)}`,
)}
</li>
<li>
${msg(
html`Uploads:
<sl-format-bytes
value=${org.bytesStoredUploads}
></sl-format-bytes>`,
html`${msg("Uploads")}:
${this.localize.bytes(org.bytesStoredUploads)}`,
)}
</li>
<li>
${msg(
html`Profiles:
<sl-format-bytes
value=${org.bytesStoredProfiles}
></sl-format-bytes>`,
html`${msg("Profiles")}:
${this.localize.bytes(org.bytesStoredProfiles)}`,
)}
</li>
</ul>
Expand Down Expand Up @@ -627,23 +621,14 @@ export class OrgsList extends BtrixElement {
</btrix-table-cell>
<btrix-table-cell class="p-2">
<sl-format-date
class="truncate"
date=${org.created}
month="2-digit"
day="2-digit"
year="2-digit"
></sl-format-date>
${this.localize.date(org.created, { dateStyle: "short" })}
</btrix-table-cell>
<btrix-table-cell class="p-2">
${memberCount ? this.localize.number(memberCount) : none}
</btrix-table-cell>
<btrix-table-cell class="p-2">
${org.bytesStored
? html`<sl-format-bytes
value=${org.bytesStored}
display="narrow"
></sl-format-bytes>`
? this.localize.bytes(org.bytesStored, { unitDisplay: "narrow" })
: none}
</btrix-table-cell>
<btrix-table-cell class="p-1">
Expand Down
5 changes: 1 addition & 4 deletions frontend/src/components/ui/config-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,7 @@ export class ConfigDetails extends BtrixElement {
const renderSize = (valueBytes?: number | null) => {
// Eventually we will want to set this to the selected locale
if (valueBytes) {
return html`<sl-format-bytes
value=${valueBytes}
display="narrow"
></sl-format-bytes>`;
return this.localize.bytes(valueBytes, { unitDisplay: "narrow" });
}

return html`<span class="text-neutral-400"
Expand Down
13 changes: 7 additions & 6 deletions frontend/src/components/ui/file-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
queryAssignedElements,
} from "lit/decorators.js";

import { BtrixElement } from "@/classes/BtrixElement";
import { TailwindElement } from "@/classes/TailwindElement";
import { truncate } from "@/utils/css";

Expand All @@ -19,7 +20,7 @@ export type FileRemoveEvent = CustomEvent<FileRemoveDetail>;
*/
@localized()
@customElement("btrix-file-list-item")
export class FileListItem extends TailwindElement {
export class FileListItem extends BtrixElement {
static styles = [
truncate,
css`
Expand Down Expand Up @@ -82,11 +83,11 @@ export class FileListItem extends TailwindElement {
<div class="name">${this.file.name}</div>
<div class="size">
${this.progressValue !== undefined
? html`<sl-format-bytes
value=${(this.progressValue / 100) * this.file.size}
></sl-format-bytes>
/ `
: ""}<sl-format-bytes value=${this.file.size}></sl-format-bytes>
? html`${this.localize.bytes(
(this.progressValue / 100) * this.file.size,
)}
/ `
: ""}${this.localize.bytes(this.file.size)}
</div>
</div>
<div class="actions">
Expand Down
90 changes: 90 additions & 0 deletions frontend/src/components/ui/format-date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { localized } from "@lit/localize";
import { html, LitElement } from "lit";
import { customElement } from "lit/decorators/custom-element.js";
import { property } from "lit/decorators/property.js";

import { LocalizeController } from "@/controllers/localize";

/**
* Re-implementation of Shoelace's `<sl-format-date>` element using
* Browsertrix's localization implementation.
*
* This allows for multiple locales to be passed into the date formatter, in
* order of the user's preferences.
*/
@customElement("btrix-format-date")
@localized()
export class FormatDate extends LitElement {
private readonly localize = new LocalizeController(this);

/**
* The date/time to format. If not set, the current date and time will be used. When passing a string, it's strongly
* recommended to use the ISO 8601 format to ensure timezones are handled correctly. To convert a date to this format
* in JavaScript, use [`date.toISOString()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString).
*/
@property() date?: Date | string | null = new Date();

/** The format for displaying the weekday. */
@property() weekday?: "narrow" | "short" | "long";

/** The format for displaying the era. */
@property() era?: "narrow" | "short" | "long";

/** The format for displaying the year. */
@property() year?: "numeric" | "2-digit";

/** The format for displaying the month. */
@property() month?: "numeric" | "2-digit" | "narrow" | "short" | "long";

/** The format for displaying the day. */
@property() day?: "numeric" | "2-digit";

/** The format for displaying the hour. */
@property() hour?: "numeric" | "2-digit";

/** The format for displaying the minute. */
@property() minute?: "numeric" | "2-digit";

/** The format for displaying the second. */
@property() second?: "numeric" | "2-digit";

/** The format for displaying the time. */
@property({ attribute: "time-zone-name" }) timeZoneName?: "short" | "long";

/** The time zone to express the time in. */
@property({ attribute: "time-zone" }) timeZone?: string;

/** The format for displaying the hour. */
@property({ attribute: "hour-format" }) hourFormat: "auto" | "12" | "24" =
"auto";

render() {
if (!this.date) return undefined;
const date = new Date(this.date);
const hour12 =
this.hourFormat === "auto" ? undefined : this.hourFormat === "12";

// Check for an invalid date
if (isNaN(date.getMilliseconds())) {
return undefined;
}

return html`
<time datetime=${date.toISOString()}>
${this.localize.date(date, {
weekday: this.weekday,
era: this.era,
year: this.year,
month: this.month,
day: this.day,
hour: this.hour,
minute: this.minute,
second: this.second,
timeZoneName: this.timeZoneName,
timeZone: this.timeZone,
hour12: hour12,
})}
</time>
`;
}
}
5 changes: 3 additions & 2 deletions frontend/src/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import("./copy-button");
import("./copy-field");
import("./details");
import("./file-list");
import("./format-date");
import("./inline-input");
import("./language-select");
import("./user-language-select");
import("./markdown-editor");
import("./markdown-viewer");
import("./menu-item-link");
Expand All @@ -30,9 +30,10 @@ import("./pw-strength-alert");
import("./relative-duration");
import("./search-combobox");
import("./section-heading");
import("./select-crawler");
import("./select-crawler-proxy");
import("./select-crawler");
import("./table");
import("./tag-input");
import("./tag");
import("./time-input");
import("./user-language-select");
4 changes: 2 additions & 2 deletions frontend/src/components/ui/language-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
import sortBy from "lodash/fp/sortBy";

import { allLanguageCodes, type LanguageCode } from "@/types/localization";
import { getBrowserLang } from "@/utils/localize";
import { getDefaultLang } from "@/utils/localize";

const languages = sortBy("name")(
ISO6391.getLanguages(allLanguageCodes),
Expand Down Expand Up @@ -53,7 +53,7 @@ export class LanguageSelect extends LitElement {
return html`
<sl-select
placeholder=${msg("Browser Default")}
value=${ifDefined(this.value || getBrowserLang() || undefined)}
value=${ifDefined(this.value || getDefaultLang())}
size=${ifDefined(this.size)}
?hoist=${this.hoist}
@sl-change=${async (e: Event) => {
Expand Down
Loading

0 comments on commit b650762

Please sign in to comment.