feat: Add details on translation status (#2139)

languages specified in `translatedLocales`
- Updates user language preference beta badge text
- Adds link to translation contribution docs
- Refactors component name

---------

Co-authored-by: SuaYoo <SuaYoo@users.noreply.github.com>
Co-authored-by: Henry Wilkinson <henry@wilkinson.graphics>
Co-authored-by: emma <hi@emma.cafe>
This commit is contained in:
sua yoo 2024-11-12 13:59:34 -08:00 committed by GitHub
parent 0fb6571990
commit ab9edfa064
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 180 additions and 114 deletions

View File

@ -1,7 +1,7 @@
name: Localization Request
description: Request a new language or translation.
title: "[L10N]: "
labels: ["enhancement"]
labels: ["localization"]
body:
- type: textarea
attributes:

View File

@ -35,6 +35,8 @@ To add a new language directly through code change:
3. Open a pull request with the changes.
4. Once the pull request is merged, manually refresh the language list in the [Weblate Browsertrix project](https://hosted.weblate.org/projects/browsertrix). Translations are managed entirely through the Weblate interface.
New languages will be available in user preferences only after the app is redeployed.
## Making Strings Localizable
All text should be wrapped in the `msg` helper to make them localizable:

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<path fill="currentColor" d="M4 .5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1H11v1h-1a.5.5 0 0 0 0 1h1v1h-1a.5.5 0 0 0 0 1h1v1h-1a.5.5 0 0 0 0 1h1.392l.61 1H11a.5.5 0 0 0 0 1h1.5a.5.5 0 0 0 .107-.011L13.225 10H12a.5.5 0 0 0 0 1h1.836l.611 1H13a.5.5 0 0 0 0 1h2v-.095l.497.813A1.5 1.5 0 0 1 14.217 16H1.783a1.5 1.5 0 0 1-1.28-2.282L5 6.359V1h-.5A.5.5 0 0 1 4 .5"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

View File

@ -14,12 +14,14 @@ const styles = unsafeCSS(stylesheet);
export class BetaIcon extends TailwindElement {
static styles = styles;
render() {
return html`<sl-tooltip content=${msg("Beta feature")} hoist>
return html`<sl-tooltip hoist>
<sl-icon
name="stars"
name="flask-fill"
library="app"
label="Beta"
class="size-4 text-brand-green"
></sl-icon>
<span slot="content" class="text-xs">${msg("Beta feature")}</span>
</sl-tooltip>`;
}
}
@ -34,15 +36,20 @@ export class BetaBadge extends TailwindElement {
render() {
return html`<sl-tooltip hoist placement=${this.placement}>
<div slot="content">
<b>${msg("This part of Browsertrix is in beta!")}</b>
${msg(
"Parts might change or be broken. Please share your thoughts with us!",
)}
<div slot="content" class="text-xs">
<slot name="content">
<b>${msg("This part of Browsertrix is in beta!")}</b>
<p>
${msg(
"Parts might change or be broken. Please share your thoughts with us!",
)}
</p>
</slot>
</div>
<span class="inline-block align-middle text-xs text-brand-green">
<sl-icon
name="stars"
name="flask-fill"
library="app"
label="Beta feature"
class="size-4 align-middle"
></sl-icon

View File

@ -18,7 +18,7 @@ import("./details");
import("./file-list");
import("./inline-input");
import("./language-select");
import("./locale-picker");
import("./user-language-select");
import("./markdown-editor");
import("./markdown-viewer");
import("./menu-item-link");

View File

@ -1,77 +0,0 @@
import { localized } from "@lit/localize";
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";
type LocaleNames = {
[L in LocaleCodeEnum]: string;
};
@localized()
@customElement("btrix-locale-picker")
export class LocalePicker extends BtrixElement {
@state()
private localeNames: LocaleNames | undefined = {} as LocaleNames;
private readonly setLocaleName = (locale: LocaleCodeEnum) => {
this.localeNames![locale] = new Intl.DisplayNames([locale], {
type: "language",
}).of(locale.toUpperCase())!;
};
firstUpdated() {
this.localeNames = {} as LocaleNames;
allLocales.forEach(this.setLocaleName);
}
render() {
if (!this.localeNames) {
return;
}
const selectedLocale =
this.appState.userPreferences?.locale || sourceLocale;
return html`
<sl-dropdown
@sl-select=${this.localeChanged}
placement="top-end"
distance="4"
hoist
>
<sl-button class="capitalize" slot="trigger" size="small" caret>
${this.localeNames[selectedLocale as LocaleCodeEnum]}
</sl-button>
<sl-menu>
${allLocales.map(
(locale) =>
html`<sl-menu-item
class="capitalize"
type="checkbox"
value=${locale}
?checked=${locale === selectedLocale}
>
${this.localeNames![locale]}
</sl-menu-item>`,
)}
</sl-menu>
</sl-dropdown>
`;
}
async localeChanged(event: SlSelectEvent) {
const newLocale = event.detail.item.value as LocaleCodeEnum;
AppStateService.partialUpdateUserPreferences({ locale: newLocale });
if (newLocale !== getLocale()) {
void setLocale(newLocale);
}
}
}

View File

@ -0,0 +1,91 @@
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";
/**
* Select language that Browsertrix app will be shown in
*/
@customElement("btrix-user-language-select")
export class LocalePicker extends BtrixElement {
@state()
private localeNames: { [locale: string]: string } = {};
firstUpdated() {
this.setLocaleNames();
}
private setLocaleNames() {
const localeNames: LocalePicker["localeNames"] = {};
// TODO Add browser-preferred languages
// https://github.com/webrecorder/browsertrix/issues/2143
allLocales.forEach((locale) => {
const name = new Intl.DisplayNames([locale], {
type: "language",
}).of(locale);
if (!name) return;
localeNames[locale] = name;
});
this.localeNames = localeNames;
}
render() {
const selectedLocale =
this.appState.userPreferences?.locale || sourceLocale;
return html`
<sl-dropdown
@sl-select=${this.localeChanged}
placement="top-end"
distance="4"
hoist
>
<sl-button
slot="trigger"
size="small"
caret
?disabled=${(allLocales as unknown as string[]).length < 2}
>
<sl-icon slot="prefix" name="translate"></sl-icon>
<span class="capitalize"
>${this.localeNames[selectedLocale as LocaleCodeEnum]}</span
>
</sl-button>
<sl-menu>
${Object.keys(this.localeNames)
.sort()
.map(
(locale) =>
html`<sl-menu-item
class="capitalize"
type="checkbox"
value=${locale}
?checked=${locale === selectedLocale}
>
${this.localeNames[locale]}
</sl-menu-item>`,
)}
</sl-menu>
</sl-dropdown>
`;
}
async localeChanged(event: SlSelectEvent) {
const newLocale = event.detail.item.value as LocaleCodeEnum;
AppStateService.partialUpdateUserPreferences({ locale: newLocale });
if (newLocale !== getLocale()) {
void setLocale(newLocale);
}
}
}

View File

@ -27,7 +27,6 @@ import type { ListWorkflow } from "@/types/crawler";
import { humanizeSchedule } from "@/utils/cron";
import { srOnly, truncate } from "@/utils/css";
import { formatNumber, getLocale } from "@/utils/localization";
import { numberFormatter } from "@/utils/number";
import { pluralOf } from "@/utils/pluralize";
const formatNumberCompact = (v: number) =>
@ -245,13 +244,9 @@ export class WorkflowListItem extends LitElement {
${this.safeRender((workflow) => {
if (workflow.schedule) {
return msg(
str`${humanizeSchedule(
workflow.schedule,
{
length: "short",
},
numberFormatter,
)}`,
str`${humanizeSchedule(workflow.schedule, {
length: "short",
})}`,
);
}
if (workflow.lastStartedByName) {

View File

@ -30,7 +30,7 @@ import type { NavigateEventDetail } from "@/controllers/navigate";
import type { NotifyEventDetail } from "@/controllers/notify";
import { theme } from "@/theme";
import { type Auth } from "@/types/auth";
import { type LocaleCodeEnum } from "@/types/localization";
import { translatedLocales, type LocaleCodeEnum } from "@/types/localization";
import { type AppSettings } from "@/utils/app";
import {
getLocale,
@ -454,9 +454,13 @@ export class App extends BtrixElement {
</sl-dropdown>`
: html`
${this.renderSignUpLink()}
<btrix-locale-picker
@sl-select=${this.onSelectLocale}
></btrix-locale-picker>
${(translatedLocales as unknown as string[]).length > 1
? html`
<btrix-user-language-select
@sl-select=${this.onSelectLocale}
></btrix-user-language-select>
`
: nothing}
`}
</div>
${isSuperAdmin

View File

@ -243,7 +243,7 @@ export class AccountSettings extends LiteElement {
</form>
${(allLocales as unknown as string[]).length > 1
? this.renderPreferences()
? this.renderLanguage()
: nothing}
`;
}
@ -322,18 +322,45 @@ export class AccountSettings extends LiteElement {
`;
}
private renderPreferences() {
private renderLanguage() {
return html`
<h2 class="mb-2 mt-7 text-lg font-medium">${msg("Preferences")}</h2>
<h2 class="mb-2 mt-7 flex items-center gap-2 text-lg font-medium">
${msg("Language")}
<btrix-beta-badge>
<div slot="content">
<b>${msg("Translations are in beta")}</b>
<p>
${msg(
"Parts of the app may not be translated yet in some languages.",
)}
</p>
</div>
</btrix-beta-badge>
</h2>
<section class="mb-5 rounded-lg border">
<div class="flex items-center justify-between px-4 py-2.5">
<h3 class="font-medium">
${msg("Language")} <btrix-beta-badge></btrix-beta-badge>
<div class="flex items-center justify-between gap-2 px-4 py-2.5">
<h3>
${msg(
"Choose your preferred language for displaying Browsertrix in your browser.",
)}
</h3>
<btrix-locale-picker
<btrix-user-language-select
@sl-select=${this.onSelectLocale}
></btrix-locale-picker>
></btrix-user-language-select>
</div>
<footer class="flex items-center justify-start border-t px-4 py-3">
<p class="text-neutral-600">
${msg("Help us translate Browsertrix.")}
<a
class="inline-flex items-center gap-1 text-blue-500 hover:text-blue-600"
href="https://docs.browsertrix.com/develop/localization/"
target="_blank"
>
${msg("Contribute to translations")}
<sl-icon slot="suffix" name="arrow-right"></sl-icon
></a>
</p>
</footer>
</section>
`;
}

View File

@ -280,7 +280,7 @@ export class OrgSettingsBilling extends BtrixElement {
quotas.maxExecMinutesPerMonth &&
humanizeSeconds(
quotas.maxExecMinutesPerMonth * 60,
undefined,
getLocale(),
undefined,
"long",
);

View File

@ -3,5 +3,8 @@ import { z } from "zod";
import { allLocales } from "@/__generated__/locale-codes";
export { allLocales };
// Translated languages to show in app:
export const translatedLocales = ["en"] as const;
export const localeCodeEnum = z.enum(allLocales);
export type LocaleCodeEnum = z.infer<typeof localeCodeEnum>;

View File

@ -62,7 +62,6 @@ export function humanizeNextDate(
export function humanizeSchedule(
schedule: string,
options: { length?: "short" } = {},
numberFormatter = numberUtils.numberFormatter,
): string {
const interval = getScheduleInterval(schedule);
const parsed = parseCron(schedule);
@ -92,7 +91,7 @@ export function humanizeSchedule(
intervalMsg = msg(str`Every ${formattedWeekDay}`);
break;
case "monthly": {
const { format } = numberFormatter(getLocale());
const { format } = numberUtils.numberFormatter(getLocale());
intervalMsg = msg(
str`Monthly on the ${format(days[0], { ordinal: true })}`,
);

View File

@ -2929,7 +2929,7 @@
</trans-unit>
<trans-unit id="saf63d34c8601dd41">
<source>
<x equiv-text="${humanizeSchedule(workflow.schedule, {&#10; length: &quot;short&quot;,&#10;}, numberFormatter)}" id="0"/>
<x equiv-text="${humanizeSchedule(workflow.schedule, {&#10; length: &quot;short&quot;,&#10;})}" id="0"/>
</source>
</trans-unit>
<trans-unit id="s136b21dec9a221bd">
@ -3773,9 +3773,6 @@
<trans-unit id="s01a8d45397ec133b">
<source>Security</source>
</trans-unit>
<trans-unit id="s2b5047d39b9baf3d">
<source>Preferences</source>
</trans-unit>
<trans-unit id="sfa66f095b5a35ccc">
<source>Your language preference has been updated.</source>
</trans-unit>
@ -3786,6 +3783,21 @@
<x equiv-text="&lt;a class=&quot;text-primary hover:text-primary-400&quot; href=&quot;/docs/user-guide/browser-profiles/&quot; target=&quot;_blank&quot;&gt;&#10; ${msg(&quot;browser profile best practices&quot;)}&lt;/a&gt;" id="0"/>.
</source>
</trans-unit>
<trans-unit id="s5f35a66624e4d770">
<source>Translations are in beta</source>
</trans-unit>
<trans-unit id="s3f3f33356a6d42ff">
<source>Parts of the app may not be translated yet in some languages.</source>
</trans-unit>
<trans-unit id="sae935ffa510dc00f">
<source>Help us translate Browsertrix.</source>
</trans-unit>
<trans-unit id="s3b8e2d51d9b86e21">
<source>Contribute to translations</source>
</trans-unit>
<trans-unit id="seb49ad0f81062f64">
<source>Choose your preferred language for displaying Browsertrix in your browser.</source>
</trans-unit>
</body>
</file>
</xliff>