Skip to content

Commit

Permalink
Add frontend localization support (#18)
Browse files Browse the repository at this point in the history
closes #17
  • Loading branch information
SuaYoo committed Nov 20, 2021
1 parent 76e5ceb commit 14f2d13
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 22 deletions.
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

0 comments on commit 14f2d13

Please sign in to comment.