feat(web): integrate localized application catalogs
- compose app, UI, and independently imported block catalogs from JSON configuration\n- persist the selected locale on the server and activate it without hydration flicker\n- add a flag-based language switcher and localized navigation descriptors\n- mount the redesigned i18n devtool only in development\n- regenerate English and Simplified Chinese catalogs and add switcher coverage
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { CN, US } from "country-flag-icons/react/1x1"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@workspace/ui/components/dropdown-menu"
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
import { HeaderActionButton } from "@workspace/blocks/layout"
|
||||
|
||||
import { appLocales, isAppLocale, type AppLocaleCountry } from "./app-locales"
|
||||
import { useAppLocale } from "./app-i18n-provider"
|
||||
|
||||
const localeFlags = {
|
||||
CN,
|
||||
US,
|
||||
} satisfies Record<AppLocaleCountry, typeof CN>
|
||||
|
||||
function LocaleFlag({
|
||||
country,
|
||||
className,
|
||||
}: {
|
||||
className?: string
|
||||
country: AppLocaleCountry
|
||||
}) {
|
||||
const Flag = localeFlags[country]
|
||||
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"inline-flex shrink-0 overflow-hidden rounded-full ring-1 ring-foreground/15",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Flag className="size-full" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function AppLanguageSwitcher() {
|
||||
const { locale, setLocale } = useAppLocale()
|
||||
const activeLocale =
|
||||
appLocales.find((definition) => definition.locale === locale) ??
|
||||
appLocales[0]
|
||||
const menuLabel = locale === "zh-Hans" ? "语言" : "Language"
|
||||
const triggerLabel =
|
||||
locale === "zh-Hans"
|
||||
? `切换语言,当前为${activeLocale.label}`
|
||||
: `Change language, currently ${activeLocale.label}`
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<HeaderActionButton
|
||||
aria-label={triggerLabel}
|
||||
className="rounded-full"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LocaleFlag country={activeLocale.country} className="size-6" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-44"
|
||||
showArrow={{ cornerRadius: "lg", tailSize: "sm" }}
|
||||
>
|
||||
<DropdownMenuRadioGroup
|
||||
value={locale}
|
||||
onValueChange={(value) => {
|
||||
if (isAppLocale(value)) {
|
||||
setLocale(value)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenuLabel>{menuLabel}</DropdownMenuLabel>
|
||||
{appLocales.map((definition) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={definition.locale}
|
||||
className="py-2"
|
||||
value={definition.locale}
|
||||
>
|
||||
<LocaleFlag country={definition.country} className="size-5" />
|
||||
<span lang={definition.languageTag}>{definition.label}</span>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user