Localize mismatch actions and polish locale UI
Render the switch-to-saved button in the identity locale, translate remaining locale stubs, and use a checkmark for selected dropdown radio items. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -95,13 +95,13 @@
|
|||||||
"next": "Halaman berikutnya"
|
"next": "Halaman berikutnya"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"label": "Language",
|
"label": "Bahasa",
|
||||||
"updateFailed": "Couldn't update language",
|
"updateFailed": "Tidak dapat memperbarui bahasa",
|
||||||
"mismatch": {
|
"mismatch": {
|
||||||
"message": "You're viewing this page in {{language}}.",
|
"message": "Anda sedang melihat halaman ini dalam {{language}}.",
|
||||||
"switchToMine": "Switch to {{language}}",
|
"switchToMine": "Beralih ke {{language}}",
|
||||||
"useThis": "Use {{language}} as my language",
|
"useThis": "Gunakan {{language}} sebagai bahasa saya",
|
||||||
"dismiss": "Dismiss"
|
"dismiss": "Tutup"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,13 +95,13 @@
|
|||||||
"next": "Pagina successiva"
|
"next": "Pagina successiva"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"label": "Language",
|
"label": "Lingua",
|
||||||
"updateFailed": "Couldn't update language",
|
"updateFailed": "Impossibile aggiornare la lingua",
|
||||||
"mismatch": {
|
"mismatch": {
|
||||||
"message": "You're viewing this page in {{language}}.",
|
"message": "Stai visualizzando questa pagina in {{language}}.",
|
||||||
"switchToMine": "Switch to {{language}}",
|
"switchToMine": "Passa a {{language}}",
|
||||||
"useThis": "Use {{language}} as my language",
|
"useThis": "Usa {{language}} come mia lingua",
|
||||||
"dismiss": "Dismiss"
|
"dismiss": "Ignora"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,13 +95,13 @@
|
|||||||
"next": "次のページ"
|
"next": "次のページ"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"label": "Language",
|
"label": "言語",
|
||||||
"updateFailed": "Couldn't update language",
|
"updateFailed": "言語を更新できませんでした",
|
||||||
"mismatch": {
|
"mismatch": {
|
||||||
"message": "You're viewing this page in {{language}}.",
|
"message": "このページは{{language}}で表示されています。",
|
||||||
"switchToMine": "Switch to {{language}}",
|
"switchToMine": "{{language}}に切り替え",
|
||||||
"useThis": "Use {{language}} as my language",
|
"useThis": "{{language}}を自分の言語として使用",
|
||||||
"dismiss": "Dismiss"
|
"dismiss": "閉じる"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,13 +95,13 @@
|
|||||||
"next": "다음 페이지"
|
"next": "다음 페이지"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"label": "Language",
|
"label": "언어",
|
||||||
"updateFailed": "Couldn't update language",
|
"updateFailed": "언어를 업데이트하지 못했습니다",
|
||||||
"mismatch": {
|
"mismatch": {
|
||||||
"message": "You're viewing this page in {{language}}.",
|
"message": "{{language}}(으)로 이 페이지를 보고 있습니다.",
|
||||||
"switchToMine": "Switch to {{language}}",
|
"switchToMine": "{{language}}(으)로 전환",
|
||||||
"useThis": "Use {{language}} as my language",
|
"useThis": "{{language}}을(를) 내 언어로 사용",
|
||||||
"dismiss": "Dismiss"
|
"dismiss": "닫기"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,13 +95,13 @@
|
|||||||
"next": "Następna strona"
|
"next": "Następna strona"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"label": "Language",
|
"label": "Język",
|
||||||
"updateFailed": "Couldn't update language",
|
"updateFailed": "Nie udało się zaktualizować języka",
|
||||||
"mismatch": {
|
"mismatch": {
|
||||||
"message": "You're viewing this page in {{language}}.",
|
"message": "Przeglądasz tę stronę w języku: {{language}}.",
|
||||||
"switchToMine": "Switch to {{language}}",
|
"switchToMine": "Przełącz na {{language}}",
|
||||||
"useThis": "Use {{language}} as my language",
|
"useThis": "Użyj {{language}} jako mojego języka",
|
||||||
"dismiss": "Dismiss"
|
"dismiss": "Zamknij"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,13 +95,13 @@
|
|||||||
"next": "Página seguinte"
|
"next": "Página seguinte"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"label": "Language",
|
"label": "Idioma",
|
||||||
"updateFailed": "Couldn't update language",
|
"updateFailed": "Não foi possível atualizar o idioma",
|
||||||
"mismatch": {
|
"mismatch": {
|
||||||
"message": "You're viewing this page in {{language}}.",
|
"message": "Está a ver esta página em {{language}}.",
|
||||||
"switchToMine": "Switch to {{language}}",
|
"switchToMine": "Mudar para {{language}}",
|
||||||
"useThis": "Use {{language}} as my language",
|
"useThis": "Usar {{language}} como o meu idioma",
|
||||||
"dismiss": "Dismiss"
|
"dismiss": "Ignorar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,13 +95,13 @@
|
|||||||
"next": "Sonraki sayfa"
|
"next": "Sonraki sayfa"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"label": "Language",
|
"label": "Dil",
|
||||||
"updateFailed": "Couldn't update language",
|
"updateFailed": "Dil güncellenemedi",
|
||||||
"mismatch": {
|
"mismatch": {
|
||||||
"message": "You're viewing this page in {{language}}.",
|
"message": "Bu sayfayı {{language}} olarak görüntülüyorsunuz.",
|
||||||
"switchToMine": "Switch to {{language}}",
|
"switchToMine": "{{language}} diline geç",
|
||||||
"useThis": "Use {{language}} as my language",
|
"useThis": "{{language}} dilini benim dilim olarak kullan",
|
||||||
"dismiss": "Dismiss"
|
"dismiss": "Kapat"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,13 +95,13 @@
|
|||||||
"next": "Наступна сторінка"
|
"next": "Наступна сторінка"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"label": "Language",
|
"label": "Мова",
|
||||||
"updateFailed": "Couldn't update language",
|
"updateFailed": "Не вдалося оновити мову",
|
||||||
"mismatch": {
|
"mismatch": {
|
||||||
"message": "You're viewing this page in {{language}}.",
|
"message": "Ви переглядаєте цю сторінку мовою {{language}}.",
|
||||||
"switchToMine": "Switch to {{language}}",
|
"switchToMine": "Перейти на {{language}}",
|
||||||
"useThis": "Use {{language}} as my language",
|
"useThis": "Використовувати {{language}} як мою мову",
|
||||||
"dismiss": "Dismiss"
|
"dismiss": "Закрити"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,13 +95,13 @@
|
|||||||
"next": "下一页"
|
"next": "下一页"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"label": "Language",
|
"label": "语言",
|
||||||
"updateFailed": "Couldn't update language",
|
"updateFailed": "无法更新语言",
|
||||||
"mismatch": {
|
"mismatch": {
|
||||||
"message": "You're viewing this page in {{language}}.",
|
"message": "您正在以{{language}}查看此页面。",
|
||||||
"switchToMine": "Switch to {{language}}",
|
"switchToMine": "切换到{{language}}",
|
||||||
"useThis": "Use {{language}} as my language",
|
"useThis": "将{{language}}设为我的语言",
|
||||||
"dismiss": "Dismiss"
|
"dismiss": "关闭"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,15 @@ import { GlobeIcon, XIcon } from "@phosphor-icons/react";
|
|||||||
import { Button } from "@probo/ui/src/v2/Button/Button";
|
import { Button } from "@probo/ui/src/v2/Button/Button";
|
||||||
import { IconButton } from "@probo/ui/src/v2/IconButton/IconButton";
|
import { IconButton } from "@probo/ui/src/v2/IconButton/IconButton";
|
||||||
import { Text } from "@probo/ui/src/v2/typography/Text";
|
import { Text } from "@probo/ui/src/v2/typography/Text";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql, useFragment } from "react-relay";
|
import { graphql, useFragment } from "react-relay";
|
||||||
|
|
||||||
|
import { DEFAULT_NAMESPACE } from "#/lib/i18n/backend";
|
||||||
import {
|
import {
|
||||||
isUrlLocale,
|
isUrlLocale,
|
||||||
URL_LOCALE_LABELS,
|
URL_LOCALE_LABELS,
|
||||||
|
urlLocaleToLanguage,
|
||||||
} from "#/lib/i18n/locale";
|
} from "#/lib/i18n/locale";
|
||||||
import { useChangeLocale } from "#/lib/i18n/useChangeLocale";
|
import { useChangeLocale } from "#/lib/i18n/useChangeLocale";
|
||||||
import { useLocale } from "#/lib/i18n/useLocale";
|
import { useLocale } from "#/lib/i18n/useLocale";
|
||||||
@@ -49,16 +51,39 @@ interface LocaleMismatchCalloutProps {
|
|||||||
// Full-bleed notice when the URL locale differs from the signed-in identity
|
// Full-bleed notice when the URL locale differs from the signed-in identity
|
||||||
// preference. Dismissed state is React-only (no localStorage/cookies).
|
// preference. Dismissed state is React-only (no localStorage/cookies).
|
||||||
export function LocaleMismatchCallout({ identityKey }: LocaleMismatchCalloutProps) {
|
export function LocaleMismatchCallout({ identityKey }: LocaleMismatchCalloutProps) {
|
||||||
const { t } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const identity = useFragment(localeMismatchCalloutFragment, identityKey);
|
const identity = useFragment(localeMismatchCalloutFragment, identityKey);
|
||||||
const urlLocale = useLocale();
|
const urlLocale = useLocale();
|
||||||
const [changeLocale, isChanging] = useChangeLocale();
|
const [changeLocale, isChanging] = useChangeLocale();
|
||||||
const [updateLocale, isUpdating] = useUpdateLocale();
|
const [updateLocale, isUpdating] = useUpdateLocale();
|
||||||
const [dismissed, setDismissed] = useState(false);
|
const [dismissed, setDismissed] = useState(false);
|
||||||
|
// Bumps after the identity-locale catalog loads so the switch button can
|
||||||
|
// re-render in that language (it may not be the active i18n language).
|
||||||
|
const [, setSavedCatalogTick] = useState(0);
|
||||||
|
|
||||||
const savedLocale = isUrlLocale(identity.locale) ? identity.locale : null;
|
const savedLocale = isUrlLocale(identity.locale) ? identity.locale : null;
|
||||||
|
const savedLanguage = savedLocale != null ? urlLocaleToLanguage(savedLocale) : null;
|
||||||
|
const visible = !dismissed && savedLocale != null && savedLocale !== urlLocale;
|
||||||
|
|
||||||
if (dismissed || savedLocale == null || savedLocale === urlLocale) {
|
useEffect(() => {
|
||||||
|
if (!visible || savedLanguage == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (i18n.hasResourceBundle(savedLanguage, DEFAULT_NAMESPACE)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cancelled = false;
|
||||||
|
void i18n.loadLanguages(savedLanguage).then(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setSavedCatalogTick(tick => tick + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [visible, savedLanguage, i18n]);
|
||||||
|
|
||||||
|
if (!visible || savedLocale == null || savedLanguage == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +130,12 @@ export function LocaleMismatchCallout({ identityKey }: LocaleMismatchCalloutProp
|
|||||||
className="max-md:w-full"
|
className="max-md:w-full"
|
||||||
onClick={switchToSaved}
|
onClick={switchToSaved}
|
||||||
>
|
>
|
||||||
{t("locale.mismatch.switchToMine", { language: savedLabel })}
|
{t("locale.mismatch.switchToMine", {
|
||||||
|
language: savedLabel,
|
||||||
|
// Label this action in the user's saved locale so it reads as
|
||||||
|
// "switch back to my language", not the page they're visiting.
|
||||||
|
lng: savedLanguage,
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size={1}
|
size={1}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { Menu } from "@base-ui/react/menu";
|
import { Menu } from "@base-ui/react/menu";
|
||||||
|
import { CheckIcon } from "@phosphor-icons/react";
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
|
|
||||||
import { useDropdownContext } from "./context";
|
import { useDropdownContext } from "./context";
|
||||||
@@ -31,7 +32,8 @@ export type DropdownRadioItemProps
|
|||||||
shortcut?: ReactNode;
|
shortcut?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A menu item with a radio dot indicator. Used inside a DropdownRadioGroup.
|
// A menu item with a checkmark for the selected value. Used inside a
|
||||||
|
// DropdownRadioGroup (same indicator treatment as DropdownCheckboxItem).
|
||||||
export function DropdownRadioItem(props: DropdownRadioItemProps) {
|
export function DropdownRadioItem(props: DropdownRadioItemProps) {
|
||||||
const { className, shortcut, children, ...rest } = props;
|
const { className, shortcut, children, ...rest } = props;
|
||||||
const { size, variant, highContrast } = useDropdownContext();
|
const { size, variant, highContrast } = useDropdownContext();
|
||||||
@@ -42,7 +44,9 @@ export function DropdownRadioItem(props: DropdownRadioItemProps) {
|
|||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<span className={dropdownItemIndicator()}>
|
<span className={dropdownItemIndicator()}>
|
||||||
<Menu.RadioItemIndicator className="size-1.5 rounded-full bg-current" />
|
<Menu.RadioItemIndicator>
|
||||||
|
<CheckIcon weight="bold" />
|
||||||
|
</Menu.RadioItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
<span className={dropdownItemLabel()}>{children}</span>
|
<span className={dropdownItemLabel()}>{children}</span>
|
||||||
{shortcut != null && <span className={dropdownItemShortcut()}>{shortcut}</span>}
|
{shortcut != null && <span className={dropdownItemShortcut()}>{shortcut}</span>}
|
||||||
|
|||||||
Reference in New Issue
Block a user