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:
parent
0fb6571990
commit
ab9edfa064
@ -1,7 +1,7 @@
|
||||
name: Localization Request
|
||||
description: Request a new language or translation.
|
||||
title: "[L10N]: "
|
||||
labels: ["enhancement"]
|
||||
labels: ["localization"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
@ -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:
|
||||
|
3
frontend/src/assets/icons/flask-fill.svg
Normal file
3
frontend/src/assets/icons/flask-fill.svg
Normal 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 |
@ -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
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
91
frontend/src/components/ui/user-language-select.ts
Normal file
91
frontend/src/components/ui/user-language-select.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
@ -280,7 +280,7 @@ export class OrgSettingsBilling extends BtrixElement {
|
||||
quotas.maxExecMinutesPerMonth &&
|
||||
humanizeSeconds(
|
||||
quotas.maxExecMinutesPerMonth * 60,
|
||||
undefined,
|
||||
getLocale(),
|
||||
undefined,
|
||||
"long",
|
||||
);
|
||||
|
@ -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>;
|
||||
|
@ -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 })}`,
|
||||
);
|
||||
|
@ -2929,7 +2929,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="saf63d34c8601dd41">
|
||||
<source>
|
||||
<x equiv-text="${humanizeSchedule(workflow.schedule, { length: "short", }, numberFormatter)}" id="0"/>
|
||||
<x equiv-text="${humanizeSchedule(workflow.schedule, { length: "short", })}" 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="<a class="text-primary hover:text-primary-400" href="/docs/user-guide/browser-profiles/" target="_blank"> ${msg("browser profile best practices")}</a>" 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>
|
||||
|
Loading…
Reference in New Issue
Block a user