Skip to content
Merged
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
59 changes: 51 additions & 8 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ follow instructions for deploying to a local Docker instance. Update `API_BASE_U

## Scripts

| `yarn <name>` | |
| ------------- | ------------------------------------------------------------------- |
| `start-dev` | runs app in development server, reloading on file changes |
| `test` | runs tests in chromium with playwright |
| `build-dev` | bundles app and outputs it in `dist` directory |
| `build` | bundles app app, optimized for production, and outputs it to `dist` |
| `lint` | find and fix auto-fixable javascript errors |
| `format` | formats js, html and css files |
| `yarn <name>` | |
| ------------------ | ------------------------------------------------------------------- |
| `start-dev` | runs app in development server, reloading on file changes |
| `test` | runs tests in chromium with playwright |
| `build-dev` | bundles app and outputs it in `dist` directory |
| `build` | bundles app app, optimized for production, and outputs it to `dist` |
| `lint` | find and fix auto-fixable javascript errors |
| `format` | formats js, html and css files |
| `localize:extract` | generate XLIFF file to be translated |
| `localize:build` | output a localized version of strings/templates |

## Testing

Expand All @@ -51,3 +53,44 @@ To run tests in multiple browsers:
```sh
yarn test --browsers chromium firefox webkit
```

## Localization

Wrap text or templates in the `msg` helper to make them localizable:

```js
// import from @lit/localize:
import { msg } from "@lit/localize";

// later, in the render function:
render() {
return html`
<button>
${msg("Click me")}
</button>
`
}
```

Entire templates can be wrapped as well:

```js
render() {
return msg(html`
<p>Click the button</p>
<button>Click me</button>
`)
}
```

See: <https://lit.dev/docs/localization/overview/#message-types>

To add new languages:

1. Add [BCP 47 language tag](https://www.w3.org/International/articles/language-tags/index.en) to `targetLocales` in `lit-localize.json`
2. Run `yarn localize:extract` to generate new .xlf file in `/xliff`
3. Provide .xlf file to translation team
4. Replace .xlf file once translated
5. Run `yarn localize:build` bring translation into `src`

See: <https://lit.dev/docs/localization/overview/#extracting-messages>
15 changes: 15 additions & 0 deletions frontend/lit-localize.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://raw.githubusercontent.com/lit/lit/main/packages/localize-tools/config.schema.json",
"sourceLocale": "en",
"targetLocales": ["ko"],
"tsConfig": "tsconfig.json",
"output": {
"mode": "runtime",
"outputDir": "src/__generated__/locales",
"localeCodesModule": "src/__generated__/locale-codes.ts"
},
"interchange": {
"format": "xliff",
"xliffDir": "xliff"
}
}
10 changes: 9 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"license": "MIT",
"private": true,
"dependencies": {
"@formatjs/intl-displaynames": "^5.2.5",
"@formatjs/intl-getcanonicallocales": "^1.8.0",
"@lit/localize": "^0.11.1",
"@shoelace-style/shoelace": "^2.0.0-beta.61",
"axios": "^0.22.0",
"lit": "^2.0.0",
Expand All @@ -14,14 +17,19 @@
},
"scripts": {
"test": "web-test-runner \"src/**/*.test.{ts,js}\" --node-resolve --playwright --browsers chromium",
"prebuild": "npm run localize:build",
"prebuild-dev": "npm run localize:build",
"build": "webpack --mode production",
"build-dev": "webpack --mode development",
"start-dev": "webpack serve --mode=development",
"lint": "eslint --fix \"src/**/*.{ts,js}\"",
"format": "prettier --write \"**/*.{ts,js,html,css}\""
"format": "prettier --write \"**/*.{ts,js,html,css}\"",
"localize:extract": "lit-localize extract",
"localize:build": "lit-localize build"
},
"devDependencies": {
"@esm-bundle/chai": "^4.3.4-fix.0",
"@lit/localize-tools": "^0.5.0",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@web/dev-server-esbuild": "^0.2.16",
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/__generated__/locale-codes.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions frontend/src/__generated__/locales/ko.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

90 changes: 90 additions & 0 deletions frontend/src/components/locale-picker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { LitElement, html } from "lit";
import { shouldPolyfill } from "@formatjs/intl-displaynames/should-polyfill";

import { allLocales } from "../__generated__/locale-codes";
import { getLocale, setLocaleFromUrl } from "../utils/localization";
import { localized } from "@lit/localize";

type LocaleCode = typeof allLocales[number];
type LocaleNames = {
[L in LocaleCode]: string;
};

@localized()
export class LocalePicker extends LitElement {
localeNames?: LocaleNames;

private setLocaleName = (locale: LocaleCode) => {
this.localeNames![locale] = new Intl.DisplayNames([locale], {
type: "language",
}).of(locale);
};

async firstUpdated() {
let isFirstPolyfill = true;

// Polyfill if needed
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames#browser_compatibility
// TODO actually test if polyfill works in older browser
const polyfill = async (locale: LocaleCode) => {
if (!shouldPolyfill(locale)) {
return;
}

if (isFirstPolyfill) {
await import("@formatjs/intl-getcanonicallocales/polyfill");
await import("@formatjs/intl-displaynames/polyfill");

isFirstPolyfill = false;
}

try {
await import("@formatjs/intl-displaynames/locale-data/" + locale);
} catch (e) {
console.debug(e);
}
};

await Promise.all(
allLocales.map((locale) => polyfill(locale as LocaleCode))
);

this.localeNames = {} as LocaleNames;
allLocales.forEach(this.setLocaleName);

this.requestUpdate();
}

render() {
if (!this.localeNames) {
return;
}

const selectedLocale = getLocale();

return html`
<sl-select value=${selectedLocale} @sl-change=${this.localeChanged}>
${allLocales.map(
(locale) =>
html`<sl-menu-item
value=${locale}
?selected=${locale === selectedLocale}
>
${this.localeNames![locale]}
</sl-menu-item>`
)}
</sl-select>
`;
}

async localeChanged(event: Event) {
const newLocale = (event.target as HTMLSelectElement).value as LocaleCode;

if (newLocale !== getLocale()) {
const url = new URL(window.location.href);
url.searchParams.set("locale", newLocale);
window.history.pushState(null, "", url.toString());
setLocaleFromUrl();
}
}
}
34 changes: 22 additions & 12 deletions frontend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { msg, updateWhenLocaleChanges } from "@lit/localize";

import "./shoelace";
import { LocalePicker } from "./components/locale-picker";
import { LogInPage } from "./pages/log-in";
import { MyAccountPage } from "./pages/my-account";
import { ArchivePage } from "./pages/archive-info";
Expand All @@ -21,6 +24,12 @@ export class App extends LiteElement {

constructor() {
super();

// Note we use updateWhenLocaleChanges here so that we're always up to date with
// the active locale (the result of getLocale()) when the locale changes via a
// history navigation.
updateWhenLocaleChanges(this);

this.authState = null;

const authState = window.localStorage.getItem("authState");
Expand Down Expand Up @@ -78,6 +87,9 @@ export class App extends LiteElement {
return html`
${this.renderNavBar()}
<div class="w-full h-full px-12 py-12">${this.renderPage()}</div>
<footer class="flex justify-center p-4">
<locale-picker></locale-picker>
</footer>
`;
}

Expand All @@ -87,10 +99,12 @@ export class App extends LiteElement {
${theme}
</style>

<div class="flex p-3 shadow-lg bg-white text-neutral-content">
<div
class="flex p-2 items-center shadow-lg bg-white text-neutral-content"
>
<div class="flex-1 px-2 mx-2">
<a href="/" class="text-lg font-bold" @click="${this.navLink}"
>Browsertrix Cloud</a
>${msg("Browsertrix Cloud")}</a
>
</div>
<div class="flex-none">
Expand All @@ -99,20 +113,15 @@ export class App extends LiteElement {
class="font-bold px-4"
href="/my-account"
@click="${this.navLink}"
>My Account</a
>${msg("My Account")}</a
>
<button class="btn btn-error" @click="${this.onLogOut}">
Log Out
${msg("Log Out")}
</button>`
: html`
<button
class="btn ${this.viewState._route !== "login"
? "btn-primary"
: "btn-ghost"}"
@click="${this.onNeedLogin}"
>
Log In
</button>
<sl-button type="primary" @click="${this.onNeedLogin}">
${msg("Log In")}
</sl-button>
`}
</div>
</div>
Expand Down Expand Up @@ -178,6 +187,7 @@ export class App extends LiteElement {
}
}

customElements.define("locale-picker", LocalePicker);
customElements.define("browsertrix-app", App);
customElements.define("log-in", LogInPage);
customElements.define("my-account", MyAccountPage);
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/shoelace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ import "@shoelace-style/shoelace/dist/themes/light.css";
import "@shoelace-style/shoelace/dist/components/button/button";
import "@shoelace-style/shoelace/dist/components/form/form";
import "@shoelace-style/shoelace/dist/components/input/input";
import "@shoelace-style/shoelace/dist/components/menu/menu";
import "@shoelace-style/shoelace/dist/components/menu-item/menu-item";
import "@shoelace-style/shoelace/dist/components/select/select";
16 changes: 16 additions & 0 deletions frontend/src/utils/localization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { configureLocalization } from "@lit/localize";

import { sourceLocale, targetLocales } from "../__generated__/locale-codes";

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

export const setLocaleFromUrl = async () => {
const url = new URL(window.location.href);
const locale = url.searchParams.get("locale") || sourceLocale;
await setLocale(locale);
};
23 changes: 23 additions & 0 deletions frontend/xliff/ko.xlf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file target-language="ko" source-language="en" original="lit-localize-inputs" datatype="plaintext">
<body>
<trans-unit id="s47d31e4dbe55f7d9">
<source>Browsertrix Cloud</source>
<target>Browsertrix Cloud</target>
</trans-unit>
<trans-unit id="sd03ac20f93055ed8">
<source>My Account</source>
<target>내 계정</target>
</trans-unit>
<trans-unit id="sa03807e44737a915">
<source>Log Out</source>
<target>로그아웃</target>
</trans-unit>
<trans-unit id="sca974356724f8230">
<source>Log In</source>
<target>로그인</target>
</trans-unit>
</body>
</file>
</xliff>
Loading