feat(i18n): standardize bilingual catalogs and tooling

This commit is contained in:
Maofeng
2026-09-20 15:52:31 +08:00
parent ea00a71112
commit 6cd981d6a2
92 changed files with 685 additions and 1986 deletions
+10 -5
View File
@@ -1,6 +1,6 @@
{ {
"sourceLocale": "en", "sourceLocale": "en-US",
"locales": ["en", "zh-Hans"], "locales": ["en-US", "zh-Hans"],
"catalogPath": "src/locales/{locale}/messages", "catalogPath": "src/locales/{locale}/messages",
"catalogSources": [ "catalogSources": [
"@workspace/ui/locales/{locale}", "@workspace/ui/locales/{locale}",
@@ -10,9 +10,14 @@
"@workspace/blocks/media/locales/{locale}", "@workspace/blocks/media/locales/{locale}",
"@workspace/blocks/navigation/locales/{locale}", "@workspace/blocks/navigation/locales/{locale}",
"@workspace/blocks/notifications/locales/{locale}", "@workspace/blocks/notifications/locales/{locale}",
"@workspace/blocks/search/locales/{locale}", "@workspace/search/locales/{locale}",
"@workspace/lexical/locales/{locale}" "@workspace/lexical/locales/{locale}"
], ],
"include": ["src", "../../packages/blocks/src", "../../packages/ui/src"], "include": [
"src",
"../../packages/blocks/src",
"../../packages/search/src",
"../../packages/ui/src"
],
"exclude": ["**/*.test.{ts,tsx}", "**/node_modules/**"] "exclude": ["**/*.test.{ts,tsx}", "**/node_modules/**"]
} }
+2 -2
View File
@@ -19,7 +19,7 @@ import {
I18nProvider as UiI18nProvider, I18nProvider as UiI18nProvider,
type CalendarLocale, type CalendarLocale,
} from "@workspace/ui/components/i18n" } from "@workspace/ui/components/i18n"
import { calendarLocale as englishCalendarLocale } from "@workspace/ui/locales/en" import { calendarLocale as englishCalendarLocale } from "@workspace/ui/locales/en-US"
import { calendarLocale as simplifiedChineseCalendarLocale } from "@workspace/ui/locales/zh-Hans" import { calendarLocale as simplifiedChineseCalendarLocale } from "@workspace/ui/locales/zh-Hans"
import { appLocales, DEFAULT_APP_LOCALE, type AppLocale } from "./app-locales" import { appLocales, DEFAULT_APP_LOCALE, type AppLocale } from "./app-locales"
@@ -27,7 +27,7 @@ import { AppI18nDevtool } from "./app-i18n-devtool"
import { setAppLocale as persistAppLocale } from "@/server/app-locale.functions" import { setAppLocale as persistAppLocale } from "@/server/app-locale.functions"
const appCalendarLocales: Partial<Record<AppLocale, CalendarLocale>> = { const appCalendarLocales: Partial<Record<AppLocale, CalendarLocale>> = {
en: englishCalendarLocale, "en-US": englishCalendarLocale,
"zh-Hans": simplifiedChineseCalendarLocale, "zh-Hans": simplifiedChineseCalendarLocale,
} }
@@ -7,16 +7,11 @@ import { AppLanguageSwitcher } from "./app-language-switcher"
vi.mock("./app-i18n-provider", () => ({ vi.mock("./app-i18n-provider", () => ({
useAppLocale: () => ({ useAppLocale: () => ({
locale: "en", locale: "en-US",
setLocale: vi.fn(), setLocale: vi.fn(),
}), }),
})) }))
vi.mock("country-flag-icons/react/1x1", () => ({
CN: () => <svg />,
US: () => <svg />,
}))
beforeAll(() => { beforeAll(() => {
vi.stubGlobal( vi.stubGlobal(
"ResizeObserver", "ResizeObserver",
+2 -2
View File
@@ -1,6 +1,6 @@
import type { LocaleDefinition } from "@workspace/i18n" import type { LocaleDefinition } from "@workspace/i18n"
export type AppLocale = "en" | "zh-Hans" export type AppLocale = "en-US" | "zh-Hans"
export type AppLocaleCountry = "CN" | "US" export type AppLocaleCountry = "CN" | "US"
export interface AppLocaleDefinition extends LocaleDefinition { export interface AppLocaleDefinition extends LocaleDefinition {
@@ -16,7 +16,7 @@ export const appLocales = [
direction: "ltr", direction: "ltr",
languageTag: "en-US", languageTag: "en-US",
label: "English", label: "English",
locale: "en", locale: "en-US",
}, },
{ {
country: "CN", country: "CN",
@@ -4,7 +4,7 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n" "X-Generator: @lingui/cli\n"
"Language: en\n" "Language: en-US\n"
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: \n" "POT-Creation-Date: \n"
@@ -23,6 +23,21 @@ msgstr "Account password"
msgid "blocks.appearance.active" msgid "blocks.appearance.active"
msgstr "Active" msgstr "Active"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:16
msgid "blocks.chats.actions.addEmoji"
msgstr "Add emoji"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:4
msgid "blocks.chats.actions.addFiles"
msgstr "Add files"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:8
msgid "blocks.chats.actions.addImage"
msgstr "Add image"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:276 #: src/data/navigation-messages.ts:276
msgid "app.navigation.system-members-admins" msgid "app.navigation.system-members-admins"
@@ -33,6 +48,11 @@ msgstr "Administrator accounts"
msgid "app.navigation.after-sales-center" msgid "app.navigation.after-sales-center"
msgstr "After-sales" msgstr "After-sales"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:12
msgid "blocks.chats.emojiPicker.allEmojis"
msgstr "All emojis"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:91 #: ../../packages/blocks/src/blocks/appearance/messages.ts:91
msgid "blocks.appearance.color.amber" msgid "blocks.appearance.color.amber"
@@ -73,6 +93,11 @@ msgstr "Awaiting payment"
msgid "app.navigation.trade-orders-awaiting-shipment" msgid "app.navigation.trade-orders-awaiting-shipment"
msgstr "Awaiting shipment" msgstr "Awaiting shipment"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:20
msgid "blocks.chats.actions.backToConversations"
msgstr "Back to conversations"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:16 #: ../../packages/blocks/src/blocks/appearance/messages.ts:16
msgid "blocks.appearance.baseColor" msgid "blocks.appearance.baseColor"
@@ -124,7 +149,7 @@ msgid "app.navigation.report-channels"
msgstr "Channel analysis" msgstr "Channel analysis"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:4 #: ../../packages/blocks/src/blocks/chats/messages.ts:112
msgid "blocks.chats.title" msgid "blocks.chats.title"
msgstr "Chats" msgstr "Chats"
@@ -133,6 +158,11 @@ msgstr "Chats"
msgid "blocks.notifications.error.description" msgid "blocks.notifications.error.description"
msgstr "Check your network connection and try again." msgstr "Check your network connection and try again."
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:108
msgid "blocks.chats.selectConversation.description"
msgstr "Choose a conversation from the list to read its messages."
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:29 #: ../../packages/ui/src/components/i18n.tsx:29
msgid "ui.common.close" msgid "ui.common.close"
@@ -158,6 +188,11 @@ msgstr "Completed orders"
msgid "app.navigation.content-assets" msgid "app.navigation.content-assets"
msgstr "Content" msgstr "Content"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:24
msgid "blocks.chats.actions.conversationDetails"
msgstr "Conversation details"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:192 #: src/data/navigation-messages.ts:192
msgid "app.navigation.marketing-coupons" msgid "app.navigation.marketing-coupons"
@@ -218,6 +253,11 @@ msgstr "Dark theme"
msgid "app.navigation.data-and-finance" msgid "app.navigation.data-and-finance"
msgstr "Data and finance" msgstr "Data and finance"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:52
msgid "blocks.chats.messageStatus.delivered"
msgstr "Delivered"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:296 #: src/data/navigation-messages.ts:296
msgid "app.navigation.system-notification-logs" msgid "app.navigation.system-notification-logs"
@@ -343,6 +383,11 @@ msgstr "Light theme"
msgid "blocks.appearance.color.lime" msgid "blocks.appearance.color.lime"
msgstr "Lime" msgstr "Lime"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:28
msgid "blocks.chats.actions.loadEarlier"
msgstr "Load earlier messages"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:37 #: ../../packages/ui/src/components/i18n.tsx:37
msgid "ui.common.loading" msgid "ui.common.loading"
@@ -383,6 +428,11 @@ msgstr "Media"
msgid "app.navigation.loyalty-center" msgid "app.navigation.loyalty-center"
msgstr "Membership benefits" msgstr "Membership benefits"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:84
msgid "blocks.chats.emptyMessages.description"
msgstr "Messages in this conversation will appear here."
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:127 #: ../../packages/blocks/src/blocks/appearance/messages.ts:127
msgid "blocks.appearance.color.mist" msgid "blocks.appearance.color.mist"
@@ -433,11 +483,31 @@ msgstr "Next"
msgid "ui.carousel.nextSlide" msgid "ui.carousel.nextSlide"
msgstr "Next slide" msgstr "Next slide"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:80
msgid "blocks.chats.emptyConversations.title"
msgstr "No conversations"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:92
msgid "blocks.chats.emptySearch"
msgstr "No conversations match your search."
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:88
msgid "blocks.chats.emptyMessages.title"
msgstr "No messages yet"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:12 #: ../../packages/blocks/src/blocks/notifications/messages.ts:12
msgid "blocks.notifications.empty.title" msgid "blocks.notifications.empty.title"
msgstr "No notifications" msgstr "No notifications"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:56
msgid "blocks.chats.messageStatus.failed"
msgstr "Not sent"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:292 #: src/data/navigation-messages.ts:292
msgid "app.navigation.system-notification-templates" msgid "app.navigation.system-notification-templates"
@@ -543,6 +613,11 @@ msgstr "Pink"
msgid "app.navigation.loyalty-points" msgid "app.navigation.loyalty-points"
msgstr "Points rules" msgstr "Points rules"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:44
msgid "blocks.chats.actions.sendOnEnter"
msgstr "Press Enter to send"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:65 #: ../../packages/ui/src/components/i18n.tsx:65
msgid "ui.pagination.previous" msgid "ui.pagination.previous"
@@ -598,11 +673,21 @@ msgstr "Products and inventory"
msgid "blocks.appearance.color.purple" msgid "blocks.appearance.color.purple"
msgstr "Purple" msgstr "Purple"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:60
msgid "blocks.chats.messageStatus.read"
msgstr "Read"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:12 #: src/data/navigation-messages.ts:12
msgid "app.navigation.overview-realtime" msgid "app.navigation.overview-realtime"
msgstr "Real-time monitoring" msgstr "Real-time monitoring"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:64
msgid "blocks.chats.emojiPicker.recentEmojis"
msgstr "Recently used"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:256 #: src/data/navigation-messages.ts:256
msgid "app.navigation.finance-reconciliation" msgid "app.navigation.finance-reconciliation"
@@ -638,6 +723,11 @@ msgstr "Refresh the current page data"
msgid "app.navigation.after-sales-refunds" msgid "app.navigation.after-sales-refunds"
msgstr "Refund review" msgstr "Refund review"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:32
msgid "blocks.chats.actions.removeAttachment"
msgstr "Remove {name}"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:228 #: src/data/navigation-messages.ts:228
msgid "app.navigation.report-center" msgid "app.navigation.report-center"
@@ -668,6 +758,36 @@ msgstr "Rose"
msgid "app.navigation.report-sales" msgid "app.navigation.report-sales"
msgstr "Sales analysis" msgstr "Sales analysis"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:100
msgid "blocks.chats.search"
msgstr "Search conversations"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:104
msgid "blocks.chats.selectConversation"
msgstr "Select conversation {title}"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:40
msgid "blocks.chats.actions.send"
msgstr "Send"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:72
msgid "blocks.chats.messageStatus.sending"
msgstr "Sending"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:48
msgid "blocks.chats.actions.sending"
msgstr "Sending…"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:68
msgid "blocks.chats.messageStatus.sent"
msgstr "Sent"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:252 #: src/data/navigation-messages.ts:252
msgid "app.navigation.finance-settlements" msgid "app.navigation.finance-settlements"
@@ -698,6 +818,11 @@ msgstr "Sky"
msgid "app.navigation.catalog-product-specifications" msgid "app.navigation.catalog-product-specifications"
msgstr "Specification templates" msgstr "Specification templates"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:76
msgid "blocks.chats.emptyConversations.description"
msgstr "Start a conversation to see it here."
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:80 #: src/data/navigation-messages.ts:80
msgid "app.navigation.inventory-transfers" msgid "app.navigation.inventory-transfers"
@@ -803,6 +928,11 @@ msgstr "Toggle theme"
msgid "app.navigation.after-sales-disputes" msgid "app.navigation.after-sales-disputes"
msgstr "Transaction disputes" msgstr "Transaction disputes"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:36
msgid "blocks.chats.actions.retry"
msgstr "Try again"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:36 #: ../../packages/blocks/src/blocks/notifications/messages.ts:36
msgid "blocks.notifications.retry" msgid "blocks.notifications.retry"
@@ -843,6 +973,11 @@ msgstr "Violet"
msgid "app.navigation.system-integration-webhooks" msgid "app.navigation.system-integration-webhooks"
msgstr "Webhook configuration" msgstr "Webhook configuration"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:96
msgid "blocks.chats.messagePlaceholder"
msgstr "Write a message…"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:179 #: ../../packages/blocks/src/blocks/appearance/messages.ts:179
msgid "blocks.appearance.color.yellow" msgid "blocks.appearance.color.yellow"
File diff suppressed because one or more lines are too long
+136 -1
View File
@@ -23,6 +23,21 @@ msgstr "账号密码"
msgid "blocks.appearance.active" msgid "blocks.appearance.active"
msgstr "使用中" msgstr "使用中"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:16
msgid "blocks.chats.actions.addEmoji"
msgstr ""
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:4
msgid "blocks.chats.actions.addFiles"
msgstr ""
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:8
msgid "blocks.chats.actions.addImage"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:276 #: src/data/navigation-messages.ts:276
msgid "app.navigation.system-members-admins" msgid "app.navigation.system-members-admins"
@@ -33,6 +48,11 @@ msgstr "管理员账号"
msgid "app.navigation.after-sales-center" msgid "app.navigation.after-sales-center"
msgstr "售后中心" msgstr "售后中心"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:12
msgid "blocks.chats.emojiPicker.allEmojis"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:91 #: ../../packages/blocks/src/blocks/appearance/messages.ts:91
msgid "blocks.appearance.color.amber" msgid "blocks.appearance.color.amber"
@@ -73,6 +93,11 @@ msgstr "待付款订单"
msgid "app.navigation.trade-orders-awaiting-shipment" msgid "app.navigation.trade-orders-awaiting-shipment"
msgstr "待发货订单" msgstr "待发货订单"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:20
msgid "blocks.chats.actions.backToConversations"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:16 #: ../../packages/blocks/src/blocks/appearance/messages.ts:16
msgid "blocks.appearance.baseColor" msgid "blocks.appearance.baseColor"
@@ -124,7 +149,7 @@ msgid "app.navigation.report-channels"
msgstr "渠道分析" msgstr "渠道分析"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:4 #: ../../packages/blocks/src/blocks/chats/messages.ts:112
msgid "blocks.chats.title" msgid "blocks.chats.title"
msgstr "聊天" msgstr "聊天"
@@ -133,6 +158,11 @@ msgstr "聊天"
msgid "blocks.notifications.error.description" msgid "blocks.notifications.error.description"
msgstr "请检查网络连接后重试。" msgstr "请检查网络连接后重试。"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:108
msgid "blocks.chats.selectConversation.description"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:29 #: ../../packages/ui/src/components/i18n.tsx:29
msgid "ui.common.close" msgid "ui.common.close"
@@ -158,6 +188,11 @@ msgstr "已完成订单"
msgid "app.navigation.content-assets" msgid "app.navigation.content-assets"
msgstr "内容素材" msgstr "内容素材"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:24
msgid "blocks.chats.actions.conversationDetails"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:192 #: src/data/navigation-messages.ts:192
msgid "app.navigation.marketing-coupons" msgid "app.navigation.marketing-coupons"
@@ -218,6 +253,11 @@ msgstr "深色主题"
msgid "app.navigation.data-and-finance" msgid "app.navigation.data-and-finance"
msgstr "数据与财务" msgstr "数据与财务"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:52
msgid "blocks.chats.messageStatus.delivered"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:296 #: src/data/navigation-messages.ts:296
msgid "app.navigation.system-notification-logs" msgid "app.navigation.system-notification-logs"
@@ -343,6 +383,11 @@ msgstr "浅色主题"
msgid "blocks.appearance.color.lime" msgid "blocks.appearance.color.lime"
msgstr "青柠色" msgstr "青柠色"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:28
msgid "blocks.chats.actions.loadEarlier"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:37 #: ../../packages/ui/src/components/i18n.tsx:37
msgid "ui.common.loading" msgid "ui.common.loading"
@@ -383,6 +428,11 @@ msgstr "媒体资源"
msgid "app.navigation.loyalty-center" msgid "app.navigation.loyalty-center"
msgstr "会员权益" msgstr "会员权益"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:84
msgid "blocks.chats.emptyMessages.description"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:127 #: ../../packages/blocks/src/blocks/appearance/messages.ts:127
msgid "blocks.appearance.color.mist" msgid "blocks.appearance.color.mist"
@@ -433,11 +483,31 @@ msgstr "下一页"
msgid "ui.carousel.nextSlide" msgid "ui.carousel.nextSlide"
msgstr "下一张" msgstr "下一张"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:80
msgid "blocks.chats.emptyConversations.title"
msgstr ""
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:92
msgid "blocks.chats.emptySearch"
msgstr ""
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:88
msgid "blocks.chats.emptyMessages.title"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:12 #: ../../packages/blocks/src/blocks/notifications/messages.ts:12
msgid "blocks.notifications.empty.title" msgid "blocks.notifications.empty.title"
msgstr "暂无通知" msgstr "暂无通知"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:56
msgid "blocks.chats.messageStatus.failed"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:292 #: src/data/navigation-messages.ts:292
msgid "app.navigation.system-notification-templates" msgid "app.navigation.system-notification-templates"
@@ -543,6 +613,11 @@ msgstr "粉色"
msgid "app.navigation.loyalty-points" msgid "app.navigation.loyalty-points"
msgstr "积分规则" msgstr "积分规则"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:44
msgid "blocks.chats.actions.sendOnEnter"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:65 #: ../../packages/ui/src/components/i18n.tsx:65
msgid "ui.pagination.previous" msgid "ui.pagination.previous"
@@ -598,11 +673,21 @@ msgstr "商品与库存"
msgid "blocks.appearance.color.purple" msgid "blocks.appearance.color.purple"
msgstr "紫色" msgstr "紫色"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:60
msgid "blocks.chats.messageStatus.read"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:12 #: src/data/navigation-messages.ts:12
msgid "app.navigation.overview-realtime" msgid "app.navigation.overview-realtime"
msgstr "实时监控" msgstr "实时监控"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:64
msgid "blocks.chats.emojiPicker.recentEmojis"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:256 #: src/data/navigation-messages.ts:256
msgid "app.navigation.finance-reconciliation" msgid "app.navigation.finance-reconciliation"
@@ -638,6 +723,11 @@ msgstr "刷新当前页面的数据"
msgid "app.navigation.after-sales-refunds" msgid "app.navigation.after-sales-refunds"
msgstr "退款审核" msgstr "退款审核"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:32
msgid "blocks.chats.actions.removeAttachment"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:228 #: src/data/navigation-messages.ts:228
msgid "app.navigation.report-center" msgid "app.navigation.report-center"
@@ -668,6 +758,36 @@ msgstr "玫红色"
msgid "app.navigation.report-sales" msgid "app.navigation.report-sales"
msgstr "销售分析" msgstr "销售分析"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:100
msgid "blocks.chats.search"
msgstr ""
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:104
msgid "blocks.chats.selectConversation"
msgstr ""
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:40
msgid "blocks.chats.actions.send"
msgstr ""
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:72
msgid "blocks.chats.messageStatus.sending"
msgstr ""
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:48
msgid "blocks.chats.actions.sending"
msgstr ""
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:68
msgid "blocks.chats.messageStatus.sent"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:252 #: src/data/navigation-messages.ts:252
msgid "app.navigation.finance-settlements" msgid "app.navigation.finance-settlements"
@@ -698,6 +818,11 @@ msgstr "天蓝色"
msgid "app.navigation.catalog-product-specifications" msgid "app.navigation.catalog-product-specifications"
msgstr "规格模板" msgstr "规格模板"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:76
msgid "blocks.chats.emptyConversations.description"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/data/navigation-messages.ts:80 #: src/data/navigation-messages.ts:80
msgid "app.navigation.inventory-transfers" msgid "app.navigation.inventory-transfers"
@@ -803,6 +928,11 @@ msgstr "切换主题"
msgid "app.navigation.after-sales-disputes" msgid "app.navigation.after-sales-disputes"
msgstr "交易纠纷" msgstr "交易纠纷"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:36
msgid "blocks.chats.actions.retry"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:36 #: ../../packages/blocks/src/blocks/notifications/messages.ts:36
msgid "blocks.notifications.retry" msgid "blocks.notifications.retry"
@@ -843,6 +973,11 @@ msgstr "紫罗兰色"
msgid "app.navigation.system-integration-webhooks" msgid "app.navigation.system-integration-webhooks"
msgstr "Webhook 配置" msgstr "Webhook 配置"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:96
msgid "blocks.chats.messagePlaceholder"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:179 #: ../../packages/blocks/src/blocks/appearance/messages.ts:179
msgid "blocks.appearance.color.yellow" msgid "blocks.appearance.color.yellow"
File diff suppressed because one or more lines are too long
+1
View File
@@ -1,3 +1,4 @@
// oxlint-disable-next-line import/no-unassigned-import -- Enforces server-only module usage.
import "@tanstack/react-start/server-only" import "@tanstack/react-start/server-only"
import { getCookie, setCookie } from "@tanstack/react-start/server" import { getCookie, setCookie } from "@tanstack/react-start/server"
@@ -3,27 +3,15 @@ import { describe, it } from "vitest"
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs" import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
import { appearanceMessages, themeColorMessages } from "../messages" import { appearanceMessages, themeColorMessages } from "../messages"
import { appearanceCatalogLocales } from "./catalogs" import { appearanceCatalogLocales } from "./catalogs"
import { messages as de } from "./de" import { messages as enUS } from "./en-US"
import { messages as en } from "./en"
import { messages as es } from "./es"
import { messages as fr } from "./fr"
import { messages as ja } from "./ja"
import { messages as ko } from "./ko"
import { messages as zhHans } from "./zh-Hans" import { messages as zhHans } from "./zh-Hans"
import { messages as zhHant } from "./zh-Hant"
describe("appearance locale catalogs", () => { describe("appearance locale catalogs", () => {
it("ship every appearance message in every built-in locale", () => { it("ship every appearance message in every built-in locale", () => {
expectCompleteCatalogs({ expectCompleteCatalogs({
catalogs: { catalogs: {
de, "en-US": enUS,
en,
es,
fr,
ja,
ko,
"zh-Hans": zhHans, "zh-Hans": zhHans,
"zh-Hant": zhHant,
}, },
locales: appearanceCatalogLocales, locales: appearanceCatalogLocales,
messageIds: [ messageIds: [
@@ -1,60 +0,0 @@
import type { AppearanceMessageCatalog } from "./catalogs"
export const locale = "de"
export const languageTag = "de-DE"
export const messages = {
"blocks.appearance.active": "Aktiv",
"blocks.appearance.baseColor": "Basisfarbe",
"blocks.appearance.color.amber": "Bernstein",
"blocks.appearance.color.blue": "Blau",
"blocks.appearance.color.cyan": "Cyan",
"blocks.appearance.color.emerald": "Smaragd",
"blocks.appearance.color.fuchsia": "Fuchsia",
"blocks.appearance.color.green": "Grün",
"blocks.appearance.color.indigo": "Indigo",
"blocks.appearance.color.lime": "Limette",
"blocks.appearance.color.mauve": "Malve",
"blocks.appearance.color.mist": "Nebel",
"blocks.appearance.color.neutral": "Neutral",
"blocks.appearance.color.olive": "Oliv",
"blocks.appearance.color.orange": "Orange",
"blocks.appearance.color.pink": "Rosa",
"blocks.appearance.color.purple": "Lila",
"blocks.appearance.color.red": "Rot",
"blocks.appearance.color.rose": "Rose",
"blocks.appearance.color.sky": "Himmelblau",
"blocks.appearance.color.stone": "Stein",
"blocks.appearance.color.taupe": "Taupe",
"blocks.appearance.color.teal": "Blaugrün",
"blocks.appearance.color.violet": "Violett",
"blocks.appearance.color.yellow": "Gelb",
"blocks.appearance.color.zinc": "Zink",
"blocks.appearance.command.description":
"Design- und Layout-Einstellungen öffnen",
"blocks.appearance.compact.description":
"Eine kompakte Breite für den Hauptinhalt verwenden",
"blocks.appearance.compact.title": "Kompaktes Layout",
"blocks.appearance.description": "Design- und Layout-Einstellungen",
"blocks.appearance.mode.dark": "Dunkel",
"blocks.appearance.mode.dark.description":
"Die Oberfläche verwendet immer das dunkle Design",
"blocks.appearance.mode.light": "Hell",
"blocks.appearance.mode.light.description":
"Die Oberfläche verwendet immer das helle Design",
"blocks.appearance.mode.system": "System",
"blocks.appearance.mode.system.description":
"Das Design der Oberfläche folgt der Systemeinstellung",
"blocks.appearance.mode.title": "Darstellungsmodus",
"blocks.appearance.primaryColor": "Primärfarbe",
"blocks.appearance.theme.dark": "Dunkles Design",
"blocks.appearance.theme.dark.description":
"Dieses Design wird im dunklen Systemmodus verwendet",
"blocks.appearance.theme.light": "Helles Design",
"blocks.appearance.theme.light.description":
"Dieses Design wird im hellen Systemmodus verwendet",
"blocks.appearance.title": "Darstellung",
"blocks.appearance.toggleTheme": "Design wechseln",
"blocks.appearance.toggleTheme.description":
"Zwischen hellem und dunklem Design wechseln",
} as const satisfies AppearanceMessageCatalog
@@ -3,7 +3,7 @@ import type { MessageDescriptor } from "@workspace/i18n"
import { appearanceMessages, themeColorMessages } from "../messages.ts" import { appearanceMessages, themeColorMessages } from "../messages.ts"
import type { AppearanceMessageCatalog } from "./catalogs" import type { AppearanceMessageCatalog } from "./catalogs"
export const locale = "en" export const locale = "en-US"
export const languageTag = "en-US" export const languageTag = "en-US"
function createSourceCatalog( function createSourceCatalog(
@@ -1,60 +0,0 @@
import type { AppearanceMessageCatalog } from "./catalogs"
export const locale = "es"
export const languageTag = "es-ES"
export const messages = {
"blocks.appearance.active": "Activo",
"blocks.appearance.baseColor": "Color base",
"blocks.appearance.color.amber": "Ámbar",
"blocks.appearance.color.blue": "Azul",
"blocks.appearance.color.cyan": "Cian",
"blocks.appearance.color.emerald": "Esmeralda",
"blocks.appearance.color.fuchsia": "Fucsia",
"blocks.appearance.color.green": "Verde",
"blocks.appearance.color.indigo": "Índigo",
"blocks.appearance.color.lime": "Lima",
"blocks.appearance.color.mauve": "Malva",
"blocks.appearance.color.mist": "Niebla",
"blocks.appearance.color.neutral": "Neutro",
"blocks.appearance.color.olive": "Oliva",
"blocks.appearance.color.orange": "Naranja",
"blocks.appearance.color.pink": "Rosa",
"blocks.appearance.color.purple": "Morado",
"blocks.appearance.color.red": "Rojo",
"blocks.appearance.color.rose": "Rosa intenso",
"blocks.appearance.color.sky": "Celeste",
"blocks.appearance.color.stone": "Piedra",
"blocks.appearance.color.taupe": "Topo",
"blocks.appearance.color.teal": "Verde azulado",
"blocks.appearance.color.violet": "Violeta",
"blocks.appearance.color.yellow": "Amarillo",
"blocks.appearance.color.zinc": "Zinc",
"blocks.appearance.command.description":
"Abrir las preferencias de tema y diseño",
"blocks.appearance.compact.description":
"Usar un ancho compacto para el contenido principal",
"blocks.appearance.compact.title": "Diseño compacto",
"blocks.appearance.description": "Preferencias de tema y diseño",
"blocks.appearance.mode.dark": "Oscuro",
"blocks.appearance.mode.dark.description":
"La interfaz siempre usará el tema oscuro",
"blocks.appearance.mode.light": "Claro",
"blocks.appearance.mode.light.description":
"La interfaz siempre usará el tema claro",
"blocks.appearance.mode.system": "Sistema",
"blocks.appearance.mode.system.description":
"El tema de la interfaz seguirá la apariencia del sistema",
"blocks.appearance.mode.title": "Modo de tema",
"blocks.appearance.primaryColor": "Color principal",
"blocks.appearance.theme.dark": "Tema oscuro",
"blocks.appearance.theme.dark.description":
"Este tema se usa cuando el sistema está en modo oscuro",
"blocks.appearance.theme.light": "Tema claro",
"blocks.appearance.theme.light.description":
"Este tema se usa cuando el sistema está en modo claro",
"blocks.appearance.title": "Apariencia",
"blocks.appearance.toggleTheme": "Cambiar tema",
"blocks.appearance.toggleTheme.description":
"Cambiar entre los temas claro y oscuro",
} as const satisfies AppearanceMessageCatalog
@@ -1,60 +0,0 @@
import type { AppearanceMessageCatalog } from "./catalogs"
export const locale = "fr"
export const languageTag = "fr-FR"
export const messages = {
"blocks.appearance.active": "Actif",
"blocks.appearance.baseColor": "Couleur de base",
"blocks.appearance.color.amber": "Ambre",
"blocks.appearance.color.blue": "Bleu",
"blocks.appearance.color.cyan": "Cyan",
"blocks.appearance.color.emerald": "Émeraude",
"blocks.appearance.color.fuchsia": "Fuchsia",
"blocks.appearance.color.green": "Vert",
"blocks.appearance.color.indigo": "Indigo",
"blocks.appearance.color.lime": "Citron vert",
"blocks.appearance.color.mauve": "Mauve",
"blocks.appearance.color.mist": "Brume",
"blocks.appearance.color.neutral": "Neutre",
"blocks.appearance.color.olive": "Olive",
"blocks.appearance.color.orange": "Orange",
"blocks.appearance.color.pink": "Rose",
"blocks.appearance.color.purple": "Pourpre",
"blocks.appearance.color.red": "Rouge",
"blocks.appearance.color.rose": "Rose soutenu",
"blocks.appearance.color.sky": "Bleu ciel",
"blocks.appearance.color.stone": "Pierre",
"blocks.appearance.color.taupe": "Taupe",
"blocks.appearance.color.teal": "Sarcelle",
"blocks.appearance.color.violet": "Violet",
"blocks.appearance.color.yellow": "Jaune",
"blocks.appearance.color.zinc": "Zinc",
"blocks.appearance.command.description":
"Ouvrir les préférences de thème et de mise en page",
"blocks.appearance.compact.description":
"Utiliser une largeur compacte pour le contenu principal",
"blocks.appearance.compact.title": "Mise en page compacte",
"blocks.appearance.description": "Préférences de thème et de mise en page",
"blocks.appearance.mode.dark": "Sombre",
"blocks.appearance.mode.dark.description":
"Linterface utilisera toujours le thème sombre",
"blocks.appearance.mode.light": "Clair",
"blocks.appearance.mode.light.description":
"Linterface utilisera toujours le thème clair",
"blocks.appearance.mode.system": "Système",
"blocks.appearance.mode.system.description":
"Le thème de linterface suivra lapparence du système",
"blocks.appearance.mode.title": "Mode du thème",
"blocks.appearance.primaryColor": "Couleur principale",
"blocks.appearance.theme.dark": "Thème sombre",
"blocks.appearance.theme.dark.description":
"Ce thème est utilisé lorsque le système est en mode sombre",
"blocks.appearance.theme.light": "Thème clair",
"blocks.appearance.theme.light.description":
"Ce thème est utilisé lorsque le système est en mode clair",
"blocks.appearance.title": "Apparence",
"blocks.appearance.toggleTheme": "Changer de thème",
"blocks.appearance.toggleTheme.description":
"Basculer entre les thèmes clair et sombre",
} as const satisfies AppearanceMessageCatalog
@@ -1,57 +0,0 @@
import type { AppearanceMessageCatalog } from "./catalogs"
export const locale = "ja"
export const languageTag = "ja-JP"
export const messages = {
"blocks.appearance.active": "使用中",
"blocks.appearance.baseColor": "ベースカラー",
"blocks.appearance.color.amber": "アンバー",
"blocks.appearance.color.blue": "ブルー",
"blocks.appearance.color.cyan": "シアン",
"blocks.appearance.color.emerald": "エメラルド",
"blocks.appearance.color.fuchsia": "フクシア",
"blocks.appearance.color.green": "グリーン",
"blocks.appearance.color.indigo": "インディゴ",
"blocks.appearance.color.lime": "ライム",
"blocks.appearance.color.mauve": "モーブ",
"blocks.appearance.color.mist": "ミスト",
"blocks.appearance.color.neutral": "ニュートラル",
"blocks.appearance.color.olive": "オリーブ",
"blocks.appearance.color.orange": "オレンジ",
"blocks.appearance.color.pink": "ピンク",
"blocks.appearance.color.purple": "パープル",
"blocks.appearance.color.red": "レッド",
"blocks.appearance.color.rose": "ローズ",
"blocks.appearance.color.sky": "スカイ",
"blocks.appearance.color.stone": "ストーン",
"blocks.appearance.color.taupe": "トープ",
"blocks.appearance.color.teal": "ティール",
"blocks.appearance.color.violet": "バイオレット",
"blocks.appearance.color.yellow": "イエロー",
"blocks.appearance.color.zinc": "ジンク",
"blocks.appearance.command.description": "テーマとレイアウト設定を開く",
"blocks.appearance.compact.description":
"メインページのコンテンツをコンパクトな幅で表示します",
"blocks.appearance.compact.title": "コンパクトレイアウト",
"blocks.appearance.description": "テーマとレイアウトの設定",
"blocks.appearance.mode.dark": "ダーク",
"blocks.appearance.mode.dark.description": "常にダークテーマを使用します",
"blocks.appearance.mode.light": "ライト",
"blocks.appearance.mode.light.description": "常にライトテーマを使用します",
"blocks.appearance.mode.system": "システム",
"blocks.appearance.mode.system.description":
"インターフェースのテーマをシステムの外観に合わせます",
"blocks.appearance.mode.title": "テーマモード",
"blocks.appearance.primaryColor": "プライマリカラー",
"blocks.appearance.theme.dark": "ダークテーマ",
"blocks.appearance.theme.dark.description":
"システムがダークモードのときに使用するテーマです",
"blocks.appearance.theme.light": "ライトテーマ",
"blocks.appearance.theme.light.description":
"システムがライトモードのときに使用するテーマです",
"blocks.appearance.title": "外観",
"blocks.appearance.toggleTheme": "テーマを切り替える",
"blocks.appearance.toggleTheme.description":
"ライトテーマとダークテーマを切り替えます",
} as const satisfies AppearanceMessageCatalog
@@ -1,59 +0,0 @@
import type { AppearanceMessageCatalog } from "./catalogs"
export const locale = "ko"
export const languageTag = "ko-KR"
export const messages = {
"blocks.appearance.active": "사용 중",
"blocks.appearance.baseColor": "기본 색상",
"blocks.appearance.color.amber": "호박색",
"blocks.appearance.color.blue": "파란색",
"blocks.appearance.color.cyan": "청록색",
"blocks.appearance.color.emerald": "에메랄드",
"blocks.appearance.color.fuchsia": "자홍색",
"blocks.appearance.color.green": "초록색",
"blocks.appearance.color.indigo": "남색",
"blocks.appearance.color.lime": "라임",
"blocks.appearance.color.mauve": "연보라",
"blocks.appearance.color.mist": "안개색",
"blocks.appearance.color.neutral": "중성색",
"blocks.appearance.color.olive": "올리브",
"blocks.appearance.color.orange": "주황색",
"blocks.appearance.color.pink": "분홍색",
"blocks.appearance.color.purple": "보라색",
"blocks.appearance.color.red": "빨간색",
"blocks.appearance.color.rose": "장미색",
"blocks.appearance.color.sky": "하늘색",
"blocks.appearance.color.stone": "돌색",
"blocks.appearance.color.taupe": "회갈색",
"blocks.appearance.color.teal": "틸",
"blocks.appearance.color.violet": "제비꽃색",
"blocks.appearance.color.yellow": "노란색",
"blocks.appearance.color.zinc": "아연색",
"blocks.appearance.command.description": "테마 및 레이아웃 환경설정 열기",
"blocks.appearance.compact.description":
"기본 페이지 콘텐츠에 좁은 너비를 사용합니다",
"blocks.appearance.compact.title": "컴팩트 레이아웃",
"blocks.appearance.description": "테마 및 레이아웃 환경설정",
"blocks.appearance.mode.dark": "어둡게",
"blocks.appearance.mode.dark.description":
"인터페이스에서 항상 어두운 테마를 사용합니다",
"blocks.appearance.mode.light": "밝게",
"blocks.appearance.mode.light.description":
"인터페이스에서 항상 밝은 테마를 사용합니다",
"blocks.appearance.mode.system": "시스템",
"blocks.appearance.mode.system.description":
"인터페이스 테마가 시스템 화면 모드를 따릅니다",
"blocks.appearance.mode.title": "테마 모드",
"blocks.appearance.primaryColor": "주요 색상",
"blocks.appearance.theme.dark": "어두운 테마",
"blocks.appearance.theme.dark.description":
"시스템이 어두운 모드일 때 사용하는 테마입니다",
"blocks.appearance.theme.light": "밝은 테마",
"blocks.appearance.theme.light.description":
"시스템이 밝은 모드일 때 사용하는 테마입니다",
"blocks.appearance.title": "화면 모양",
"blocks.appearance.toggleTheme": "테마 전환",
"blocks.appearance.toggleTheme.description":
"밝은 테마와 어두운 테마 사이를 전환합니다",
} as const satisfies AppearanceMessageCatalog
@@ -1,52 +0,0 @@
import type { AppearanceMessageCatalog } from "./catalogs"
export const locale = "zh-Hant"
export const languageTag = "zh-TW"
export const messages = {
"blocks.appearance.active": "使用中",
"blocks.appearance.baseColor": "基礎顏色",
"blocks.appearance.color.amber": "琥珀色",
"blocks.appearance.color.blue": "藍色",
"blocks.appearance.color.cyan": "青色",
"blocks.appearance.color.emerald": "翠綠色",
"blocks.appearance.color.fuchsia": "紫紅色",
"blocks.appearance.color.green": "綠色",
"blocks.appearance.color.indigo": "靛藍色",
"blocks.appearance.color.lime": "萊姆色",
"blocks.appearance.color.mauve": "淡紫色",
"blocks.appearance.color.mist": "霧灰色",
"blocks.appearance.color.neutral": "中性灰",
"blocks.appearance.color.olive": "橄欖色",
"blocks.appearance.color.orange": "橙色",
"blocks.appearance.color.pink": "粉紅色",
"blocks.appearance.color.purple": "紫色",
"blocks.appearance.color.red": "紅色",
"blocks.appearance.color.rose": "玫瑰色",
"blocks.appearance.color.sky": "天藍色",
"blocks.appearance.color.stone": "石灰色",
"blocks.appearance.color.taupe": "灰褐色",
"blocks.appearance.color.teal": "藍綠色",
"blocks.appearance.color.violet": "紫羅蘭色",
"blocks.appearance.color.yellow": "黃色",
"blocks.appearance.color.zinc": "鋅灰色",
"blocks.appearance.command.description": "開啟主題與版面配置偏好",
"blocks.appearance.compact.description": "使用較緊湊的主頁內容寬度",
"blocks.appearance.compact.title": "緊湊版面配置",
"blocks.appearance.description": "主題與版面配置偏好",
"blocks.appearance.mode.dark": "深色",
"blocks.appearance.mode.dark.description": "介面將一律使用深色主題",
"blocks.appearance.mode.light": "淺色",
"blocks.appearance.mode.light.description": "介面將一律使用淺色主題",
"blocks.appearance.mode.system": "系統",
"blocks.appearance.mode.system.description": "介面主題將跟隨系統外觀設定",
"blocks.appearance.mode.title": "主題模式",
"blocks.appearance.primaryColor": "主要顏色",
"blocks.appearance.theme.dark": "深色主題",
"blocks.appearance.theme.dark.description": "系統使用深色模式時套用此主題",
"blocks.appearance.theme.light": "淺色主題",
"blocks.appearance.theme.light.description": "系統使用淺色模式時套用此主題",
"blocks.appearance.title": "介面外觀",
"blocks.appearance.toggleTheme": "切換主題",
"blocks.appearance.toggleTheme.description": "在淺色與深色主題之間切換",
} as const satisfies AppearanceMessageCatalog
@@ -5,7 +5,7 @@ import { I18nProvider } from "@workspace/i18n"
import { afterEach, describe, expect, it, vi } from "vitest" import { afterEach, describe, expect, it, vi } from "vitest"
import { ChatMessageList } from "./chat-message-list" import { ChatMessageList } from "./chat-message-list"
import { messages as englishMessages } from "./locales/en" import { messages as englishMessages } from "./locales/en-US"
import type { ChatConversation } from "./types" import type { ChatConversation } from "./types"
const conversation: ChatConversation = { const conversation: ChatConversation = {
@@ -57,7 +57,7 @@ describe("ChatMessageList", () => {
) )
render( render(
<I18nProvider locale="en" catalogs={{ en: englishMessages }}> <I18nProvider locale="en-US" catalogs={{ "en-US": englishMessages }}>
<ChatMessageList <ChatMessageList
conversation={conversation} conversation={conversation}
resolveMediaUrl={resolveMediaUrl} resolveMediaUrl={resolveMediaUrl}
@@ -11,7 +11,7 @@ import { I18nProvider } from "@workspace/i18n"
import { afterEach, describe, expect, it, vi } from "vitest" import { afterEach, describe, expect, it, vi } from "vitest"
import { ChatWorkspace } from "./chat-workspace" import { ChatWorkspace } from "./chat-workspace"
import { messages as englishMessages } from "./locales/en" import { messages as englishMessages } from "./locales/en-US"
import type { ChatConversation, ChatSendHandler } from "./types" import type { ChatConversation, ChatSendHandler } from "./types"
vi.stubGlobal( vi.stubGlobal(
@@ -72,7 +72,7 @@ function renderWorkspace({
onSend?: ChatSendHandler onSend?: ChatSendHandler
} = {}) { } = {}) {
return render( return render(
<I18nProvider locale="en" catalogs={{ en: englishMessages }}> <I18nProvider locale="en-US" catalogs={{ "en-US": englishMessages }}>
<ChatWorkspace <ChatWorkspace
conversations={conversations} conversations={conversations}
draft={draft} draft={draft}
@@ -210,7 +210,7 @@ describe("ChatWorkspace", () => {
expect(editor.getAttribute("contenteditable")).toBe("true") expect(editor.getAttribute("contenteditable")).toBe("true")
rerender( rerender(
<I18nProvider locale="en" catalogs={{ en: englishMessages }}> <I18nProvider locale="en-US" catalogs={{ "en-US": englishMessages }}>
<ChatWorkspace conversations={conversations} draft="Lexical draft" /> <ChatWorkspace conversations={conversations} draft="Lexical draft" />
</I18nProvider> </I18nProvider>
) )
@@ -3,27 +3,15 @@ import { describe, it } from "vitest"
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs" import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
import { chatMessages } from "../messages" import { chatMessages } from "../messages"
import { chatCatalogLocales } from "./catalogs" import { chatCatalogLocales } from "./catalogs"
import { messages as de } from "./de" import { messages as enUS } from "./en-US"
import { messages as en } from "./en"
import { messages as es } from "./es"
import { messages as fr } from "./fr"
import { messages as ja } from "./ja"
import { messages as ko } from "./ko"
import { messages as zhHans } from "./zh-Hans" import { messages as zhHans } from "./zh-Hans"
import { messages as zhHant } from "./zh-Hant"
describe("chat locale catalogs", () => { describe("chat locale catalogs", () => {
it("ship every chat message in every built-in locale", () => { it("ship every chat message in every built-in locale", () => {
expectCompleteCatalogs({ expectCompleteCatalogs({
catalogs: { catalogs: {
de, "en-US": enUS,
en,
es,
fr,
ja,
ko,
"zh-Hans": zhHans, "zh-Hans": zhHans,
"zh-Hant": zhHant,
}, },
locales: chatCatalogLocales, locales: chatCatalogLocales,
messageIds: Object.values(chatMessages).map( messageIds: Object.values(chatMessages).map(
@@ -1,37 +0,0 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "de"
export const languageTag = "de-DE"
export const messages = {
"blocks.chats.actions.addFiles": "Dateien hinzufügen",
"blocks.chats.actions.addEmoji": "Emoji hinzufügen",
"blocks.chats.actions.addImage": "Bild hinzufügen",
"blocks.chats.actions.backToConversations": "Zurück zu Unterhaltungen",
"blocks.chats.actions.conversationDetails": "Unterhaltungsdetails",
"blocks.chats.actions.loadEarlier": "Frühere Nachrichten laden",
"blocks.chats.actions.removeAttachment": "{name} entfernen",
"blocks.chats.actions.retry": "Erneut versuchen",
"blocks.chats.actions.send": "Senden",
"blocks.chats.actions.sendOnEnter": "Mit Enter senden",
"blocks.chats.actions.sending": "Wird gesendet…",
"blocks.chats.emptyConversations.description":
"Starte eine Unterhaltung, damit sie hier erscheint.",
"blocks.chats.emptyConversations.title": "Keine Unterhaltungen",
"blocks.chats.emptyMessages.description":
"Nachrichten dieser Unterhaltung werden hier angezeigt.",
"blocks.chats.emptyMessages.title": "Noch keine Nachrichten",
"blocks.chats.emptySearch": "Keine Unterhaltungen entsprechen deiner Suche.",
"blocks.chats.emojiPicker.allEmojis": "Alle Emojis",
"blocks.chats.emojiPicker.recentEmojis": "Kürzlich verwendet",
"blocks.chats.messagePlaceholder": "Nachricht schreiben…",
"blocks.chats.messageStatus.delivered": "Zugestellt",
"blocks.chats.messageStatus.failed": "Nicht gesendet",
"blocks.chats.messageStatus.read": "Gelesen",
"blocks.chats.messageStatus.sending": "Wird gesendet",
"blocks.chats.messageStatus.sent": "Gesendet",
"blocks.chats.search": "Unterhaltungen suchen",
"blocks.chats.selectConversation": "Unterhaltung {title} auswählen",
"blocks.chats.selectConversation.description":
"Wähle eine Unterhaltung aus der Liste, um ihre Nachrichten zu lesen.",
"blocks.chats.title": "Chats",
} as const satisfies ChatMessageCatalog
@@ -1,6 +1,6 @@
import type { ChatMessageCatalog } from "./catalogs" import type { ChatMessageCatalog } from "./catalogs"
export const locale = "en" export const locale = "en-US"
export const languageTag = "en-US" export const languageTag = "en-US"
export const messages = { export const messages = {
"blocks.chats.actions.addFiles": "Add files", "blocks.chats.actions.addFiles": "Add files",
@@ -1,38 +0,0 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "es"
export const languageTag = "es-ES"
export const messages = {
"blocks.chats.actions.addFiles": "Añadir archivos",
"blocks.chats.actions.addEmoji": "Añadir emoji",
"blocks.chats.actions.addImage": "Añadir imagen",
"blocks.chats.actions.backToConversations": "Volver a conversaciones",
"blocks.chats.actions.conversationDetails": "Detalles de la conversación",
"blocks.chats.actions.loadEarlier": "Cargar mensajes anteriores",
"blocks.chats.actions.removeAttachment": "Quitar {name}",
"blocks.chats.actions.retry": "Intentar de nuevo",
"blocks.chats.actions.send": "Enviar",
"blocks.chats.actions.sendOnEnter": "Enviar con Intro",
"blocks.chats.actions.sending": "Enviando…",
"blocks.chats.emptyConversations.description":
"Inicia una conversación para verla aquí.",
"blocks.chats.emptyConversations.title": "No hay conversaciones",
"blocks.chats.emptyMessages.description":
"Los mensajes de esta conversación aparecerán aquí.",
"blocks.chats.emptyMessages.title": "Aún no hay mensajes",
"blocks.chats.emptySearch":
"No hay conversaciones que coincidan con tu búsqueda.",
"blocks.chats.emojiPicker.allEmojis": "Todos los emojis",
"blocks.chats.emojiPicker.recentEmojis": "Usados recientemente",
"blocks.chats.messagePlaceholder": "Escribe un mensaje…",
"blocks.chats.messageStatus.delivered": "Entregado",
"blocks.chats.messageStatus.failed": "No enviado",
"blocks.chats.messageStatus.read": "Leído",
"blocks.chats.messageStatus.sending": "Enviando",
"blocks.chats.messageStatus.sent": "Enviado",
"blocks.chats.search": "Buscar conversaciones",
"blocks.chats.selectConversation": "Seleccionar conversación {title}",
"blocks.chats.selectConversation.description":
"Elige una conversación de la lista para leer sus mensajes.",
"blocks.chats.title": "Chats",
} as const satisfies ChatMessageCatalog
@@ -1,38 +0,0 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "fr"
export const languageTag = "fr-FR"
export const messages = {
"blocks.chats.actions.addFiles": "Ajouter des fichiers",
"blocks.chats.actions.addEmoji": "Ajouter un emoji",
"blocks.chats.actions.addImage": "Ajouter une image",
"blocks.chats.actions.backToConversations": "Retour aux discussions",
"blocks.chats.actions.conversationDetails": "Détails de la discussion",
"blocks.chats.actions.loadEarlier": "Charger les messages précédents",
"blocks.chats.actions.removeAttachment": "Retirer {name}",
"blocks.chats.actions.retry": "Réessayer",
"blocks.chats.actions.send": "Envoyer",
"blocks.chats.actions.sendOnEnter": "Envoyer avec Entrée",
"blocks.chats.actions.sending": "Envoi…",
"blocks.chats.emptyConversations.description":
"Démarrez une discussion pour la voir ici.",
"blocks.chats.emptyConversations.title": "Aucune discussion",
"blocks.chats.emptyMessages.description":
"Les messages de cette discussion apparaîtront ici.",
"blocks.chats.emptyMessages.title": "Pas encore de messages",
"blocks.chats.emptySearch":
"Aucune discussion ne correspond à votre recherche.",
"blocks.chats.emojiPicker.allEmojis": "Tous les emojis",
"blocks.chats.emojiPicker.recentEmojis": "Récemment utilisés",
"blocks.chats.messagePlaceholder": "Écrivez un message…",
"blocks.chats.messageStatus.delivered": "Distribué",
"blocks.chats.messageStatus.failed": "Non envoyé",
"blocks.chats.messageStatus.read": "Lu",
"blocks.chats.messageStatus.sending": "Envoi",
"blocks.chats.messageStatus.sent": "Envoyé",
"blocks.chats.search": "Rechercher des discussions",
"blocks.chats.selectConversation": "Sélectionner la discussion {title}",
"blocks.chats.selectConversation.description":
"Choisissez une discussion dans la liste pour lire ses messages.",
"blocks.chats.title": "Discussions",
} as const satisfies ChatMessageCatalog
@@ -1,37 +0,0 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "ja"
export const languageTag = "ja-JP"
export const messages = {
"blocks.chats.actions.addFiles": "ファイルを追加",
"blocks.chats.actions.addEmoji": "絵文字を追加",
"blocks.chats.actions.addImage": "画像を追加",
"blocks.chats.actions.backToConversations": "会話一覧に戻る",
"blocks.chats.actions.conversationDetails": "会話の詳細",
"blocks.chats.actions.loadEarlier": "以前のメッセージを読み込む",
"blocks.chats.actions.removeAttachment": "{name} を削除",
"blocks.chats.actions.retry": "再試行",
"blocks.chats.actions.send": "送信",
"blocks.chats.actions.sendOnEnter": "Enter キーで送信",
"blocks.chats.actions.sending": "送信中…",
"blocks.chats.emptyConversations.description":
"会話を始めると、ここに表示されます。",
"blocks.chats.emptyConversations.title": "会話はありません",
"blocks.chats.emptyMessages.description":
"この会話のメッセージはここに表示されます。",
"blocks.chats.emptyMessages.title": "メッセージはまだありません",
"blocks.chats.emptySearch": "検索に一致する会話はありません。",
"blocks.chats.emojiPicker.allEmojis": "すべての絵文字",
"blocks.chats.emojiPicker.recentEmojis": "最近使用した絵文字",
"blocks.chats.messagePlaceholder": "メッセージを入力…",
"blocks.chats.messageStatus.delivered": "配信済み",
"blocks.chats.messageStatus.failed": "未送信",
"blocks.chats.messageStatus.read": "既読",
"blocks.chats.messageStatus.sending": "送信中",
"blocks.chats.messageStatus.sent": "送信済み",
"blocks.chats.search": "会話を検索",
"blocks.chats.selectConversation": "会話 {title} を選択",
"blocks.chats.selectConversation.description":
"一覧から会話を選択してメッセージを読みます。",
"blocks.chats.title": "チャット",
} as const satisfies ChatMessageCatalog
@@ -1,37 +0,0 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "ko"
export const languageTag = "ko-KR"
export const messages = {
"blocks.chats.actions.addFiles": "파일 추가",
"blocks.chats.actions.addEmoji": "이모지 추가",
"blocks.chats.actions.addImage": "이미지 추가",
"blocks.chats.actions.backToConversations": "대화 목록으로 돌아가기",
"blocks.chats.actions.conversationDetails": "대화 세부 정보",
"blocks.chats.actions.loadEarlier": "이전 메시지 불러오기",
"blocks.chats.actions.removeAttachment": "{name} 제거",
"blocks.chats.actions.retry": "다시 시도",
"blocks.chats.actions.send": "보내기",
"blocks.chats.actions.sendOnEnter": "Enter 키로 전송",
"blocks.chats.actions.sending": "보내는 중…",
"blocks.chats.emptyConversations.description":
"대화를 시작하면 여기에 표시됩니다.",
"blocks.chats.emptyConversations.title": "대화가 없습니다",
"blocks.chats.emptyMessages.description":
"이 대화의 메시지가 여기에 표시됩니다.",
"blocks.chats.emptyMessages.title": "아직 메시지가 없습니다",
"blocks.chats.emptySearch": "검색과 일치하는 대화가 없습니다.",
"blocks.chats.emojiPicker.allEmojis": "모든 이모지",
"blocks.chats.emojiPicker.recentEmojis": "최근 사용",
"blocks.chats.messagePlaceholder": "메시지 작성…",
"blocks.chats.messageStatus.delivered": "전달됨",
"blocks.chats.messageStatus.failed": "전송되지 않음",
"blocks.chats.messageStatus.read": "읽음",
"blocks.chats.messageStatus.sending": "보내는 중",
"blocks.chats.messageStatus.sent": "보냄",
"blocks.chats.search": "대화 검색",
"blocks.chats.selectConversation": "대화 {title} 선택",
"blocks.chats.selectConversation.description":
"목록에서 대화를 선택하여 메시지를 읽으세요.",
"blocks.chats.title": "채팅",
} as const satisfies ChatMessageCatalog
@@ -1,34 +0,0 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "zh-Hant"
export const languageTag = "zh-TW"
export const messages = {
"blocks.chats.actions.addFiles": "新增檔案",
"blocks.chats.actions.addEmoji": "新增表情",
"blocks.chats.actions.addImage": "新增圖片",
"blocks.chats.actions.backToConversations": "返回對話列表",
"blocks.chats.actions.conversationDetails": "對話詳細資料",
"blocks.chats.actions.loadEarlier": "載入較早訊息",
"blocks.chats.actions.removeAttachment": "移除 {name}",
"blocks.chats.actions.retry": "再試一次",
"blocks.chats.actions.send": "傳送",
"blocks.chats.actions.sendOnEnter": "按 Enter 傳送",
"blocks.chats.actions.sending": "傳送中…",
"blocks.chats.emptyConversations.description": "開始對話後會顯示在這裡。",
"blocks.chats.emptyConversations.title": "沒有對話",
"blocks.chats.emptyMessages.description": "此對話的訊息會顯示在這裡。",
"blocks.chats.emptyMessages.title": "尚無訊息",
"blocks.chats.emptySearch": "沒有符合搜尋條件的對話。",
"blocks.chats.emojiPicker.allEmojis": "所有表情",
"blocks.chats.emojiPicker.recentEmojis": "最近使用",
"blocks.chats.messagePlaceholder": "輸入訊息…",
"blocks.chats.messageStatus.delivered": "已送達",
"blocks.chats.messageStatus.failed": "未傳送",
"blocks.chats.messageStatus.read": "已讀",
"blocks.chats.messageStatus.sending": "傳送中",
"blocks.chats.messageStatus.sent": "已傳送",
"blocks.chats.search": "搜尋對話",
"blocks.chats.selectConversation": "選取對話 {title}",
"blocks.chats.selectConversation.description": "從列表中選取對話以閱讀訊息。",
"blocks.chats.title": "聊天",
} as const satisfies ChatMessageCatalog
@@ -3,27 +3,15 @@ import { describe, it } from "vitest"
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs" import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
import { layoutMessages } from "../messages" import { layoutMessages } from "../messages"
import { layoutCatalogLocales } from "./catalogs" import { layoutCatalogLocales } from "./catalogs"
import { messages as de } from "./de" import { messages as enUS } from "./en-US"
import { messages as en } from "./en"
import { messages as es } from "./es"
import { messages as fr } from "./fr"
import { messages as ja } from "./ja"
import { messages as ko } from "./ko"
import { messages as zhHans } from "./zh-Hans" import { messages as zhHans } from "./zh-Hans"
import { messages as zhHant } from "./zh-Hant"
describe("layout locale catalogs", () => { describe("layout locale catalogs", () => {
it("ship every layout message in every built-in locale", () => { it("ship every layout message in every built-in locale", () => {
expectCompleteCatalogs({ expectCompleteCatalogs({
catalogs: { catalogs: {
de, "en-US": enUS,
en,
es,
fr,
ja,
ko,
"zh-Hans": zhHans, "zh-Hans": zhHans,
"zh-Hant": zhHant,
}, },
locales: layoutCatalogLocales, locales: layoutCatalogLocales,
messageIds: Object.values(layoutMessages).map( messageIds: Object.values(layoutMessages).map(
@@ -1,15 +0,0 @@
import type { LayoutMessageCatalog } from "./catalogs"
export const locale = "de"
export const languageTag = "de-DE"
export const messages = {
"blocks.breadcrumb.menuLabel": "Breadcrumb-Menü öffnen",
"blocks.refresh.description": "Daten der aktuellen Seite aktualisieren",
"blocks.refresh.label": "Aktuelle Seitendaten aktualisieren",
"blocks.userMenu.accountPassword": "Kontopasswort",
"blocks.userMenu.appearance": "Darstellung",
"blocks.userMenu.keybindings": "Tastenkürzel",
"blocks.userMenu.logout": "Abmelden",
"blocks.userMenu.open": "Benutzermenü öffnen",
"blocks.userMenu.title": "Benutzermenü",
} as const satisfies LayoutMessageCatalog
@@ -1,6 +1,6 @@
import type { LayoutMessageCatalog } from "./catalogs" import type { LayoutMessageCatalog } from "./catalogs"
export const locale = "en" export const locale = "en-US"
export const languageTag = "en-US" export const languageTag = "en-US"
export const messages = { export const messages = {
"blocks.breadcrumb.menuLabel": "Open breadcrumb menu", "blocks.breadcrumb.menuLabel": "Open breadcrumb menu",
@@ -1,15 +0,0 @@
import type { LayoutMessageCatalog } from "./catalogs"
export const locale = "es"
export const languageTag = "es-ES"
export const messages = {
"blocks.breadcrumb.menuLabel": "Abrir el menú de ruta de navegación",
"blocks.refresh.description": "Actualizar los datos de la página actual",
"blocks.refresh.label": "Actualizar datos de la página actual",
"blocks.userMenu.accountPassword": "Contraseña de la cuenta",
"blocks.userMenu.appearance": "Apariencia",
"blocks.userMenu.keybindings": "Atajos de teclado",
"blocks.userMenu.logout": "Cerrar sesión",
"blocks.userMenu.open": "Abrir menú de usuario",
"blocks.userMenu.title": "Menú de usuario",
} as const satisfies LayoutMessageCatalog
@@ -1,15 +0,0 @@
import type { LayoutMessageCatalog } from "./catalogs"
export const locale = "fr"
export const languageTag = "fr-FR"
export const messages = {
"blocks.breadcrumb.menuLabel": "Ouvrir le menu du fil dAriane",
"blocks.refresh.description": "Actualiser les données de la page actuelle",
"blocks.refresh.label": "Actualiser les données de la page",
"blocks.userMenu.accountPassword": "Mot de passe du compte",
"blocks.userMenu.appearance": "Apparence",
"blocks.userMenu.keybindings": "Raccourcis clavier",
"blocks.userMenu.logout": "Se déconnecter",
"blocks.userMenu.open": "Ouvrir le menu utilisateur",
"blocks.userMenu.title": "Menu utilisateur",
} as const satisfies LayoutMessageCatalog
@@ -1,15 +0,0 @@
import type { LayoutMessageCatalog } from "./catalogs"
export const locale = "ja"
export const languageTag = "ja-JP"
export const messages = {
"blocks.breadcrumb.menuLabel": "パンくずメニューを開く",
"blocks.refresh.description": "現在のページのデータを更新します",
"blocks.refresh.label": "現在のページを更新",
"blocks.userMenu.accountPassword": "アカウントのパスワード",
"blocks.userMenu.appearance": "外観",
"blocks.userMenu.keybindings": "キーバインド",
"blocks.userMenu.logout": "ログアウト",
"blocks.userMenu.open": "ユーザーメニューを開く",
"blocks.userMenu.title": "ユーザーメニュー",
} as const satisfies LayoutMessageCatalog
@@ -1,15 +0,0 @@
import type { LayoutMessageCatalog } from "./catalogs"
export const locale = "ko"
export const languageTag = "ko-KR"
export const messages = {
"blocks.breadcrumb.menuLabel": "이동 경로 메뉴 열기",
"blocks.refresh.description": "현재 페이지 데이터를 새로 고칩니다",
"blocks.refresh.label": "현재 페이지 데이터 새로 고침",
"blocks.userMenu.accountPassword": "계정 비밀번호",
"blocks.userMenu.appearance": "화면 모양",
"blocks.userMenu.keybindings": "키 바인딩",
"blocks.userMenu.logout": "로그아웃",
"blocks.userMenu.open": "사용자 메뉴 열기",
"blocks.userMenu.title": "사용자 메뉴",
} as const satisfies LayoutMessageCatalog
@@ -1,15 +0,0 @@
import type { LayoutMessageCatalog } from "./catalogs"
export const locale = "zh-Hant"
export const languageTag = "zh-TW"
export const messages = {
"blocks.breadcrumb.menuLabel": "開啟麵包屑選單",
"blocks.refresh.description": "重新整理目前頁面的資料",
"blocks.refresh.label": "重新整理目前頁面資料",
"blocks.userMenu.accountPassword": "帳號密碼",
"blocks.userMenu.appearance": "介面外觀",
"blocks.userMenu.keybindings": "按鍵綁定",
"blocks.userMenu.logout": "登出",
"blocks.userMenu.open": "開啟使用者選單",
"blocks.userMenu.title": "使用者選單",
} as const satisfies LayoutMessageCatalog
@@ -0,0 +1,22 @@
import { describe, it } from "vitest"
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
import { mediaMessages } from "../messages"
import { mediaCatalogLocales } from "./catalogs"
import { messages as enUS } from "./en-US"
import { messages as zhHans } from "./zh-Hans"
describe("media locale catalogs", () => {
it("ships every media message in every built-in locale", () => {
expectCompleteCatalogs({
catalogs: {
"en-US": enUS,
"zh-Hans": zhHans,
},
locales: mediaCatalogLocales,
messageIds: Object.values(mediaMessages).map(
(descriptor) => descriptor.id
),
})
})
})
@@ -1,4 +1,10 @@
import type { mediaMessages } from "../messages" import type { mediaMessages } from "../messages"
import type { BlockMessageCatalog } from "../../../i18n/catalogs" import {
blockCatalogLocales,
type BlockCatalogLocale,
type BlockMessageCatalog,
} from "../../../i18n/catalogs"
export { blockCatalogLocales as mediaCatalogLocales }
export type MediaCatalogLocale = BlockCatalogLocale
export type MediaMessageCatalog = BlockMessageCatalog<typeof mediaMessages> export type MediaMessageCatalog = BlockMessageCatalog<typeof mediaMessages>
@@ -1,6 +1,6 @@
import type { MediaMessageCatalog } from "./catalogs" import type { MediaMessageCatalog } from "./catalogs"
export const locale = "en" export const locale = "en-US"
export const languageTag = "en-US" export const languageTag = "en-US"
export const messages = { export const messages = {
"blocks.media.actions.cancel": "Cancel", "blocks.media.actions.cancel": "Cancel",
@@ -21,10 +21,13 @@ export const messages = {
"blocks.media.actions.unfavorite": "Unfavorite", "blocks.media.actions.unfavorite": "Unfavorite",
"blocks.media.actions.upload": "Upload", "blocks.media.actions.upload": "Upload",
"blocks.media.actions.viewOriginal": "View original", "blocks.media.actions.viewOriginal": "View original",
"blocks.media.copy.failed": "Copy failed. Check browser clipboard permissions.", "blocks.media.copy.failed":
"blocks.media.deleteAsset.description": "This media item will no longer appear in the library.", "Copy failed. Check browser clipboard permissions.",
"blocks.media.deleteAsset.description":
"This media item will no longer appear in the library.",
"blocks.media.deleteAsset.title": "Delete media", "blocks.media.deleteAsset.title": "Delete media",
"blocks.media.deleteFolder.description": "This folder and its child folders will be deleted. Their media items will become unclassified.", "blocks.media.deleteFolder.description":
"This folder and its child folders will be deleted. Their media items will become unclassified.",
"blocks.media.deleteFolder.title": "Delete folder", "blocks.media.deleteFolder.title": "Delete folder",
"blocks.media.details.dimensions": "Dimensions", "blocks.media.details.dimensions": "Dimensions",
"blocks.media.details.fileName": "File name", "blocks.media.details.fileName": "File name",
@@ -32,7 +35,8 @@ export const messages = {
"blocks.media.details.size": "Size", "blocks.media.details.size": "Size",
"blocks.media.details.type": "Type", "blocks.media.details.type": "Type",
"blocks.media.details.uploadedAt": "Uploaded", "blocks.media.details.uploadedAt": "Uploaded",
"blocks.media.empty.description": "Upload images or videos to manage them here.", "blocks.media.empty.description":
"Upload images or videos to manage them here.",
"blocks.media.empty.title": "No media", "blocks.media.empty.title": "No media",
"blocks.media.emptySearch.description": "No media files match your search.", "blocks.media.emptySearch.description": "No media files match your search.",
"blocks.media.filters.all": "All media", "blocks.media.filters.all": "All media",
@@ -40,7 +44,8 @@ export const messages = {
"blocks.media.filters.image": "Images", "blocks.media.filters.image": "Images",
"blocks.media.filters.unclassified": "Unclassified", "blocks.media.filters.unclassified": "Unclassified",
"blocks.media.filters.video": "Videos", "blocks.media.filters.video": "Videos",
"blocks.media.folders.classification": "Folders only organize media in this library and do not change file URLs.", "blocks.media.folders.classification":
"Folders only organize media in this library and do not change file URLs.",
"blocks.media.folders.create": "New folder", "blocks.media.folders.create": "New folder",
"blocks.media.folders.empty": "No folders yet. Create one.", "blocks.media.folders.empty": "No folders yet. Create one.",
"blocks.media.folders.name": "Folder name", "blocks.media.folders.name": "Folder name",
@@ -50,11 +55,13 @@ export const messages = {
"blocks.media.folders.title": "Folders", "blocks.media.folders.title": "Folders",
"blocks.media.load.error": "Unable to load media", "blocks.media.load.error": "Unable to load media",
"blocks.media.loading": "Loading media…", "blocks.media.loading": "Loading media…",
"blocks.media.picker.description": "Choose an uploaded image or video from the media library.", "blocks.media.picker.description":
"Choose an uploaded image or video from the media library.",
"blocks.media.picker.selectedCount": "{count} selected", "blocks.media.picker.selectedCount": "{count} selected",
"blocks.media.picker.title": "Select media", "blocks.media.picker.title": "Select media",
"blocks.media.picker.useSelected": "Use selected media", "blocks.media.picker.useSelected": "Use selected media",
"blocks.media.renameMedia.description": "This changes the display name without changing the file or its URL.", "blocks.media.renameMedia.description":
"This changes the display name without changing the file or its URL.",
"blocks.media.renameMedia.title": "Rename media", "blocks.media.renameMedia.title": "Rename media",
"blocks.media.search": "Search media files", "blocks.media.search": "Search media files",
"blocks.media.title": "Media library", "blocks.media.title": "Media library",
@@ -72,7 +72,7 @@ function MediaTestProvider({
children: ReactNode children: ReactNode
}) { }) {
return ( return (
<I18nProvider locale="en"> <I18nProvider locale="en-US">
<MediaProvider adapter={adapter}>{children}</MediaProvider> <MediaProvider adapter={adapter}>{children}</MediaProvider>
</I18nProvider> </I18nProvider>
) )
@@ -3,27 +3,15 @@ import { describe, it } from "vitest"
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs" import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
import { navigationMessages } from "../messages" import { navigationMessages } from "../messages"
import { navigationCatalogLocales } from "./catalogs" import { navigationCatalogLocales } from "./catalogs"
import { messages as de } from "./de" import { messages as enUS } from "./en-US"
import { messages as en } from "./en"
import { messages as es } from "./es"
import { messages as fr } from "./fr"
import { messages as ja } from "./ja"
import { messages as ko } from "./ko"
import { messages as zhHans } from "./zh-Hans" import { messages as zhHans } from "./zh-Hans"
import { messages as zhHant } from "./zh-Hant"
describe("navigation locale catalogs", () => { describe("navigation locale catalogs", () => {
it("ship every navigation message in every built-in locale", () => { it("ship every navigation message in every built-in locale", () => {
expectCompleteCatalogs({ expectCompleteCatalogs({
catalogs: { catalogs: {
de, "en-US": enUS,
en,
es,
fr,
ja,
ko,
"zh-Hans": zhHans, "zh-Hans": zhHans,
"zh-Hant": zhHant,
}, },
locales: navigationCatalogLocales, locales: navigationCatalogLocales,
messageIds: Object.values(navigationMessages).map( messageIds: Object.values(navigationMessages).map(
@@ -1,9 +0,0 @@
import type { NavigationMessageCatalog } from "./catalogs"
export const locale = "de"
export const languageTag = "de-DE"
export const messages = {
"blocks.navigation.description": "Durch die Anwendung navigieren.",
"blocks.navigation.open": "Navigation öffnen",
"blocks.navigation.title": "Navigation",
} as const satisfies NavigationMessageCatalog
@@ -1,6 +1,6 @@
import type { NavigationMessageCatalog } from "./catalogs" import type { NavigationMessageCatalog } from "./catalogs"
export const locale = "en" export const locale = "en-US"
export const languageTag = "en-US" export const languageTag = "en-US"
export const messages = { export const messages = {
"blocks.navigation.description": "Navigate through the app.", "blocks.navigation.description": "Navigate through the app.",
@@ -1,9 +0,0 @@
import type { NavigationMessageCatalog } from "./catalogs"
export const locale = "es"
export const languageTag = "es-ES"
export const messages = {
"blocks.navigation.description": "Navega por la aplicación.",
"blocks.navigation.open": "Abrir navegación",
"blocks.navigation.title": "Navegación",
} as const satisfies NavigationMessageCatalog
@@ -1,9 +0,0 @@
import type { NavigationMessageCatalog } from "./catalogs"
export const locale = "fr"
export const languageTag = "fr-FR"
export const messages = {
"blocks.navigation.description": "Naviguez dans lapplication.",
"blocks.navigation.open": "Ouvrir la navigation",
"blocks.navigation.title": "Navigation",
} as const satisfies NavigationMessageCatalog
@@ -1,9 +0,0 @@
import type { NavigationMessageCatalog } from "./catalogs"
export const locale = "ja"
export const languageTag = "ja-JP"
export const messages = {
"blocks.navigation.description": "アプリ内のページを移動します。",
"blocks.navigation.open": "ナビゲーションを開く",
"blocks.navigation.title": "ナビゲーション",
} as const satisfies NavigationMessageCatalog
@@ -1,9 +0,0 @@
import type { NavigationMessageCatalog } from "./catalogs"
export const locale = "ko"
export const languageTag = "ko-KR"
export const messages = {
"blocks.navigation.description": "앱의 페이지를 탐색합니다.",
"blocks.navigation.open": "탐색 메뉴 열기",
"blocks.navigation.title": "탐색",
} as const satisfies NavigationMessageCatalog
@@ -1,9 +0,0 @@
import type { NavigationMessageCatalog } from "./catalogs"
export const locale = "zh-Hant"
export const languageTag = "zh-TW"
export const messages = {
"blocks.navigation.description": "瀏覽應用程式中的頁面。",
"blocks.navigation.open": "開啟導覽",
"blocks.navigation.title": "導覽",
} as const satisfies NavigationMessageCatalog
@@ -3,27 +3,15 @@ import { describe, it } from "vitest"
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs" import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
import { notificationMessages } from "../messages" import { notificationMessages } from "../messages"
import { notificationCatalogLocales } from "./catalogs" import { notificationCatalogLocales } from "./catalogs"
import { messages as de } from "./de" import { messages as enUS } from "./en-US"
import { messages as en } from "./en"
import { messages as es } from "./es"
import { messages as fr } from "./fr"
import { messages as ja } from "./ja"
import { messages as ko } from "./ko"
import { messages as zhHans } from "./zh-Hans" import { messages as zhHans } from "./zh-Hans"
import { messages as zhHant } from "./zh-Hant"
describe("notification locale catalogs", () => { describe("notification locale catalogs", () => {
it("ship every notification message in every built-in locale", () => { it("ship every notification message in every built-in locale", () => {
expectCompleteCatalogs({ expectCompleteCatalogs({
catalogs: { catalogs: {
de, "en-US": enUS,
en,
es,
fr,
ja,
ko,
"zh-Hans": zhHans, "zh-Hans": zhHans,
"zh-Hant": zhHant,
}, },
locales: notificationCatalogLocales, locales: notificationCatalogLocales,
messageIds: Object.values(notificationMessages).map( messageIds: Object.values(notificationMessages).map(
@@ -1,21 +0,0 @@
import type { NotificationMessageCatalog } from "./catalogs"
export const locale = "de"
export const languageTag = "de-DE"
export const messages = {
"blocks.notifications.description":
"Benachrichtigungen anzeigen und verwalten",
"blocks.notifications.empty.description":
"Neue Aktivitäten werden hier angezeigt.",
"blocks.notifications.empty.title": "Keine Benachrichtigungen",
"blocks.notifications.error.description":
"Überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut.",
"blocks.notifications.error.title":
"Benachrichtigungen konnten nicht geladen werden",
"blocks.notifications.justNow": "gerade eben",
"blocks.notifications.loading": "Benachrichtigungen werden geladen…",
"blocks.notifications.markAllAsRead": "Alle als gelesen markieren",
"blocks.notifications.retry": "Erneut versuchen",
"blocks.notifications.title": "Benachrichtigungen",
"blocks.notifications.viewAll": "Alle anzeigen",
} as const satisfies NotificationMessageCatalog
@@ -1,6 +1,6 @@
import type { NotificationMessageCatalog } from "./catalogs" import type { NotificationMessageCatalog } from "./catalogs"
export const locale = "en" export const locale = "en-US"
export const languageTag = "en-US" export const languageTag = "en-US"
export const messages = { export const messages = {
"blocks.notifications.description": "View and manage notifications", "blocks.notifications.description": "View and manage notifications",
@@ -1,20 +0,0 @@
import type { NotificationMessageCatalog } from "./catalogs"
export const locale = "es"
export const languageTag = "es-ES"
export const messages = {
"blocks.notifications.description": "Ver y gestionar notificaciones",
"blocks.notifications.empty.description":
"La actividad nueva aparecerá aquí.",
"blocks.notifications.empty.title": "No hay notificaciones",
"blocks.notifications.error.description":
"Comprueba la conexión de red e inténtalo de nuevo.",
"blocks.notifications.error.title":
"No se pudieron cargar las notificaciones",
"blocks.notifications.justNow": "ahora mismo",
"blocks.notifications.loading": "Cargando notificaciones…",
"blocks.notifications.markAllAsRead": "Marcar todo como leído",
"blocks.notifications.retry": "Intentar de nuevo",
"blocks.notifications.title": "Notificaciones",
"blocks.notifications.viewAll": "Ver todo",
} as const satisfies NotificationMessageCatalog
@@ -1,19 +0,0 @@
import type { NotificationMessageCatalog } from "./catalogs"
export const locale = "fr"
export const languageTag = "fr-FR"
export const messages = {
"blocks.notifications.description": "Afficher et gérer les notifications",
"blocks.notifications.empty.description":
"Les nouvelles activités apparaîtront ici.",
"blocks.notifications.empty.title": "Aucune notification",
"blocks.notifications.error.description":
"Vérifiez votre connexion réseau et réessayez.",
"blocks.notifications.error.title": "Impossible de charger les notifications",
"blocks.notifications.justNow": "à linstant",
"blocks.notifications.loading": "Chargement des notifications…",
"blocks.notifications.markAllAsRead": "Tout marquer comme lu",
"blocks.notifications.retry": "Réessayer",
"blocks.notifications.title": "Notifications",
"blocks.notifications.viewAll": "Tout afficher",
} as const satisfies NotificationMessageCatalog
@@ -1,19 +0,0 @@
import type { NotificationMessageCatalog } from "./catalogs"
export const locale = "ja"
export const languageTag = "ja-JP"
export const messages = {
"blocks.notifications.description": "通知を表示・管理します",
"blocks.notifications.empty.description":
"新しいアクティビティがここに表示されます。",
"blocks.notifications.empty.title": "通知はありません",
"blocks.notifications.error.description":
"ネットワーク接続を確認して、もう一度お試しください。",
"blocks.notifications.error.title": "通知を読み込めませんでした",
"blocks.notifications.justNow": "たった今",
"blocks.notifications.loading": "通知を読み込み中…",
"blocks.notifications.markAllAsRead": "すべて既読にする",
"blocks.notifications.retry": "再試行",
"blocks.notifications.title": "通知",
"blocks.notifications.viewAll": "すべて表示",
} as const satisfies NotificationMessageCatalog
@@ -1,18 +0,0 @@
import type { NotificationMessageCatalog } from "./catalogs"
export const locale = "ko"
export const languageTag = "ko-KR"
export const messages = {
"blocks.notifications.description": "알림 보기 및 관리",
"blocks.notifications.empty.description": "새 활동이 여기에 표시됩니다.",
"blocks.notifications.empty.title": "알림 없음",
"blocks.notifications.error.description":
"네트워크 연결을 확인한 후 다시 시도하세요.",
"blocks.notifications.error.title": "알림을 불러올 수 없음",
"blocks.notifications.justNow": "방금",
"blocks.notifications.loading": "알림 불러오는 중…",
"blocks.notifications.markAllAsRead": "모두 읽음으로 표시",
"blocks.notifications.retry": "다시 시도",
"blocks.notifications.title": "알림",
"blocks.notifications.viewAll": "모두 보기",
} as const satisfies NotificationMessageCatalog
@@ -1,17 +0,0 @@
import type { NotificationMessageCatalog } from "./catalogs"
export const locale = "zh-Hant"
export const languageTag = "zh-TW"
export const messages = {
"blocks.notifications.description": "檢視並管理通知",
"blocks.notifications.empty.description": "新的動態會顯示在這裡。",
"blocks.notifications.empty.title": "沒有通知",
"blocks.notifications.error.description": "請檢查網路連線後再試一次。",
"blocks.notifications.error.title": "無法載入通知",
"blocks.notifications.justNow": "剛剛",
"blocks.notifications.loading": "正在載入通知…",
"blocks.notifications.markAllAsRead": "全部標示為已讀",
"blocks.notifications.retry": "再試一次",
"blocks.notifications.title": "通知",
"blocks.notifications.viewAll": "檢視全部",
} as const satisfies NotificationMessageCatalog
+1 -10
View File
@@ -1,13 +1,4 @@
export const blockCatalogLocales = [ export const blockCatalogLocales = ["en-US", "zh-Hans"] as const
"de",
"en",
"es",
"fr",
"ja",
"ko",
"zh-Hans",
"zh-Hant",
] as const
interface MessageDescriptorMap { interface MessageDescriptorMap {
readonly [key: string]: { readonly [key: string]: {
+5 -1
View File
@@ -11,9 +11,13 @@ export function expectCompleteCatalogs({
}) { }) {
expect(Object.keys(catalogs)).toEqual([...locales]) expect(Object.keys(catalogs)).toEqual([...locales])
const expectedIds = [...messageIds].sort() // ES2022 compatibility: copy before sorting so the input remains immutable.
// oxlint-disable-next-line unicorn/no-array-sort
const expectedIds = Array.from(messageIds).sort()
for (const messages of Object.values(catalogs)) { for (const messages of Object.values(catalogs)) {
// Object.keys returns a fresh array.
// oxlint-disable-next-line unicorn/no-array-sort
expect(Object.keys(messages).sort()).toEqual(expectedIds) expect(Object.keys(messages).sort()).toEqual(expectedIds)
} }
} }
+13 -15
View File
@@ -28,8 +28,8 @@ Create `i18n.config.json` in that application:
```json ```json
{ {
"sourceLocale": "en", "sourceLocale": "en-US",
"locales": ["en", "zh-Hans"], "locales": ["en-US", "zh-Hans"],
"catalogPath": "src/locales/{locale}/messages", "catalogPath": "src/locales/{locale}/messages",
"catalogSources": [ "catalogSources": [
"@workspace/ui/locales/{locale}", "@workspace/ui/locales/{locale}",
@@ -96,7 +96,7 @@ function Root({
<I18nProvider <I18nProvider
locale={locale} locale={locale}
locales={[ locales={[
{ locale: "en", label: "English" }, { locale: "en-US", label: "English" },
{ locale: "zh-Hans", label: "简体中文" }, { locale: "zh-Hans", label: "简体中文" },
]} ]}
catalogs={{ [locale]: messages }} catalogs={{ [locale]: messages }}
@@ -126,22 +126,22 @@ function Greeting() {
} }
``` ```
`@workspace/ui` and every `@workspace/blocks` block provide `de`, `en`, `es`, `@workspace/ui`, `@workspace/lexical`, and every `@workspace/blocks` block
`fr`, `ja`, `ko`, `zh-Hans`, and `zh-Hant` as independent locale subpaths. provide only `en-US` and `zh-Hans` as independent locale subpaths. Import only
Import only the blocks and languages enabled by the application: the blocks and language enabled by the application:
```ts ```ts
import { import {
calendarLocale, calendarLocale,
messages as uiJapanese, messages as uiEnglish,
} from "@workspace/ui/locales/ja" } from "@workspace/ui/locales/en-US"
import { messages as navigationJapanese } from "@workspace/blocks/navigation/locales/ja" import { messages as navigationEnglish } from "@workspace/blocks/navigation/locales/en-US"
``` ```
The UI root `locales` entry and each block-specific `locales` entry export The UI root `locales` entry and each block-specific `locales` entry export
metadata and types only. They do not statically import catalogs. For example, metadata and types only. They do not statically import catalogs. For example,
using `@workspace/blocks/navigation` and using `@workspace/blocks/navigation` and
`@workspace/blocks/navigation/locales/ja` does not import appearance, chats, `@workspace/blocks/navigation/locales/en-US` does not import appearance, chats,
layout, notifications, or their translations. The UI locale entry also layout, notifications, or their translations. The UI locale entry also
exposes its matching `react-day-picker` `calendarLocale`. exposes its matching `react-day-picker` `calendarLocale`.
@@ -182,7 +182,7 @@ and available locales from the provider, while the Vite `i18n()` plugin serves
catalog reads, updates, extraction, and compilation. catalog reads, updates, extraction, and compilation.
```tsx ```tsx
<I18nProvider locale="zh-Hans" locales={["en", "zh-Hans"]}> <I18nProvider locale="zh-Hans" locales={["en-US", "zh-Hans"]}>
<App /> <App />
{import.meta.env.DEV && <I18nDevtool dark={"ui\\:dark"} />} {import.meta.env.DEV && <I18nDevtool dark={"ui\\:dark"} />}
</I18nProvider> </I18nProvider>
@@ -205,8 +205,7 @@ Pass a BCP 47 language tag with `locale` to select it explicitly:
When `locale` is omitted, the Devtool selects the first supported entry from When `locale` is omitted, the Devtool selects the first supported entry from
`navigator.languages` and falls back to English. Built-in control-panel `navigator.languages` and falls back to English. Built-in control-panel
locales are English, German, Spanish, French, Japanese, Korean, Simplified locales are English (`en-US`) and Simplified Chinese (`zh-Hans`).
Chinese, and Traditional Chinese.
Lower-level `MessagePanel`, `MessageRepositoryProvider`, and repository types Lower-level `MessagePanel`, `MessageRepositoryProvider`, and repository types
remain available from `@workspace/i18n/devtool`. remain available from `@workspace/i18n/devtool`.
@@ -222,7 +221,6 @@ Run inside the consuming application so the CLI discovers that application's
`i18n.config.json`: `i18n.config.json`:
```sh ```sh
bun run i18n new ja
bun run i18n extract bun run i18n extract
bun run i18n compile bun run i18n compile
bun run i18n ui bun run i18n ui
@@ -249,4 +247,4 @@ bun run i18n:compile
The application's `i18n.config.json` is the single machine-editable source of The application's `i18n.config.json` is the single machine-editable source of
truth. Runtime catalog access, Devtool actions, extraction, and compilation all truth. Runtime catalog access, Devtool actions, extraction, and compilation all
derive their Lingui configuration from it. derive their Lingui configuration from it.
+35 -29
View File
@@ -26,33 +26,39 @@ async function createProject() {
catalogPath: "app/{locale}/messages", catalogPath: "app/{locale}/messages",
catalogSources: ["./dependency/{locale}.js"], catalogSources: ["./dependency/{locale}.js"],
include: ["src"], include: ["src"],
locales: ["en", "zh-Hans"], locales: ["en-US", "zh-Hans"],
sourceLocale: "en", sourceLocale: "en-US",
} }
temporaryDirectories.push(projectRoot) temporaryDirectories.push(projectRoot)
for (const locale of project.locales) { await Promise.all(
const dependencyDirectory = join(projectRoot, "dependency") project.locales.map(async (locale) => {
const applicationDirectory = join(projectRoot, "app", locale) const dependencyDirectory = join(projectRoot, "dependency")
const applicationDirectory = join(projectRoot, "app", locale)
await mkdir(dependencyDirectory, { recursive: true }) await Promise.all([
await mkdir(applicationDirectory, { recursive: true }) mkdir(dependencyDirectory, { recursive: true }),
await writeFile( mkdir(applicationDirectory, { recursive: true }),
join(dependencyDirectory, `${locale}.js`), ])
`export const messages = ${JSON.stringify({ await Promise.all([
dependency: `${locale}: dependency`, writeFile(
overridden: `${locale}: dependency`, join(dependencyDirectory, `${locale}.js`),
})}\n` `export const messages = ${JSON.stringify({
) dependency: `${locale}: dependency`,
await writeFile( overridden: `${locale}: dependency`,
join(applicationDirectory, "messages.js"), })}\n`
`export const messages = ${JSON.stringify({ ),
application: `${locale}: application`, writeFile(
overridden: `${locale}: application`, join(applicationDirectory, "messages.js"),
})}\n` `export const messages = ${JSON.stringify({
) application: `${locale}: application`,
} overridden: `${locale}: application`,
})}\n`
),
])
})
)
return { project, projectRoot } return { project, projectRoot }
} }
@@ -63,12 +69,12 @@ describe("production catalog sources", () => {
const filenames = await resolveCatalogSourceFilenames( const filenames = await resolveCatalogSourceFilenames(
project, project,
projectRoot, projectRoot,
"en" "en-US"
) )
expect(filenames).toEqual([ expect(filenames).toEqual([
join(projectRoot, "dependency", "en.js"), join(projectRoot, "dependency", "en-US.js"),
join(projectRoot, "app", "en", "messages.js"), join(projectRoot, "app", "en-US", "messages.js"),
]) ])
}) })
@@ -76,10 +82,10 @@ describe("production catalog sources", () => {
const { project, projectRoot } = await createProject() const { project, projectRoot } = await createProject()
const { catalogs } = await loadProjectCatalogs(project, projectRoot) const { catalogs } = await loadProjectCatalogs(project, projectRoot)
expect(catalogs.en).toEqual({ expect(catalogs["en-US"]).toEqual({
application: "en: application", application: "en-US: application",
dependency: "en: dependency", dependency: "en-US: dependency",
overridden: "en: application", overridden: "en-US: application",
}) })
expect(catalogs["zh-Hans"]).toEqual({ expect(catalogs["zh-Hans"]).toEqual({
application: "zh-Hans: application", application: "zh-Hans: application",
+3 -5
View File
@@ -30,12 +30,10 @@ export async function resolveCompiledCatalogFilename(
? pathname ? pathname
: resolve(projectRoot, pathname) : resolve(projectRoot, pathname)
const candidates = [baseFilename, `${baseFilename}.ts`, `${baseFilename}.js`] const candidates = [baseFilename, `${baseFilename}.ts`, `${baseFilename}.js`]
const availability = await Promise.all(candidates.map(fileExists))
const candidate = candidates.find((_, index) => availability[index])
for (const candidate of candidates) { if (candidate) return candidate
if (await fileExists(candidate)) {
return candidate
}
}
throw new Error( throw new Error(
`Compiled i18n catalog not found for ${locale}: ${baseFilename}.{ts,js}. Run the i18n compile command before building.` `Compiled i18n catalog not found for ${locale}: ${baseFilename}.{ts,js}. Run the i18n compile command before building.`
@@ -23,7 +23,8 @@ export function createMessageIdSchema(
): MessageIdSchema { ): MessageIdSchema {
const ids = Array.from( const ids = Array.from(
new Set(Object.values(catalogs).flatMap((catalog) => Object.keys(catalog))) new Set(Object.values(catalogs).flatMap((catalog) => Object.keys(catalog)))
).sort() )
ids.sort()
const messages: Record<string, string> = {} const messages: Record<string, string> = {}
const idsByHash = new Map<string, string>() const idsByHash = new Map<string, string>()
+89 -90
View File
@@ -1,5 +1,5 @@
import type { IncomingMessage, ServerResponse } from "node:http" import type { IncomingMessage, ServerResponse } from "node:http"
import type { ViteDevServer } from "vite" import type { Connect, ViteDevServer } from "vite"
import type { UpdateMessageInput } from "../devtool/types" import type { UpdateMessageInput } from "../devtool/types"
import { CatalogService } from "./catalog-service.ts" import { CatalogService } from "./catalog-service.ts"
@@ -43,94 +43,93 @@ export function configureI18nApi(
server: ViteDevServer, server: ViteDevServer,
projectFilename: string projectFilename: string
) { ) {
server.middlewares.use(async (request, response, next) => { server.middlewares.use((request, response, next) => {
if (!request.url) { void handleI18nApiRequest(request, response, next, projectFilename).catch(
next() next
return )
}
const url = new URL(request.url, "http://localhost")
if (!url.pathname.startsWith(API_PREFIX)) {
next()
return
}
try {
if (
request.method === "GET" &&
url.pathname === `${API_PREFIX}/project`
) {
const project = await readProject(projectFilename)
sendJson(response, 200, project)
return
}
if (
request.method === "GET" &&
url.pathname === `${API_PREFIX}/messages`
) {
const locale = url.searchParams.get("locale")
if (!locale) {
sendJson(response, 400, { error: "locale is required." })
return
}
const service = new CatalogService(projectFilename)
sendJson(response, 200, await service.getMessages(locale))
return
}
if (
request.method === "GET" &&
url.pathname === `${API_PREFIX}/packages`
) {
const service = new CatalogService(projectFilename)
sendJson(response, 200, await service.getPackages())
return
}
if (
request.method === "PUT" &&
url.pathname === `${API_PREFIX}/messages`
) {
const input = await readJsonBody(request)
if (!isUpdateMessageInput(input)) {
sendJson(response, 400, {
error: "The message update payload is invalid.",
})
return
}
const service = new CatalogService(projectFilename)
sendJson(response, 200, await service.updateMessage(input))
return
}
if (
request.method === "POST" &&
url.pathname.startsWith(`${API_PREFIX}/actions/`)
) {
const action = url.pathname.slice(`${API_PREFIX}/actions/`.length)
if (action !== "extract" && action !== "compile") {
sendJson(response, 404, { error: "Unknown action." })
return
}
const result = await runProjectLinguiCommand(projectFilename, action)
sendJson(response, 200, result)
return
}
sendJson(response, 404, { error: "Not found." })
} catch (error) {
sendJson(response, 500, {
error:
error instanceof Error ? error.message : "An unknown error occurred.",
})
}
}) })
} }
async function handleI18nApiRequest(
request: IncomingMessage,
response: ServerResponse,
next: Connect.NextFunction,
projectFilename: string
) {
if (!request.url) {
next()
return
}
const url = new URL(request.url, "http://localhost")
if (!url.pathname.startsWith(API_PREFIX)) {
next()
return
}
try {
if (request.method === "GET" && url.pathname === `${API_PREFIX}/project`) {
const project = await readProject(projectFilename)
sendJson(response, 200, project)
return
}
if (request.method === "GET" && url.pathname === `${API_PREFIX}/messages`) {
const locale = url.searchParams.get("locale")
if (!locale) {
sendJson(response, 400, { error: "locale is required." })
return
}
const service = new CatalogService(projectFilename)
sendJson(response, 200, await service.getMessages(locale))
return
}
if (request.method === "GET" && url.pathname === `${API_PREFIX}/packages`) {
const service = new CatalogService(projectFilename)
sendJson(response, 200, await service.getPackages())
return
}
if (request.method === "PUT" && url.pathname === `${API_PREFIX}/messages`) {
const input = await readJsonBody(request)
if (!isUpdateMessageInput(input)) {
sendJson(response, 400, {
error: "The message update payload is invalid.",
})
return
}
const service = new CatalogService(projectFilename)
sendJson(response, 200, await service.updateMessage(input))
return
}
if (
request.method === "POST" &&
url.pathname.startsWith(`${API_PREFIX}/actions/`)
) {
const action = url.pathname.slice(`${API_PREFIX}/actions/`.length)
if (action !== "extract" && action !== "compile") {
sendJson(response, 404, { error: "Unknown action." })
return
}
const result = await runProjectLinguiCommand(projectFilename, action)
sendJson(response, 200, result)
return
}
sendJson(response, 404, { error: "Not found." })
} catch (error) {
sendJson(response, 500, {
error:
error instanceof Error ? error.message : "An unknown error occurred.",
})
}
}
@@ -39,8 +39,8 @@ describe("CatalogService", () => {
{ {
catalogPath: "locales/{locale}/messages", catalogPath: "locales/{locale}/messages",
include: ["src"], include: ["src"],
locales: ["en", "zh-Hans"], locales: ["en-US", "zh-Hans"],
sourceLocale: "en", sourceLocale: "en-US",
}, },
null, null,
2 2
@@ -54,7 +54,7 @@ describe("CatalogService", () => {
throw new Error("Expected a Lingui catalog.") throw new Error("Expected a Lingui catalog.")
} }
await catalog.write("en", { await catalog.write("en-US", {
"dashboard.title": { "dashboard.title": {
comments: ["Dashboard heading"], comments: ["Dashboard heading"],
message: "Dashboard", message: "Dashboard",
@@ -113,7 +113,7 @@ describe("CatalogService", () => {
'msgstr "数据看板"' 'msgstr "数据看板"'
) )
const sourceMessages = await service.getMessages("en") const sourceMessages = await service.getMessages("en-US")
const applicationMessage = sourceMessages.find( const applicationMessage = sourceMessages.find(
(item) => item.id === "dashboard.title" (item) => item.id === "dashboard.title"
) )
@@ -134,7 +134,7 @@ describe("CatalogService", () => {
service.updateMessage({ service.updateMessage({
catalog: applicationMessage?.catalog ?? "messages", catalog: applicationMessage?.catalog ?? "messages",
id: "dashboard.title", id: "dashboard.title",
locale: "en", locale: "en-US",
translation: "Customized dashboard", translation: "Customized dashboard",
}) })
).rejects.toThrow("must be changed in source code") ).rejects.toThrow("must be changed in source code")
@@ -142,7 +142,7 @@ describe("CatalogService", () => {
const customizedDependency = await service.updateMessage({ const customizedDependency = await service.updateMessage({
catalog: dependencyMessage?.catalog ?? "messages", catalog: dependencyMessage?.catalog ?? "messages",
id: "ui.common.close", id: "ui.common.close",
locale: "en", locale: "en-US",
translation: "Dismiss", translation: "Dismiss",
}) })
@@ -151,7 +151,7 @@ describe("CatalogService", () => {
source: "Close", source: "Close",
translation: "Dismiss", translation: "Dismiss",
}) })
expect(await readFile(catalog.getFilename("en"), "utf8")).toContain( expect(await readFile(catalog.getFilename("en-US"), "utf8")).toContain(
'msgstr "Dismiss"' 'msgstr "Dismiss"'
) )
}) })
+28 -26
View File
@@ -37,15 +37,16 @@ class PackageNameResolver {
} }
async resolve(origins: MessageType["origin"] | undefined) { async resolve(origins: MessageType["origin"] | undefined) {
for (const [filename] of origins ?? []) { const packageNames = await Promise.all(
const packageName = await this.#findNearestPackageName( (origins ?? []).map(([filename]) =>
dirname(resolve(this.#projectRoot, filename)) this.#findNearestPackageName(
dirname(resolve(this.#projectRoot, filename))
)
) )
)
const packageName = packageNames.find(Boolean)
if (packageName) { if (packageName) return packageName
return packageName
}
}
return this.#projectPackageName return this.#projectPackageName
} }
@@ -187,27 +188,28 @@ export class CatalogService {
...Object.keys(targetCatalog), ...Object.keys(targetCatalog),
]) ])
return Promise.all( const sortedIds = Array.from(ids)
Array.from(ids) sortedIds.sort((left, right) => left.localeCompare(right))
.sort((left, right) => left.localeCompare(right))
.map(async (id) => {
const sourceEntry = sourceCatalog[id]
const targetEntry = targetCatalog[id]
const packageName = await packageNameResolver.resolve(
sourceEntry?.origin ?? targetEntry?.origin
)
return toDevtoolMessage({ return Promise.all(
applicationPackageName, sortedIds.map(async (id) => {
catalog: key, const sourceEntry = sourceCatalog[id]
id, const targetEntry = targetCatalog[id]
locale, const packageName = await packageNameResolver.resolve(
packageName, sourceEntry?.origin ?? targetEntry?.origin
sourceEntry, )
sourceLocale: config.sourceLocale,
targetEntry, return toDevtoolMessage({
}) applicationPackageName,
catalog: key,
id,
locale,
packageName,
sourceEntry,
sourceLocale: config.sourceLocale,
targetEntry,
}) })
})
) )
}) })
) )
+4 -4
View File
@@ -30,8 +30,8 @@ describe("runProjectLinguiCommand", () => {
{ {
catalogPath: "locales/{locale}/messages", catalogPath: "locales/{locale}/messages",
include: ["src"], include: ["src"],
locales: ["en"], locales: ["en-US"],
sourceLocale: "en", sourceLocale: "en-US",
}, },
null, null,
2 2
@@ -45,7 +45,7 @@ describe("runProjectLinguiCommand", () => {
throw new Error("Expected a Lingui catalog.") throw new Error("Expected a Lingui catalog.")
} }
await catalog.write("en", { await catalog.write("en-US", {
greeting: { greeting: {
message: "Hello", message: "Hello",
translation: "Hello", translation: "Hello",
@@ -53,7 +53,7 @@ describe("runProjectLinguiCommand", () => {
}) })
const result = await runProjectLinguiCommand(projectFilename, "compile") const result = await runProjectLinguiCommand(projectFilename, "compile")
const compiledFilename = join(directory, "locales/en/messages.ts") const compiledFilename = join(directory, "locales/en-US/messages.ts")
const firstModifiedTime = (await stat(compiledFilename)).mtimeMs const firstModifiedTime = (await stat(compiledFilename)).mtimeMs
expect(result.output).toContain("Compiling message catalogs") expect(result.output).toContain("Compiling message catalogs")
+14 -12
View File
@@ -83,24 +83,26 @@ async function readFileIfPresent(filename: string) {
async function publishCompiledCatalogs( async function publishCompiledCatalogs(
catalogs: readonly StagedCompiledCatalog[] catalogs: readonly StagedCompiledCatalog[]
) { ) {
for (const catalog of catalogs) { await Promise.all(
const nextContent = await readFile(catalog.stagedOutputFilename, "utf8") catalogs.map(async (catalog) => {
const currentContent = await readFileIfPresent(catalog.outputFilename) const [nextContent, currentContent] = await Promise.all([
readFile(catalog.stagedOutputFilename, "utf8"),
readFileIfPresent(catalog.outputFilename),
])
if (currentContent === nextContent) { if (currentContent === nextContent) return
continue
}
await mkdir(dirname(catalog.outputFilename), { recursive: true }) await mkdir(dirname(catalog.outputFilename), { recursive: true })
await writeFile(catalog.outputFilename, nextContent, "utf8") await writeFile(catalog.outputFilename, nextContent, "utf8")
} })
)
} }
async function runLinguiCommand( async function runLinguiCommand(
args: readonly string[], args: readonly string[],
cwd: string cwd: string
): Promise<LinguiCommandResult> { ): Promise<LinguiCommandResult> {
return new Promise((resolve, reject) => { return new Promise((resolvePromise, reject) => {
const child = spawn("node", [LINGUI_BIN, ...args], { const child = spawn("node", [LINGUI_BIN, ...args], {
cwd, cwd,
env: process.env, env: process.env,
@@ -119,7 +121,7 @@ async function runLinguiCommand(
child.on("error", reject) child.on("error", reject)
child.on("close", (code) => { child.on("close", (code) => {
if (code === 0) { if (code === 0) {
resolve({ output }) resolvePromise({ output })
return return
} }
@@ -170,7 +172,7 @@ export async function runProjectLinguiCommand(
catalogs: projectConfig.catalogs?.map((catalog) => catalogs: projectConfig.catalogs?.map((catalog) =>
typeof catalog === "string" typeof catalog === "string"
? catalog ? catalog
: { ...catalog, path: stagedCatalogPath } : Object.assign({}, catalog, { path: stagedCatalogPath })
), ),
} }
: undefined), : undefined),
+2 -2
View File
@@ -62,8 +62,8 @@ describe("readProject", () => {
"@workspace/blocks/navigation/locales/{locale}", "@workspace/blocks/navigation/locales/{locale}",
], ],
include: ["src"], include: ["src"],
locales: ["en", "zh-Hans"], locales: ["en-US", "zh-Hans"],
sourceLocale: "en", sourceLocale: "en-US",
})}\n` })}\n`
) )
+8 -16
View File
@@ -60,24 +60,16 @@ function validateProjectConfig(
} }
} }
async function findInParents(startDirectory: string) { async function findInParents(
let directory = resolve(startDirectory) startDirectory: string
): Promise<string | undefined> {
const directory = resolve(startDirectory)
const candidate = join(directory, PROJECT_FILENAME)
while (true) { if (await fileExists(candidate)) return candidate
const candidate = join(directory, PROJECT_FILENAME)
if (await fileExists(candidate)) { const parent = dirname(directory)
return candidate return parent === directory ? undefined : findInParents(parent)
}
const parent = dirname(directory)
if (parent === directory) {
return undefined
}
directory = parent
}
} }
export async function resolveProjectFilename( export async function resolveProjectFilename(
@@ -13,8 +13,8 @@ describe("createLinguiConfig", () => {
catalogSources: ["@workspace/ui/locales/{locale}"], catalogSources: ["@workspace/ui/locales/{locale}"],
exclude: ["**/*.test.ts"], exclude: ["**/*.test.ts"],
include: ["src"], include: ["src"],
locales: ["en", "zh-Hans"], locales: ["en-US", "zh-Hans"],
sourceLocale: "en", sourceLocale: "en-US",
}) })
).toEqual({ ).toEqual({
catalogs: [ catalogs: [
@@ -24,11 +24,11 @@ describe("createLinguiConfig", () => {
path: "<rootDir>/locales/{locale}/messages", path: "<rootDir>/locales/{locale}/messages",
}, },
], ],
locales: ["en", "zh-Hans"], locales: ["en-US", "zh-Hans"],
runtimeConfigModule: { runtimeConfigModule: {
Trans: ["@workspace/i18n", "Translate"], Trans: ["@workspace/i18n", "Translate"],
}, },
sourceLocale: "en", sourceLocale: "en-US",
}) })
}) })
@@ -37,8 +37,8 @@ describe("createLinguiConfig", () => {
{ {
catalogPath: "locales/{locale}/messages", catalogPath: "locales/{locale}/messages",
include: ["src"], include: ["src"],
locales: ["en"], locales: ["en-US"],
sourceLocale: "en", sourceLocale: "en-US",
}, },
"/workspace/apps/web" "/workspace/apps/web"
) )
+8 -8
View File
@@ -4,16 +4,16 @@ import { describe, expect, it } from "vitest"
import { AnsiText, resolveAnsiComponent } from "./ansi-text" import { AnsiText, resolveAnsiComponent } from "./ansi-text"
function TestComponent({ children }: { children?: string }) {
return <code>{children}</code>
}
describe("AnsiText", () => { describe("AnsiText", () => {
it("resolves direct, CommonJS, and nested default exports", () => { it("resolves direct, CommonJS, and nested default exports", () => {
const Component = ({ children }: { children?: string }) => ( expect(resolveAnsiComponent(TestComponent)).toBe(TestComponent)
<code>{children}</code> expect(resolveAnsiComponent({ default: TestComponent })).toBe(TestComponent)
) expect(resolveAnsiComponent({ default: { default: TestComponent } })).toBe(
TestComponent
expect(resolveAnsiComponent(Component)).toBe(Component)
expect(resolveAnsiComponent({ default: Component })).toBe(Component)
expect(resolveAnsiComponent({ default: { default: Component } })).toBe(
Component
) )
}) })
+1 -1
View File
@@ -42,7 +42,7 @@ export function DevtoolApp() {
const locale = const locale =
state.project.locales.find( state.project.locales.find(
(locale) => locale !== state.project.sourceLocale (candidateLocale) => candidateLocale !== state.project.sourceLocale
) ?? state.project.sourceLocale ) ?? state.project.sourceLocale
if (!state.project.locales.includes(locale)) { if (!state.project.locales.includes(locale)) {
+1 -1
View File
@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en-US">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+1
View File
@@ -2,6 +2,7 @@ import { StrictMode } from "react"
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { DevtoolApp } from "./app" import { DevtoolApp } from "./app"
// oxlint-disable-next-line import/no-unassigned-import -- Loads the standalone Devtool stylesheet.
import "./styles.css" import "./styles.css"
const root = document.getElementById("root") const root = document.getElementById("root")
@@ -5,17 +5,18 @@ import { resolveI18nDevtoolLocale } from "./devtool-localization"
describe("resolveI18nDevtoolLocale", () => { describe("resolveI18nDevtoolLocale", () => {
it("normalizes common BCP 47 language tags", () => { it("normalizes common BCP 47 language tags", () => {
expect(resolveI18nDevtoolLocale("zh-CN")).toBe("zh-Hans") expect(resolveI18nDevtoolLocale("zh-CN")).toBe("zh-Hans")
expect(resolveI18nDevtoolLocale("zh-TW")).toBe("zh-Hant") expect(resolveI18nDevtoolLocale("en-GB")).toBe("en-US")
expect(resolveI18nDevtoolLocale("es-419")).toBe("es")
}) })
it("selects the first supported preferred locale", () => { it("selects the first supported preferred locale", () => {
expect( expect(
resolveI18nDevtoolLocale(undefined, ["pt-BR", "fr-CA", "en-US"]) resolveI18nDevtoolLocale(undefined, ["pt-BR", "fr-CA", "zh-CN"])
).toBe("fr") ).toBe("zh-Hans")
}) })
it("falls back to English", () => { it("falls back to English", () => {
expect(resolveI18nDevtoolLocale(undefined, ["pt-BR"])).toBe("en") expect(resolveI18nDevtoolLocale(undefined, ["pt-BR", "zh-TW"])).toBe(
"en-US"
)
}) })
}) })
@@ -1,15 +1,6 @@
import * as React from "react" import * as React from "react"
export const I18N_DEVTOOL_LOCALES = [ export const I18N_DEVTOOL_LOCALES = ["en-US", "zh-Hans"] as const
"en",
"de",
"es",
"fr",
"ja",
"ko",
"zh-Hans",
"zh-Hant",
] as const
export type I18nDevtoolLocale = (typeof I18N_DEVTOOL_LOCALES)[number] export type I18nDevtoolLocale = (typeof I18N_DEVTOOL_LOCALES)[number]
@@ -104,7 +95,7 @@ interface DevtoolMessages {
} }
const messagesByLocale: Record<I18nDevtoolLocale, DevtoolMessages> = { const messagesByLocale: Record<I18nDevtoolLocale, DevtoolMessages> = {
en: { "en-US": {
actions: { actions: {
compile: "Compile", compile: "Compile",
compileCompleted: "Compilation completed.", compileCompleted: "Compilation completed.",
@@ -195,460 +186,6 @@ const messagesByLocale: Record<I18nDevtoolLocale, DevtoolMessages> = {
open: "Open I18n Devtool", open: "Open I18n Devtool",
}, },
}, },
de: {
actions: {
compile: "Kompilieren",
compileCompleted: "Kompilierung abgeschlossen.",
compileFailed: "Kompilierung fehlgeschlagen.",
compiling: "Wird kompiliert…",
enterFullscreen: "Vollbildmodus aktivieren",
exitFullscreen: "Vollbildmodus beenden",
extract: "Extrahieren",
extractCompleted: "Extraktion abgeschlossen.",
extractFailed: "Extraktion fehlgeschlagen.",
extracting: "Wird extrahiert…",
label: "Aktionen",
},
commandOutput: {
close: "Befehlsausgabe schließen",
collapse: "Befehlsausgabe einklappen",
expand: "Befehlsausgabe ausklappen",
title: "Befehlsausgabe",
},
locale: {
controlLabel: "Zielsprache der Nachrichten",
label: "Zielsprache",
noTargetLocales: "Keine Zielsprachen konfiguriert.",
sourceLabel: "Ausgangssprache",
},
messagePanel: {
additionalSourceLocations: (count) => `${count} weitere Quellorte`,
chooseCopyContent: "Inhalt zum Kopieren auswählen",
collapseNamespace: (path) => `${path} einklappen`,
collapsePackage: (packageName) => `${packageName} einklappen`,
copied: "Kopiert",
copyDescriptor: "Nachrichten-Deskriptor kopieren",
copyFailed: "Kopieren fehlgeschlagen",
copyMessageId: "Nachrichten-ID kopieren",
copySource: "Quelltext kopieren",
copySourceLocation: "Quellort kopieren",
copyTranslation: "Aktuelle Übersetzung kopieren",
couldNotSave: "Speichern fehlgeschlagen",
emptyCatalog:
"Es sind noch keine Nachrichten verfügbar. Extrahieren Sie das Projekt, um diesen Katalog zu füllen.",
emptyPackage: "In diesem Paket wurden keine Nachrichten gefunden.",
expandNamespace: (path) => `${path} ausklappen`,
expandPackage: (packageName) => `${packageName} ausklappen`,
loading: (locale) => `${locale} wird geladen…`,
messageCount: (count) => `${count} Nachrichten`,
messageEditor: "Nachrichteneditor",
messageNavigation: "Nachrichtennavigation",
missingCount: (count) => `${count} fehlen`,
missingOnly: "Nur fehlende",
noMatches: "Keine Nachrichten entsprechen den aktuellen Filtern.",
noMissingMessages: "Alle Nachrichten sind übersetzt.",
noSearchResults: "Keine Nachrichten entsprechen Ihrer Suche.",
reset: "Zurücksetzen",
retry: "Erneut versuchen",
save: "Speichern",
saved: "Gespeichert",
saving: "Wird gespeichert…",
search: "Nachrichten durchsuchen",
selectMessage:
"Wählen Sie eine Nachricht aus, um ihre Übersetzung anzuzeigen und zu bearbeiten.",
source: "Quelle",
sourceLocations: "Quellorte",
translation: "Übersetzung",
translationFor: (id, locale) => `${id}: Übersetzung für ${locale}`,
unknownSourceLocation: "Unbekannter Quellort",
},
settings: {
auto: "Automatisch",
autoTheme: "Automatisches Design",
center: "Mitte",
dark: "Dunkel",
darkTheme: "Dunkles Design",
interfaceLanguage: "Oberflächensprache",
interfaceLanguageControl: "Oberflächensprache des I18n Devtools",
label: "I18n-Devtool-Einstellungen",
left: "Links",
light: "Hell",
lightTheme: "Helles Design",
open: "I18n-Devtool-Einstellungen öffnen",
panelPosition: "Panelposition",
panelPositionControl: "Position des I18n-Devtool-Panels",
right: "Rechts",
theme: "Design",
themeControl: "I18n-Devtool-Design",
},
trigger: {
close: "I18n Devtool schließen",
open: "I18n Devtool öffnen",
},
},
es: {
actions: {
compile: "Compilar",
compileCompleted: "Compilación completada.",
compileFailed: "Error de compilación.",
compiling: "Compilando…",
enterFullscreen: "Entrar en pantalla completa",
exitFullscreen: "Salir de pantalla completa",
extract: "Extraer",
extractCompleted: "Extracción completada.",
extractFailed: "Error de extracción.",
extracting: "Extrayendo…",
label: "Acciones",
},
commandOutput: {
close: "Cerrar salida del comando",
collapse: "Contraer salida del comando",
expand: "Expandir salida del comando",
title: "Salida del comando",
},
locale: {
controlLabel: "Idioma de destino de los mensajes",
label: "Idioma de destino",
noTargetLocales: "No hay idiomas de destino configurados.",
sourceLabel: "Idioma de origen",
},
messagePanel: {
additionalSourceLocations: (count) =>
`${count} ubicaciones de origen adicionales`,
chooseCopyContent: "Elegir contenido para copiar",
collapseNamespace: (path) => `Contraer ${path}`,
collapsePackage: (packageName) => `Contraer ${packageName}`,
copied: "Copiado",
copyDescriptor: "Copiar descriptor del mensaje",
copyFailed: "No se pudo copiar",
copyMessageId: "Copiar ID del mensaje",
copySource: "Copiar texto de origen",
copySourceLocation: "Copiar ubicación de origen",
copyTranslation: "Copiar traducción actual",
couldNotSave: "No se pudo guardar",
emptyCatalog:
"Aún no hay mensajes disponibles. Extraiga el proyecto para completar este catálogo.",
emptyPackage: "No se encontraron mensajes en este paquete.",
expandNamespace: (path) => `Expandir ${path}`,
expandPackage: (packageName) => `Expandir ${packageName}`,
loading: (locale) => `Cargando ${locale}`,
messageCount: (count) => `${count} mensajes`,
messageEditor: "Editor de mensajes",
messageNavigation: "Navegación de mensajes",
missingCount: (count) => `${count} faltantes`,
missingOnly: "Solo faltantes",
noMatches: "Ningún mensaje coincide con los filtros actuales.",
noMissingMessages: "Todos los mensajes tienen traducción.",
noSearchResults: "Ningún mensaje coincide con la búsqueda.",
reset: "Restablecer",
retry: "Reintentar",
save: "Guardar",
saved: "Guardado",
saving: "Guardando…",
search: "Buscar mensajes",
selectMessage: "Seleccione un mensaje para ver y editar su traducción.",
source: "Origen",
sourceLocations: "Ubicaciones de origen",
translation: "Traducción",
translationFor: (id, locale) => `Traducción de ${id} para ${locale}`,
unknownSourceLocation: "Ubicación de origen desconocida",
},
settings: {
auto: "Automático",
autoTheme: "Tema automático",
center: "Centro",
dark: "Oscuro",
darkTheme: "Tema oscuro",
interfaceLanguage: "Idioma de la interfaz",
interfaceLanguageControl: "Idioma de la interfaz de I18n Devtool",
label: "Ajustes de I18n Devtool",
left: "Izquierda",
light: "Claro",
lightTheme: "Tema claro",
open: "Abrir ajustes de I18n Devtool",
panelPosition: "Posición del panel",
panelPositionControl: "Posición del panel de I18n Devtool",
right: "Derecha",
theme: "Tema",
themeControl: "Tema de I18n Devtool",
},
trigger: {
close: "Cerrar I18n Devtool",
open: "Abrir I18n Devtool",
},
},
fr: {
actions: {
compile: "Compiler",
compileCompleted: "Compilation terminée.",
compileFailed: "Échec de la compilation.",
compiling: "Compilation…",
enterFullscreen: "Passer en plein écran",
exitFullscreen: "Quitter le plein écran",
extract: "Extraire",
extractCompleted: "Extraction terminée.",
extractFailed: "Échec de lextraction.",
extracting: "Extraction…",
label: "Actions",
},
commandOutput: {
close: "Fermer la sortie de commande",
collapse: "Réduire la sortie de commande",
expand: "Développer la sortie de commande",
title: "Sortie de commande",
},
locale: {
controlLabel: "Langue cible des messages",
label: "Langue cible",
noTargetLocales: "Aucune langue cible nest configurée.",
sourceLabel: "Langue source",
},
messagePanel: {
additionalSourceLocations: (count) =>
`${count} emplacements source supplémentaires`,
chooseCopyContent: "Choisir le contenu à copier",
collapseNamespace: (path) => `Réduire ${path}`,
collapsePackage: (packageName) => `Réduire ${packageName}`,
copied: "Copié",
copyDescriptor: "Copier le descripteur du message",
copyFailed: "Échec de la copie",
copyMessageId: "Copier lidentifiant du message",
copySource: "Copier le texte source",
copySourceLocation: "Copier lemplacement source",
copyTranslation: "Copier la traduction actuelle",
couldNotSave: "Échec de lenregistrement",
emptyCatalog:
"Aucun message nest encore disponible. Extrayez le projet pour remplir ce catalogue.",
emptyPackage: "Aucun message na été trouvé dans ce package.",
expandNamespace: (path) => `Développer ${path}`,
expandPackage: (packageName) => `Développer ${packageName}`,
loading: (locale) => `Chargement de ${locale}`,
messageCount: (count) => `${count} messages`,
messageEditor: "Éditeur de messages",
messageNavigation: "Navigation des messages",
missingCount: (count) => `${count} manquants`,
missingOnly: "Manquants uniquement",
noMatches: "Aucun message ne correspond aux filtres actuels.",
noMissingMessages: "Tous les messages sont traduits.",
noSearchResults: "Aucun message ne correspond à votre recherche.",
reset: "Réinitialiser",
retry: "Réessayer",
save: "Enregistrer",
saved: "Enregistré",
saving: "Enregistrement…",
search: "Rechercher des messages",
selectMessage:
"Sélectionnez un message pour afficher et modifier sa traduction.",
source: "Source",
sourceLocations: "Emplacements source",
translation: "Traduction",
translationFor: (id, locale) => `Traduction de ${id} pour ${locale}`,
unknownSourceLocation: "Emplacement source inconnu",
},
settings: {
auto: "Auto",
autoTheme: "Thème automatique",
center: "Centre",
dark: "Sombre",
darkTheme: "Thème sombre",
interfaceLanguage: "Langue de linterface",
interfaceLanguageControl: "Langue de linterface de loutil I18n",
label: "Paramètres de I18n Devtool",
left: "Gauche",
light: "Clair",
lightTheme: "Thème clair",
open: "Ouvrir les paramètres de I18n Devtool",
panelPosition: "Position du panneau",
panelPositionControl: "Position du panneau I18n Devtool",
right: "Droite",
theme: "Thème",
themeControl: "Thème de I18n Devtool",
},
trigger: {
close: "Fermer I18n Devtool",
open: "Ouvrir I18n Devtool",
},
},
ja: {
actions: {
compile: "コンパイル",
compileCompleted: "コンパイルが完了しました。",
compileFailed: "コンパイルに失敗しました。",
compiling: "コンパイル中…",
enterFullscreen: "全画面表示にする",
exitFullscreen: "全画面表示を終了",
extract: "抽出",
extractCompleted: "抽出が完了しました。",
extractFailed: "抽出に失敗しました。",
extracting: "抽出中…",
label: "操作",
},
commandOutput: {
close: "コマンド出力を閉じる",
collapse: "コマンド出力を折りたたむ",
expand: "コマンド出力を展開する",
title: "コマンド出力",
},
locale: {
controlLabel: "メッセージの翻訳先言語",
label: "翻訳先言語",
noTargetLocales: "翻訳先言語が設定されていません。",
sourceLabel: "原文言語",
},
messagePanel: {
additionalSourceLocations: (count) => `${count} 件のソース位置`,
chooseCopyContent: "コピーする内容を選択",
collapseNamespace: (path) => `${path} を折りたたむ`,
collapsePackage: (packageName) => `${packageName} を折りたたむ`,
copied: "コピーしました",
copyDescriptor: "メッセージ記述子をコピー",
copyFailed: "コピーできませんでした",
copyMessageId: "メッセージ ID をコピー",
copySource: "原文をコピー",
copySourceLocation: "ソース位置をコピー",
copyTranslation: "現在の翻訳をコピー",
couldNotSave: "保存できませんでした",
emptyCatalog:
"メッセージはまだありません。プロジェクトを抽出してカタログを作成してください。",
emptyPackage: "このパッケージにはメッセージがありません。",
expandNamespace: (path) => `${path} を展開`,
expandPackage: (packageName) => `${packageName} を展開`,
loading: (locale) => `${locale} を読み込み中…`,
messageCount: (count) => `${count}`,
messageEditor: "メッセージエディター",
messageNavigation: "メッセージナビゲーション",
missingCount: (count) => `未翻訳 ${count}`,
missingOnly: "未翻訳のみ",
noMatches: "現在のフィルターに一致するメッセージはありません。",
noMissingMessages: "すべてのメッセージが翻訳済みです。",
noSearchResults: "検索に一致するメッセージはありません。",
reset: "リセット",
retry: "再試行",
save: "保存",
saved: "保存済み",
saving: "保存中…",
search: "メッセージを検索",
selectMessage: "翻訳を表示・編集するメッセージを選択してください。",
source: "原文",
sourceLocations: "ソース位置",
translation: "翻訳",
translationFor: (id, locale) => `${id}${locale} 翻訳`,
unknownSourceLocation: "ソース位置不明",
},
settings: {
auto: "自動",
autoTheme: "自動テーマ",
center: "中央",
dark: "ダーク",
darkTheme: "ダークテーマ",
interfaceLanguage: "表示言語",
interfaceLanguageControl: "I18n Devtool の表示言語",
label: "I18n Devtool の設定",
left: "左",
light: "ライト",
lightTheme: "ライトテーマ",
open: "I18n Devtool の設定を開く",
panelPosition: "パネル位置",
panelPositionControl: "I18n Devtool のパネル位置",
right: "右",
theme: "テーマ",
themeControl: "I18n Devtool のテーマ",
},
trigger: {
close: "I18n Devtool を閉じる",
open: "I18n Devtool を開く",
},
},
ko: {
actions: {
compile: "컴파일",
compileCompleted: "컴파일이 완료되었습니다.",
compileFailed: "컴파일에 실패했습니다.",
compiling: "컴파일 중…",
enterFullscreen: "전체 화면으로 전환",
exitFullscreen: "전체 화면 종료",
extract: "추출",
extractCompleted: "추출이 완료되었습니다.",
extractFailed: "추출에 실패했습니다.",
extracting: "추출 중…",
label: "작업",
},
commandOutput: {
close: "명령 출력을 닫기",
collapse: "명령 출력을 접기",
expand: "명령 출력을 펼치기",
title: "명령 출력",
},
locale: {
controlLabel: "메시지 대상 언어",
label: "대상 언어",
noTargetLocales: "대상 언어가 설정되지 않았습니다.",
sourceLabel: "원본 언어",
},
messagePanel: {
additionalSourceLocations: (count) => `추가 소스 위치 ${count}`,
chooseCopyContent: "복사할 내용 선택",
collapseNamespace: (path) => `${path} 접기`,
collapsePackage: (packageName) => `${packageName} 접기`,
copied: "복사됨",
copyDescriptor: "메시지 설명자 복사",
copyFailed: "복사하지 못했습니다",
copyMessageId: "메시지 ID 복사",
copySource: "원문 복사",
copySourceLocation: "소스 위치 복사",
copyTranslation: "현재 번역 복사",
couldNotSave: "저장하지 못했습니다",
emptyCatalog:
"아직 사용할 수 있는 메시지가 없습니다. 프로젝트에서 메시지를 추출해 카탈로그를 채우세요.",
emptyPackage: "이 패키지에는 메시지가 없습니다.",
expandNamespace: (path) => `${path} 펼치기`,
expandPackage: (packageName) => `${packageName} 펼치기`,
loading: (locale) => `${locale} 불러오는 중…`,
messageCount: (count) => `${count}개 메시지`,
messageEditor: "메시지 편집기",
messageNavigation: "메시지 탐색",
missingCount: (count) => `${count}개 누락`,
missingOnly: "누락된 항목만",
noMatches: "현재 필터와 일치하는 메시지가 없습니다.",
noMissingMessages: "모든 메시지가 번역되었습니다.",
noSearchResults: "검색어와 일치하는 메시지가 없습니다.",
reset: "재설정",
retry: "다시 시도",
save: "저장",
saved: "저장됨",
saving: "저장 중…",
search: "메시지 검색",
selectMessage: "번역을 확인하고 편집할 메시지를 선택하세요.",
source: "원문",
sourceLocations: "소스 위치",
translation: "번역",
translationFor: (id, locale) => `${id}${locale} 번역`,
unknownSourceLocation: "알 수 없는 소스 위치",
},
settings: {
auto: "자동",
autoTheme: "자동 테마",
center: "가운데",
dark: "다크",
darkTheme: "다크 테마",
interfaceLanguage: "인터페이스 언어",
interfaceLanguageControl: "I18n Devtool 인터페이스 언어",
label: "I18n Devtool 설정",
left: "왼쪽",
light: "라이트",
lightTheme: "라이트 테마",
open: "I18n Devtool 설정 열기",
panelPosition: "패널 위치",
panelPositionControl: "I18n Devtool 패널 위치",
right: "오른쪽",
theme: "테마",
themeControl: "I18n Devtool 테마",
},
trigger: {
close: "I18n Devtool 닫기",
open: "I18n Devtool 열기",
},
},
"zh-Hans": { "zh-Hans": {
actions: { actions: {
compile: "编译", compile: "编译",
@@ -738,103 +275,14 @@ const messagesByLocale: Record<I18nDevtoolLocale, DevtoolMessages> = {
open: "打开 I18n 开发工具", open: "打开 I18n 开发工具",
}, },
}, },
"zh-Hant": {
actions: {
compile: "編譯",
compileCompleted: "編譯完成。",
compileFailed: "編譯失敗。",
compiling: "正在編譯…",
enterFullscreen: "進入全螢幕",
exitFullscreen: "退出全螢幕",
extract: "擷取",
extractCompleted: "擷取完成。",
extractFailed: "擷取失敗。",
extracting: "正在擷取…",
label: "操作",
},
commandOutput: {
close: "關閉命令輸出",
collapse: "收合命令輸出",
expand: "展開命令輸出",
title: "命令輸出",
},
locale: {
controlLabel: "訊息目標語言",
label: "目標語言",
noTargetLocales: "尚未設定目標語言。",
sourceLabel: "來源語言",
},
messagePanel: {
additionalSourceLocations: (count) => `另外 ${count} 個來源位置`,
chooseCopyContent: "選擇要複製的內容",
collapseNamespace: (path) => `收合 ${path}`,
collapsePackage: (packageName) => `收合 ${packageName}`,
copied: "已複製",
copyDescriptor: "複製 Message Descriptor",
copyFailed: "複製失敗",
copyMessageId: "複製 Message ID",
copySource: "複製原文",
copySourceLocation: "複製來源位置",
copyTranslation: "複製目前譯文",
couldNotSave: "儲存失敗",
emptyCatalog: "尚無可用訊息。請先擷取專案訊息以產生目錄。",
emptyPackage: "這個套件中暫無訊息。",
expandNamespace: (path) => `展開 ${path}`,
expandPackage: (packageName) => `展開 ${packageName}`,
loading: (locale) => `正在載入 ${locale}`,
messageCount: (count) => `${count}`,
messageEditor: "訊息編輯器",
messageNavigation: "訊息導覽",
missingCount: (count) => `缺漏 ${count}`,
missingOnly: "僅顯示缺漏項目",
noMatches: "沒有符合目前篩選條件的訊息。",
noMissingMessages: "所有訊息皆已完成翻譯。",
noSearchResults: "沒有符合搜尋內容的訊息。",
reset: "重設",
retry: "重試",
save: "儲存",
saved: "已儲存",
saving: "正在儲存…",
search: "搜尋訊息",
selectMessage: "請選擇一則訊息以檢視和編輯譯文。",
source: "原文",
sourceLocations: "來源位置",
translation: "譯文",
translationFor: (id, locale) => `${id}${locale} 翻譯`,
unknownSourceLocation: "未知來源位置",
},
settings: {
auto: "自動",
autoTheme: "自動主題",
center: "置中",
dark: "深色",
darkTheme: "深色主題",
interfaceLanguage: "介面語言",
interfaceLanguageControl: "I18n 開發工具介面語言",
label: "I18n 開發工具設定",
left: "左側",
light: "淺色",
lightTheme: "淺色主題",
open: "開啟 I18n 開發工具設定",
panelPosition: "面板位置",
panelPositionControl: "I18n 開發工具面板位置",
right: "右側",
theme: "主題",
themeControl: "I18n 開發工具主題",
},
trigger: {
close: "關閉 I18n 開發工具",
open: "開啟 I18n 開發工具",
},
},
} }
const defaultContextValue: { const defaultContextValue: {
locale: I18nDevtoolLocale locale: I18nDevtoolLocale
messages: DevtoolMessages messages: DevtoolMessages
} = { } = {
locale: "en", locale: "en-US",
messages: messagesByLocale.en, messages: messagesByLocale["en-US"],
} }
const DevtoolLocalizationContext = React.createContext(defaultContextValue) const DevtoolLocalizationContext = React.createContext(defaultContextValue)
@@ -847,30 +295,22 @@ function matchDevtoolLocale(locale: string): I18nDevtoolLocale | undefined {
} }
if ( if (
normalizedLocale === "zh-hant" || normalizedLocale === "zh" ||
normalizedLocale.startsWith("zh-hant-") normalizedLocale === "zh-hans" ||
normalizedLocale.startsWith("zh-hans-") ||
normalizedLocale === "zh-cn" ||
normalizedLocale.startsWith("zh-cn-") ||
normalizedLocale === "zh-sg" ||
normalizedLocale.startsWith("zh-sg-")
) { ) {
return "zh-Hant"
}
if (
normalizedLocale === "zh-tw" ||
normalizedLocale.startsWith("zh-tw-") ||
normalizedLocale === "zh-hk" ||
normalizedLocale.startsWith("zh-hk-") ||
normalizedLocale === "zh-mo" ||
normalizedLocale.startsWith("zh-mo-")
) {
return "zh-Hant"
}
if (normalizedLocale === "zh" || normalizedLocale.startsWith("zh-")) {
return "zh-Hans" return "zh-Hans"
} }
const language = normalizedLocale.split("-")[0] if (normalizedLocale === "en" || normalizedLocale.startsWith("en-")) {
return "en-US"
}
return I18N_DEVTOOL_LOCALES.find((candidate) => candidate === language) return undefined
} }
function getNavigatorLocales() { function getNavigatorLocales() {
@@ -899,7 +339,7 @@ export function resolveI18nDevtoolLocale(
} }
} }
return "en" return "en-US"
} }
export function getI18nDevtoolMessages(locale: I18nDevtoolLocale) { export function getI18nDevtoolMessages(locale: I18nDevtoolLocale) {
+13 -13
View File
@@ -24,7 +24,7 @@ afterEach(() => {
function renderDevtool(props?: I18nDevtoolProps) { function renderDevtool(props?: I18nDevtoolProps) {
return render( return render(
<I18nProvider catalogs={{ en: {} }} locale="en" locales={["en"]}> <I18nProvider catalogs={{ "en-US": {} }} locale="en-US" locales={["en-US"]}>
<I18nDevtool dark="ui:dark" {...props} /> <I18nDevtool dark="ui:dark" {...props} />
</I18nProvider> </I18nProvider>
) )
@@ -87,17 +87,17 @@ describe("I18nDevtool", () => {
it("detects the first supported navigator locale", async () => { it("detects the first supported navigator locale", async () => {
vi.spyOn(window.navigator, "languages", "get").mockReturnValue([ vi.spyOn(window.navigator, "languages", "get").mockReturnValue([
"pt-BR", "pt-BR",
"ja-JP", "fr-CA",
"en-US", "en-US",
]) ])
renderDevtool() renderDevtool()
const trigger = await screen.findByRole("button", { const trigger = await screen.findByRole("button", {
name: "I18n Devtool を開く", name: "Open I18n Devtool",
}) })
const devtool = trigger.closest<HTMLElement>('[data-slot="i18n-devtool"]') const devtool = trigger.closest<HTMLElement>('[data-slot="i18n-devtool"]')
expect(devtool?.getAttribute("lang")).toBe("ja") expect(devtool?.getAttribute("lang")).toBe("en-US")
}) })
it("falls back to English when an explicit locale is unsupported", async () => { it("falls back to English when an explicit locale is unsupported", async () => {
@@ -109,7 +109,7 @@ describe("I18nDevtool", () => {
}) })
const devtool = trigger.closest<HTMLElement>('[data-slot="i18n-devtool"]') const devtool = trigger.closest<HTMLElement>('[data-slot="i18n-devtool"]')
expect(devtool?.getAttribute("lang")).toBe("en") expect(devtool?.getAttribute("lang")).toBe("en-US")
}) })
it("includes the project source locale as a customization target", async () => { it("includes the project source locale as a customization target", async () => {
@@ -118,7 +118,7 @@ describe("I18nDevtool", () => {
vi.fn(async (input: RequestInfo | URL) => { vi.fn(async (input: RequestInfo | URL) => {
const url = String(input) const url = String(input)
const body = url.endsWith("/project") const body = url.endsWith("/project")
? { locales: ["en", "zh-Hans"], sourceLocale: "en" } ? { locales: ["en-US", "zh-Hans"], sourceLocale: "en-US" }
: [] : []
return new Response(JSON.stringify(body), { return new Response(JSON.stringify(body), {
@@ -130,10 +130,10 @@ describe("I18nDevtool", () => {
render( render(
<I18nProvider <I18nProvider
catalogs={{ en: {}, "zh-Hans": {} }} catalogs={{ "en-US": {}, "zh-Hans": {} }}
locale="en" locale="en-US"
locales={[ locales={[
{ label: "English", locale: "en" }, { label: "English", locale: "en-US" },
{ label: "简体中文", locale: "zh-Hans" }, { label: "简体中文", locale: "zh-Hans" },
]} ]}
> >
@@ -146,7 +146,7 @@ describe("I18nDevtool", () => {
}) })
await waitFor(() => { await waitFor(() => {
expect(localeSelect).toHaveProperty("value", "en") expect(localeSelect).toHaveProperty("value", "en-US")
}) })
expect(screen.getByText("Source locale")).toBeTruthy() expect(screen.getByText("Source locale")).toBeTruthy()
expect(screen.getByText("English", { selector: "output" })).toBeTruthy() expect(screen.getByText("English", { selector: "output" })).toBeTruthy()
@@ -157,9 +157,9 @@ describe("I18nDevtool", () => {
it("tracks the configured document dark-mode class", async () => { it("tracks the configured document dark-mode class", async () => {
render( render(
<I18nProvider <I18nProvider
catalogs={{ en: {} }} catalogs={{ "en-US": {} }}
locale="en" locale="en-US"
locales={["en", "zh-Hans"]} locales={["en-US", "zh-Hans"]}
> >
<I18nDevtool dark={"ui\\:dark"} /> <I18nDevtool dark={"ui\\:dark"} />
</I18nProvider> </I18nProvider>
@@ -24,7 +24,7 @@ const message: DevtoolMessage = {
origins: [{ file: "src/navigation.tsx", line: 12 }], origins: [{ file: "src/navigation.tsx", line: 12 }],
packageName: "@workspace/ui", packageName: "@workspace/ui",
source: "Home", source: "Home",
sourceLocale: "en", sourceLocale: "en-US",
translation: "", translation: "",
} }
@@ -310,10 +310,10 @@ describe("MessagePanel", () => {
updateMessage, updateMessage,
} }
render(<MessagePanel locale="en" repository={repository} />) render(<MessagePanel locale="en-US" repository={repository} />)
const textarea = await screen.findByLabelText( const textarea = await screen.findByLabelText(
"navigation.home translation for en" "navigation.home translation for en-US"
) )
expect(textarea).toHaveProperty("readOnly", true) expect(textarea).toHaveProperty("readOnly", true)
+2 -3
View File
@@ -62,14 +62,13 @@ function normalizeLocales(
const definition = typeof input === "string" ? { locale: input } : input const definition = typeof input === "string" ? { locale: input } : input
const languageTag = definition.languageTag ?? definition.locale const languageTag = definition.languageTag ?? definition.locale
return { return Object.assign({}, definition, {
...definition,
direction: definition.direction ?? inferLocaleDirection(languageTag), direction: definition.direction ?? inferLocaleDirection(languageTag),
label: label:
definition.label ?? definition.label ??
getLocaleDisplayName(languageTag, definition.locale), getLocaleDisplayName(languageTag, definition.locale),
languageTag, languageTag,
} })
}) })
if (!definitions.some((definition) => definition.locale === locale)) { if (!definitions.some((definition) => definition.locale === locale)) {
+45 -45
View File
@@ -17,7 +17,7 @@ import { LocalizedText } from "./localized-text"
import { Translate } from "./translate" import { Translate } from "./translate"
const catalogs = { const catalogs = {
en: { "en-US": {
greeting: "Hello", greeting: "Hello",
}, },
"zh-Hans": { "zh-Hans": {
@@ -39,6 +39,43 @@ function HookProbe() {
) )
} }
function DirectionProbe() {
return <span>{useLocales()[0]?.direction}</span>
}
function LocaleLabelsProbe() {
return (
<span>
{useLocales()
.map((locale) => locale.label)
.join("|")}
</span>
)
}
function LocaleProbe() {
const locale = useLocaleDefinition()
return (
<span>
{locale.locale}|{locale.languageTag}
</span>
)
}
function FormatterProbe() {
const formatters = useFormatters()
return (
<output>
{formatters.formatNumber(1234.5)}|
{formatters.formatCurrency(1234.5, "USD")}|
{formatters.formatList(["A", "B", "C"])}|
{formatters.formatRelativeTime(-1, "day", { numeric: "auto" })}
</output>
)
}
describe("i18n runtime", () => { describe("i18n runtime", () => {
it("provides locale information and reactive translation helpers", () => { it("provides locale information and reactive translation helpers", () => {
render( render(
@@ -46,7 +83,7 @@ describe("i18n runtime", () => {
catalogs={catalogs} catalogs={catalogs}
locale="zh-Hans" locale="zh-Hans"
locales={[ locales={[
{ label: "English", locale: "en" }, { label: "English", locale: "en-US" },
{ label: "简体中文", locale: "zh-Hans" }, { label: "简体中文", locale: "zh-Hans" },
]} ]}
> >
@@ -54,7 +91,7 @@ describe("i18n runtime", () => {
</I18nProvider> </I18nProvider>
) )
expect(screen.getByText("zh-Hans|en,zh-Hans|你好|你好")).toBeTruthy() expect(screen.getByText("zh-Hans|en-US,zh-Hans|你好|你好")).toBeTruthy()
}) })
it("merges package catalogs and lets application catalogs override them", () => { it("merges package catalogs and lets application catalogs override them", () => {
@@ -87,7 +124,7 @@ describe("i18n runtime", () => {
it("wraps Lingui Trans without adding a DOM element", () => { it("wraps Lingui Trans without adding a DOM element", () => {
const { container } = render( const { container } = render(
<I18nProvider catalogs={catalogs} locale="en"> <I18nProvider catalogs={catalogs} locale="en-US">
<p> <p>
<Translate id="greeting" message="Fallback" /> <Translate id="greeting" message="Fallback" />
</p> </p>
@@ -110,10 +147,6 @@ describe("i18n runtime", () => {
}) })
it("infers right-to-left locale direction", () => { it("infers right-to-left locale direction", () => {
function DirectionProbe() {
return <span>{useLocales()[0]?.direction}</span>
}
render( render(
<I18nProvider locale="ar" locales={["ar"]}> <I18nProvider locale="ar" locales={["ar"]}>
<DirectionProbe /> <DirectionProbe />
@@ -124,36 +157,16 @@ describe("i18n runtime", () => {
}) })
it("uses localized language names when locale labels are omitted", () => { it("uses localized language names when locale labels are omitted", () => {
function LocaleLabelsProbe() {
return (
<span>
{useLocales()
.map((locale) => locale.label)
.join("|")}
</span>
)
}
render( render(
<I18nProvider locale="en" locales={["en", "zh-Hans"]}> <I18nProvider locale="en-US" locales={["en-US", "zh-Hans"]}>
<LocaleLabelsProbe /> <LocaleLabelsProbe />
</I18nProvider> </I18nProvider>
) )
expect(screen.getByText("English|简体中文")).toBeTruthy() expect(screen.getByText("American English|简体中文")).toBeTruthy()
}) })
it("keeps catalog locales separate from Intl language tags", () => { it("keeps catalog locales separate from Intl language tags", () => {
function LocaleProbe() {
const locale = useLocaleDefinition()
return (
<span>
{locale.locale}|{locale.languageTag}
</span>
)
}
render( render(
<I18nProvider <I18nProvider
locale="zh-Hans" locale="zh-Hans"
@@ -167,23 +180,10 @@ describe("i18n runtime", () => {
}) })
it("formats standard internationalized values through Intl", () => { it("formats standard internationalized values through Intl", () => {
function FormatterProbe() {
const formatters = useFormatters()
return (
<output>
{formatters.formatNumber(1234.5)}|
{formatters.formatCurrency(1234.5, "USD")}|
{formatters.formatList(["A", "B", "C"])}|
{formatters.formatRelativeTime(-1, "day", { numeric: "auto" })}
</output>
)
}
render( render(
<I18nProvider <I18nProvider
locale="en" locale="en-US"
locales={[{ languageTag: "en-US", locale: "en" }]} locales={[{ languageTag: "en-US", locale: "en-US" }]}
> >
<FormatterProbe /> <FormatterProbe />
</I18nProvider> </I18nProvider>
+2
View File
@@ -139,6 +139,8 @@ ${createCatalogLoaderRuntime(`(async () => {
function replaceMessageIds(code: string, schema: MessageIdSchema) { function replaceMessageIds(code: string, schema: MessageIdSchema) {
let transformedCode = code let transformedCode = code
// ES2022 compatibility: Object.entries returns a fresh array.
// oxlint-disable-next-line unicorn/no-array-sort
for (const [id, compactId] of Object.entries(schema.messages).sort( for (const [id, compactId] of Object.entries(schema.messages).sort(
([left], [right]) => right.length - left.length ([left], [right]) => right.length - left.length
)) { )) {
@@ -1,16 +1,19 @@
import { describe, expect, it } from "vitest" import { describe, expect, it } from "vitest"
import { lexicalMessages } from "../messages" import { lexicalMessages } from "../messages"
import { messages as en } from "./en" import { messages as enUS } from "./en-US"
import { messages as zhHans } from "./zh-Hans" import { messages as zhHans } from "./zh-Hans"
describe("Lexical locale catalogs", () => { describe("Lexical locale catalogs", () => {
it("ships every built-in message in every locale", () => { it("ships every built-in message in every locale", () => {
const messageIds = Object.values(lexicalMessages) const messageIds = Object.values(lexicalMessages).map(
.map((descriptor) => descriptor.id) (descriptor) => descriptor.id
.sort() )
messageIds.sort()
for (const messages of [en, zhHans]) { for (const messages of [enUS, zhHans]) {
// Object.keys returns a fresh array.
// oxlint-disable-next-line unicorn/no-array-sort
expect(Object.keys(messages).sort()).toEqual(messageIds) expect(Object.keys(messages).sort()).toEqual(messageIds)
} }
}) })
+1 -1
View File
@@ -1,6 +1,6 @@
import type { lexicalMessages } from "../messages" import type { lexicalMessages } from "../messages"
export const lexicalCatalogLocales = ["en", "zh-Hans"] as const export const lexicalCatalogLocales = ["en-US", "zh-Hans"] as const
type LexicalMessageId = type LexicalMessageId =
(typeof lexicalMessages)[keyof typeof lexicalMessages]["id"] (typeof lexicalMessages)[keyof typeof lexicalMessages]["id"]
@@ -1,6 +1,6 @@
import type { LexicalMessageCatalog } from "./catalogs" import type { LexicalMessageCatalog } from "./catalogs"
export const locale = "en" export const locale = "en-US"
export const languageTag = "en-US" export const languageTag = "en-US"
export const messages = { export const messages = {
+2 -2
View File
@@ -16,7 +16,7 @@ import {
findLexicalActions, findLexicalActions,
} from "./action-declarations" } from "./action-declarations"
import { LexicalActionsProvider } from "./actions-context" import { LexicalActionsProvider } from "./actions-context"
import { messages as englishMessages } from "./locales/en" import { messages as englishMessages } from "./locales/en-US"
const pluginKeys = new WeakMap<ComponentType, string>() const pluginKeys = new WeakMap<ComponentType, string>()
let nextPluginKey = 0 let nextPluginKey = 0
@@ -46,7 +46,7 @@ function LexicalI18nBoundary({ children }: { children: ReactNode }) {
if (hasI18nProvider) return children if (hasI18nProvider) return children
return ( return (
<I18nProvider catalogs={{ en: englishMessages }} locale="en"> <I18nProvider catalogs={{ "en-US": englishMessages }} locale="en-US">
{children} {children}
</I18nProvider> </I18nProvider>
) )
+1 -10
View File
@@ -1,13 +1,4 @@
export const uiCatalogLocales = [ export const uiCatalogLocales = ["en-US", "zh-Hans"] as const
"de",
"en",
"es",
"fr",
"ja",
"ko",
"zh-Hans",
"zh-Hant",
] as const
export const uiMessageIds = [ export const uiMessageIds = [
"ui.breadcrumb.label", "ui.breadcrumb.label",
-25
View File
@@ -1,25 +0,0 @@
export { de as calendarLocale } from "react-day-picker/locale/de"
import type { UiMessageCatalog } from "../catalogs"
export const locale = "de"
export const languageTag = "de-DE"
export const messages = {
"ui.breadcrumb.label": "Breadcrumb-Navigation",
"ui.carousel.nextSlide": "Nächste Folie",
"ui.carousel.previousSlide": "Vorherige Folie",
"ui.common.close": "Schließen",
"ui.common.loading": "Wird geladen",
"ui.common.more": "Mehr",
"ui.pagination.label": "Seitennavigation",
"ui.pagination.more": "Weitere Seiten",
"ui.pagination.next": "Weiter",
"ui.pagination.nextPage": "Zur nächsten Seite",
"ui.pagination.previous": "Zurück",
"ui.pagination.previousPage": "Zur vorherigen Seite",
"ui.sidebar.description": "Zeigt die mobile Seitenleiste an.",
"ui.sidebar.title": "Seitenleiste",
"ui.sidebar.toggle": "Seitenleiste umschalten",
"ui.toast.close": "Benachrichtigung schließen",
} as const satisfies UiMessageCatalog
@@ -2,7 +2,7 @@ export { enUS as calendarLocale } from "react-day-picker/locale/en-US"
import type { UiMessageCatalog } from "../catalogs" import type { UiMessageCatalog } from "../catalogs"
export const locale = "en" export const locale = "en-US"
export const languageTag = "en-US" export const languageTag = "en-US"
export const messages = { export const messages = {
-25
View File
@@ -1,25 +0,0 @@
export { es as calendarLocale } from "react-day-picker/locale/es"
import type { UiMessageCatalog } from "../catalogs"
export const locale = "es"
export const languageTag = "es-ES"
export const messages = {
"ui.breadcrumb.label": "Ruta de navegación",
"ui.carousel.nextSlide": "Diapositiva siguiente",
"ui.carousel.previousSlide": "Diapositiva anterior",
"ui.common.close": "Cerrar",
"ui.common.loading": "Cargando",
"ui.common.more": "Más",
"ui.pagination.label": "Paginación",
"ui.pagination.more": "Más páginas",
"ui.pagination.next": "Siguiente",
"ui.pagination.nextPage": "Ir a la página siguiente",
"ui.pagination.previous": "Anterior",
"ui.pagination.previousPage": "Ir a la página anterior",
"ui.sidebar.description": "Muestra la barra lateral móvil.",
"ui.sidebar.title": "Barra lateral",
"ui.sidebar.toggle": "Alternar barra lateral",
"ui.toast.close": "Cerrar notificación",
} as const satisfies UiMessageCatalog
-25
View File
@@ -1,25 +0,0 @@
export { fr as calendarLocale } from "react-day-picker/locale/fr"
import type { UiMessageCatalog } from "../catalogs"
export const locale = "fr"
export const languageTag = "fr-FR"
export const messages = {
"ui.breadcrumb.label": "Fil dAriane",
"ui.carousel.nextSlide": "Diapositive suivante",
"ui.carousel.previousSlide": "Diapositive précédente",
"ui.common.close": "Fermer",
"ui.common.loading": "Chargement",
"ui.common.more": "Plus",
"ui.pagination.label": "Pagination",
"ui.pagination.more": "Plus de pages",
"ui.pagination.next": "Suivant",
"ui.pagination.nextPage": "Aller à la page suivante",
"ui.pagination.previous": "Précédent",
"ui.pagination.previousPage": "Aller à la page précédente",
"ui.sidebar.description": "Affiche la barre latérale mobile.",
"ui.sidebar.title": "Barre latérale",
"ui.sidebar.toggle": "Afficher ou masquer la barre latérale",
"ui.toast.close": "Fermer la notification",
} as const satisfies UiMessageCatalog
-25
View File
@@ -1,25 +0,0 @@
export { ja as calendarLocale } from "react-day-picker/locale/ja"
import type { UiMessageCatalog } from "../catalogs"
export const locale = "ja"
export const languageTag = "ja-JP"
export const messages = {
"ui.breadcrumb.label": "パンくずリスト",
"ui.carousel.nextSlide": "次のスライド",
"ui.carousel.previousSlide": "前のスライド",
"ui.common.close": "閉じる",
"ui.common.loading": "読み込み中",
"ui.common.more": "その他",
"ui.pagination.label": "ページネーション",
"ui.pagination.more": "その他のページ",
"ui.pagination.next": "次へ",
"ui.pagination.nextPage": "次のページへ移動",
"ui.pagination.previous": "前へ",
"ui.pagination.previousPage": "前のページへ移動",
"ui.sidebar.description": "モバイルサイドバーを表示します。",
"ui.sidebar.title": "サイドバー",
"ui.sidebar.toggle": "サイドバーを切り替える",
"ui.toast.close": "通知を閉じる",
} as const satisfies UiMessageCatalog
-25
View File
@@ -1,25 +0,0 @@
export { ko as calendarLocale } from "react-day-picker/locale/ko"
import type { UiMessageCatalog } from "../catalogs"
export const locale = "ko"
export const languageTag = "ko-KR"
export const messages = {
"ui.breadcrumb.label": "이동 경로",
"ui.carousel.nextSlide": "다음 슬라이드",
"ui.carousel.previousSlide": "이전 슬라이드",
"ui.common.close": "닫기",
"ui.common.loading": "불러오는 중",
"ui.common.more": "더 보기",
"ui.pagination.label": "페이지 탐색",
"ui.pagination.more": "더 많은 페이지",
"ui.pagination.next": "다음",
"ui.pagination.nextPage": "다음 페이지로 이동",
"ui.pagination.previous": "이전",
"ui.pagination.previousPage": "이전 페이지로 이동",
"ui.sidebar.description": "모바일 사이드바를 표시합니다.",
"ui.sidebar.title": "사이드바",
"ui.sidebar.toggle": "사이드바 전환",
"ui.toast.close": "알림 닫기",
} as const satisfies UiMessageCatalog
-25
View File
@@ -1,25 +0,0 @@
export { zhTW as calendarLocale } from "react-day-picker/locale/zh-TW"
import type { UiMessageCatalog } from "../catalogs"
export const locale = "zh-Hant"
export const languageTag = "zh-TW"
export const messages = {
"ui.breadcrumb.label": "麵包屑導覽",
"ui.carousel.nextSlide": "下一張",
"ui.carousel.previousSlide": "上一張",
"ui.common.close": "關閉",
"ui.common.loading": "載入中",
"ui.common.more": "更多",
"ui.pagination.label": "分頁導覽",
"ui.pagination.more": "更多頁面",
"ui.pagination.next": "下一頁",
"ui.pagination.nextPage": "前往下一頁",
"ui.pagination.previous": "上一頁",
"ui.pagination.previousPage": "前往上一頁",
"ui.sidebar.description": "顯示行動版側欄。",
"ui.sidebar.title": "側欄",
"ui.sidebar.toggle": "切換側欄",
"ui.toast.close": "關閉通知",
} as const satisfies UiMessageCatalog