Compare commits

..

5 Commits

Author SHA1 Message Date
Maofeng 8cf3a1f03a feat(web): integrate localized application catalogs
- compose app, UI, and independently imported block catalogs from JSON configuration\n- persist the selected locale on the server and activate it without hydration flicker\n- add a flag-based language switcher and localized navigation descriptors\n- mount the redesigned i18n devtool only in development\n- regenerate English and Simplified Chinese catalogs and add switcher coverage
2026-07-30 14:48:25 +08:00
Maofeng 6af4d7b798 feat(blocks): localize reusable application shell blocks
- move block messages beside appearance, chats, layout, navigation, and notification features\n- publish independently tree-shakeable catalogs for eight locales per block\n- localize controllers and leaf labels without rerendering surrounding interactive components\n- support descriptor-backed navigation labels, translated breadcrumbs, and compact-label truncation\n- add catalog composition helpers and coverage for reusable block consumers
2026-07-30 14:48:06 +08:00
Maofeng ae1798f6f4 feat(ui): add framework-neutral localization support
- add a lightweight UI i18n provider with translation and locale contexts\n- localize accessibility labels across breadcrumb, carousel, dialog, pagination, sheet, sidebar, spinner, and toast components\n- integrate calendar locale data without coupling UI components to Lingui\n- publish tree-shakeable catalogs for eight common locales
2026-07-30 14:47:51 +08:00
Maofeng 0768e8fa29 feat(i18n): expand catalog runtime and developer tooling
- add Intl-backed locale definitions, formatters, catalog loaders, and runtime hooks\n- compile hashed production catalogs from package-scoped catalog sources\n- drive Lingui extraction and compilation directly from project JSON config\n- redesign the devtool with localized controls, draggable persistence, settings, navigation, copy actions, ANSI output, and fullscreen support\n- extend repository APIs, CLI coverage, documentation, and runtime tests
2026-07-30 14:47:42 +08:00
Maofeng 0d0aad1d47 chore(deps): add localization support packages
- add ANSI rendering and icon dependencies for the i18n devtool\n- add flag, calendar, and jsdom dependencies for the web locale experience\n- connect reusable blocks to the shared i18n runtime\n- refresh the Bun lockfile
2026-07-30 14:47:26 +08:00
179 changed files with 12372 additions and 888 deletions
+3
View File
@@ -19,6 +19,9 @@ dist-ssr
# turbo
.turbo
# generated i18n build schema
.i18n
# Editor directories and files
.vscode/*
!.vscode/extensions.json
+8
View File
@@ -2,6 +2,14 @@
"sourceLocale": "en",
"locales": ["en", "zh-Hans"],
"catalogPath": "src/locales/{locale}/messages",
"catalogSources": [
"@workspace/ui/locales/{locale}",
"@workspace/blocks/appearance/locales/{locale}",
"@workspace/blocks/chats/locales/{locale}",
"@workspace/blocks/layout/locales/{locale}",
"@workspace/blocks/navigation/locales/{locale}",
"@workspace/blocks/notifications/locales/{locale}"
],
"include": ["src", "../../packages/blocks/src", "../../packages/ui/src"],
"exclude": ["**/*.test.{ts,tsx}", "**/node_modules/**"]
}
-4
View File
@@ -1,4 +0,0 @@
import project from "./i18n.config.json" with { type: "json" }
import { createLinguiConfig } from "@workspace/i18n/lingui-config"
export default createLinguiConfig(project)
+4
View File
@@ -11,6 +11,7 @@
"i18n:compile": "workspace-i18n compile",
"i18n:extract": "workspace-i18n extract",
"i18n:ui": "workspace-i18n ui",
"test": "vitest run",
"typecheck": "tsc -b",
"preview": "vite preview",
"start": "node .output/server/index.mjs"
@@ -31,8 +32,10 @@
"@workspace/blocks": "workspace:*",
"@workspace/i18n": "workspace:*",
"@workspace/ui": "workspace:*",
"country-flag-icons": "^1.6.20",
"lucide-react": "^1.27.0",
"react": "^19.2.6",
"react-day-picker": "^10.0.1",
"react-dom": "^19.2.6",
"sonner": "^2.0.7"
},
@@ -43,6 +46,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^6",
"jsdom": "^30.0.1",
"nitro": "^3.0.260610-beta",
"typescript": "~6",
"vite": "^8",
+336
View File
@@ -0,0 +1,336 @@
import type { MessageDescriptor } from "@workspace/i18n"
export const navigationMessages = {
overview: /* i18n */ {
id: "app.navigation.overview",
message: "Overview",
},
"overview-dashboard": /* i18n */ {
id: "app.navigation.overview-dashboard",
message: "Business overview",
},
"overview-realtime": /* i18n */ {
id: "app.navigation.overview-realtime",
message: "Real-time monitoring",
},
"overview-tasks": /* i18n */ {
id: "app.navigation.overview-tasks",
message: "Tasks",
},
"catalog-and-inventory": /* i18n */ {
id: "app.navigation.catalog-and-inventory",
message: "Products and inventory",
},
"catalog-products": /* i18n */ {
id: "app.navigation.catalog-products",
message: "Products",
},
"catalog-products-list": /* i18n */ {
id: "app.navigation.catalog-products-list",
message: "Product list",
},
"catalog-products-drafts": /* i18n */ {
id: "app.navigation.catalog-products-drafts",
message: "Product drafts",
},
"catalog-products-bundles": /* i18n */ {
id: "app.navigation.catalog-products-bundles",
message: "Product bundles",
},
"catalog-products-attributes": /* i18n */ {
id: "app.navigation.catalog-products-attributes",
message: "Product attributes",
},
"catalog-product-specifications": /* i18n */ {
id: "app.navigation.catalog-product-specifications",
message: "Specification templates",
},
"catalog-product-brands": /* i18n */ {
id: "app.navigation.catalog-product-brands",
message: "Brands",
},
"catalog-categories": /* i18n */ {
id: "app.navigation.catalog-categories",
message: "Categories",
},
"catalog-category-tree": /* i18n */ {
id: "app.navigation.catalog-category-tree",
message: "Category tree",
},
"catalog-category-navigation": /* i18n */ {
id: "app.navigation.catalog-category-navigation",
message: "Navigation categories",
},
"inventory-center": /* i18n */ {
id: "app.navigation.inventory-center",
message: "Inventory",
},
"inventory-overview": /* i18n */ {
id: "app.navigation.inventory-overview",
message: "Inventory overview",
},
"inventory-inbound": /* i18n */ {
id: "app.navigation.inventory-inbound",
message: "Inbound orders",
},
"inventory-outbound": /* i18n */ {
id: "app.navigation.inventory-outbound",
message: "Outbound orders",
},
"inventory-transfers": /* i18n */ {
id: "app.navigation.inventory-transfers",
message: "Stock transfers",
},
"inventory-alerts": /* i18n */ {
id: "app.navigation.inventory-alerts",
message: "Inventory alerts",
},
"content-assets": /* i18n */ {
id: "app.navigation.content-assets",
message: "Content",
},
"content-articles": /* i18n */ {
id: "app.navigation.content-articles",
message: "Articles",
},
"content-media": /* i18n */ {
id: "app.navigation.content-media",
message: "Media",
},
"content-pages": /* i18n */ {
id: "app.navigation.content-pages",
message: "Custom pages",
},
"trade-and-fulfillment": /* i18n */ {
id: "app.navigation.trade-and-fulfillment",
message: "Orders and fulfillment",
},
"trade-orders": /* i18n */ {
id: "app.navigation.trade-orders",
message: "Orders",
},
"trade-orders-awaiting-payment": /* i18n */ {
id: "app.navigation.trade-orders-awaiting-payment",
message: "Awaiting payment",
},
"trade-orders-awaiting-shipment": /* i18n */ {
id: "app.navigation.trade-orders-awaiting-shipment",
message: "Awaiting shipment",
},
"trade-orders-shipped": /* i18n */ {
id: "app.navigation.trade-orders-shipped",
message: "In transit",
},
"trade-orders-completed": /* i18n */ {
id: "app.navigation.trade-orders-completed",
message: "Completed orders",
},
"trade-orders-canceled": /* i18n */ {
id: "app.navigation.trade-orders-canceled",
message: "Canceled orders",
},
"fulfillment-center": /* i18n */ {
id: "app.navigation.fulfillment-center",
message: "Fulfillment",
},
"fulfillment-deliveries": /* i18n */ {
id: "app.navigation.fulfillment-deliveries",
message: "Delivery tasks",
},
"fulfillment-pickups": /* i18n */ {
id: "app.navigation.fulfillment-pickups",
message: "Pickup verification",
},
"fulfillment-freight-rules": /* i18n */ {
id: "app.navigation.fulfillment-freight-rules",
message: "Shipping rules",
},
"after-sales-center": /* i18n */ {
id: "app.navigation.after-sales-center",
message: "After-sales",
},
"after-sales-returns": /* i18n */ {
id: "app.navigation.after-sales-returns",
message: "Return requests",
},
"after-sales-refunds": /* i18n */ {
id: "app.navigation.after-sales-refunds",
message: "Refund review",
},
"after-sales-disputes": /* i18n */ {
id: "app.navigation.after-sales-disputes",
message: "Transaction disputes",
},
"customers-and-marketing": /* i18n */ {
id: "app.navigation.customers-and-marketing",
message: "Customers and marketing",
},
"customer-center": /* i18n */ {
id: "app.navigation.customer-center",
message: "Customers",
},
"customer-profiles": /* i18n */ {
id: "app.navigation.customer-profiles",
message: "Customer profiles",
},
"customer-segments": /* i18n */ {
id: "app.navigation.customer-segments",
message: "Customer segments",
},
"customer-tiers": /* i18n */ {
id: "app.navigation.customer-tiers",
message: "Customer tiers",
},
"customer-risk-list": /* i18n */ {
id: "app.navigation.customer-risk-list",
message: "Risk list",
},
"marketing-campaigns": /* i18n */ {
id: "app.navigation.marketing-campaigns",
message: "Marketing campaigns",
},
"marketing-coupons": /* i18n */ {
id: "app.navigation.marketing-coupons",
message: "Coupons",
},
"marketing-flash-sales": /* i18n */ {
id: "app.navigation.marketing-flash-sales",
message: "Flash sales",
},
"marketing-gift-promotions": /* i18n */ {
id: "app.navigation.marketing-gift-promotions",
message: "Gift promotions",
},
"marketing-referrals": /* i18n */ {
id: "app.navigation.marketing-referrals",
message: "Referral rewards",
},
"loyalty-center": /* i18n */ {
id: "app.navigation.loyalty-center",
message: "Membership benefits",
},
"loyalty-points": /* i18n */ {
id: "app.navigation.loyalty-points",
message: "Points rules",
},
"loyalty-stored-value": /* i18n */ {
id: "app.navigation.loyalty-stored-value",
message: "Stored-value accounts",
},
"loyalty-benefits": /* i18n */ {
id: "app.navigation.loyalty-benefits",
message: "Benefits configuration",
},
"data-and-finance": /* i18n */ {
id: "app.navigation.data-and-finance",
message: "Data and finance",
},
"report-center": /* i18n */ {
id: "app.navigation.report-center",
message: "Reports",
},
"report-sales": /* i18n */ {
id: "app.navigation.report-sales",
message: "Sales analysis",
},
"report-products": /* i18n */ {
id: "app.navigation.report-products",
message: "Product analysis",
},
"report-customers": /* i18n */ {
id: "app.navigation.report-customers",
message: "Customer analysis",
},
"report-channels": /* i18n */ {
id: "app.navigation.report-channels",
message: "Channel analysis",
},
"finance-center": /* i18n */ {
id: "app.navigation.finance-center",
message: "Finance",
},
"finance-settlements": /* i18n */ {
id: "app.navigation.finance-settlements",
message: "Settlement statements",
},
"finance-reconciliation": /* i18n */ {
id: "app.navigation.finance-reconciliation",
message: "Reconciliation",
},
"finance-invoices": /* i18n */ {
id: "app.navigation.finance-invoices",
message: "Invoices",
},
"finance-payouts": /* i18n */ {
id: "app.navigation.finance-payouts",
message: "Payout history",
},
"system-management": /* i18n */ {
id: "app.navigation.system-management",
message: "System management",
},
"system-members": /* i18n */ {
id: "app.navigation.system-members",
message: "Organization members",
},
"system-members-admins": /* i18n */ {
id: "app.navigation.system-members-admins",
message: "Administrator accounts",
},
"system-members-roles": /* i18n */ {
id: "app.navigation.system-members-roles",
message: "Roles",
},
"system-members-permissions": /* i18n */ {
id: "app.navigation.system-members-permissions",
message: "Permission policies",
},
"system-notifications": /* i18n */ {
id: "app.navigation.system-notifications",
message: "Notifications",
},
"system-notification-templates": /* i18n */ {
id: "app.navigation.system-notification-templates",
message: "Notification templates",
},
"system-notification-logs": /* i18n */ {
id: "app.navigation.system-notification-logs",
message: "Delivery logs",
},
"system-audit": /* i18n */ {
id: "app.navigation.system-audit",
message: "Audit log",
},
"system-integrations": /* i18n */ {
id: "app.navigation.system-integrations",
message: "Integrations",
},
"system-integration-api-keys": /* i18n */ {
id: "app.navigation.system-integration-api-keys",
message: "API keys",
},
"system-integration-webhooks": /* i18n */ {
id: "app.navigation.system-integration-webhooks",
message: "Webhook configuration",
},
"system-settings": /* i18n */ {
id: "app.navigation.system-settings",
message: "System settings",
},
"system-settings-store": /* i18n */ {
id: "app.navigation.system-settings-store",
message: "Store information",
},
"system-settings-payments": /* i18n */ {
id: "app.navigation.system-settings-payments",
message: "Payment channels",
},
"system-settings-shipping": /* i18n */ {
id: "app.navigation.system-settings-shipping",
message: "Shipping settings",
},
"system-recycle-bin": /* i18n */ {
id: "app.navigation.system-recycle-bin",
message: "Recycle bin",
},
} as const satisfies Record<string, MessageDescriptor>
+85 -83
View File
@@ -60,73 +60,75 @@ import {
import type { NavigationGroup } from "@workspace/blocks/navigation"
import { navigationMessages } from "./navigation-messages"
export const primaryNavigationGroups: readonly NavigationGroup[] = [
{
id: "overview",
label: "概览",
label: navigationMessages["overview"],
items: [
{
id: "overview-dashboard",
to: "/",
label: "经营总览",
label: navigationMessages["overview-dashboard"],
icon: Analytics01Icon,
},
{
id: "overview-realtime",
to: "/analytics/realtime",
label: "实时监控",
label: navigationMessages["overview-realtime"],
icon: AnalyticsUpIcon,
},
{
id: "overview-tasks",
to: "/workspace/tasks",
label: "待办中心",
label: navigationMessages["overview-tasks"],
icon: Task01Icon,
},
],
},
{
id: "catalog-and-inventory",
label: "商品与库存",
label: navigationMessages["catalog-and-inventory"],
items: [
{
id: "catalog-products",
label: "商品中心",
label: navigationMessages["catalog-products"],
icon: ShoppingBag01Icon,
items: [
{
id: "catalog-products-list",
to: "/catalog/products/list",
label: "商品列表",
label: navigationMessages["catalog-products-list"],
icon: ShoppingBag01Icon,
},
{
id: "catalog-products-drafts",
to: "/catalog/products/drafts",
label: "商品草稿",
label: navigationMessages["catalog-products-drafts"],
icon: File01Icon,
},
{
id: "catalog-products-bundles",
to: "/catalog/products/bundles",
label: "组合商品",
label: navigationMessages["catalog-products-bundles"],
icon: Package02Icon,
},
{
id: "catalog-products-attributes",
label: "商品属性",
label: navigationMessages["catalog-products-attributes"],
icon: Settings03Icon,
items: [
{
id: "catalog-product-specifications",
to: "/catalog/products/attributes/specifications",
label: "规格模板",
label: navigationMessages["catalog-product-specifications"],
icon: ClipboardListIcon,
},
{
id: "catalog-product-brands",
to: "/catalog/products/attributes/brands",
label: "品牌资料",
label: navigationMessages["catalog-product-brands"],
icon: StarIcon,
},
],
@@ -136,19 +138,19 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
{
id: "catalog-categories",
to: "/catalog/categories",
label: "分类目录",
label: navigationMessages["catalog-categories"],
icon: ClipboardListIcon,
items: [
{
id: "catalog-category-tree",
to: "/catalog/categories/tree",
label: "分类树",
label: navigationMessages["catalog-category-tree"],
icon: Structure01Icon,
},
{
id: "catalog-category-navigation",
to: "/catalog/categories/navigation",
label: "导航分类",
label: navigationMessages["catalog-category-navigation"],
icon: Compass01Icon,
},
],
@@ -156,37 +158,37 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
{
id: "inventory-center",
to: "/inventory",
label: "库存中心",
label: navigationMessages["inventory-center"],
icon: PackageIcon,
items: [
{
id: "inventory-overview",
to: "/inventory/overview",
label: "库存总览",
label: navigationMessages["inventory-overview"],
icon: Chart01Icon,
},
{
id: "inventory-inbound",
to: "/inventory/inbound",
label: "入库单",
label: navigationMessages["inventory-inbound"],
icon: InboxDownloadIcon,
},
{
id: "inventory-outbound",
to: "/inventory/outbound",
label: "出库单",
label: navigationMessages["inventory-outbound"],
icon: InboxUploadIcon,
},
{
id: "inventory-transfers",
to: "/inventory/transfers",
label: "调拨单",
label: navigationMessages["inventory-transfers"],
icon: ArrowDataTransferHorizontalIcon,
},
{
id: "inventory-alerts",
to: "/inventory/alerts",
label: "库存预警",
label: navigationMessages["inventory-alerts"],
icon: Alert02Icon,
},
],
@@ -194,25 +196,25 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
{
id: "content-assets",
to: "/content",
label: "内容素材",
label: navigationMessages["content-assets"],
icon: File01Icon,
items: [
{
id: "content-articles",
to: "/content/articles",
label: "内容文章",
label: navigationMessages["content-articles"],
icon: File01Icon,
},
{
id: "content-media",
to: "/content/media",
label: "媒体资源",
label: navigationMessages["content-media"],
icon: GalleryHorizontalIcon,
},
{
id: "content-pages",
to: "/content/pages",
label: "自定义页面",
label: navigationMessages["content-pages"],
icon: AppWindowIcon,
},
],
@@ -221,42 +223,42 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
},
{
id: "trade-and-fulfillment",
label: "交易与履约",
label: navigationMessages["trade-and-fulfillment"],
items: [
{
id: "trade-orders",
to: "/trade/orders",
label: "订单中心",
label: navigationMessages["trade-orders"],
icon: ShoppingCart01Icon,
items: [
{
id: "trade-orders-awaiting-payment",
to: "/trade/orders/awaiting-payment",
label: "待付款订单",
label: navigationMessages["trade-orders-awaiting-payment"],
icon: WalletCardsIcon,
},
{
id: "trade-orders-awaiting-shipment",
to: "/trade/orders/awaiting-shipment",
label: "待发货订单",
label: navigationMessages["trade-orders-awaiting-shipment"],
icon: PackageIcon,
},
{
id: "trade-orders-shipped",
to: "/trade/orders/shipped",
label: "配送中订单",
label: navigationMessages["trade-orders-shipped"],
icon: DeliveryTruck01Icon,
},
{
id: "trade-orders-completed",
to: "/trade/orders/completed",
label: "已完成订单",
label: navigationMessages["trade-orders-completed"],
icon: CheckmarkCircle01Icon,
},
{
id: "trade-orders-canceled",
to: "/trade/orders/canceled",
label: "已取消订单",
label: navigationMessages["trade-orders-canceled"],
icon: CancelCircleIcon,
},
],
@@ -264,25 +266,25 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
{
id: "fulfillment-center",
to: "/fulfillment",
label: "配送履约",
label: navigationMessages["fulfillment-center"],
icon: DeliveryTruck01Icon,
items: [
{
id: "fulfillment-deliveries",
to: "/fulfillment/deliveries",
label: "配送任务",
label: navigationMessages["fulfillment-deliveries"],
icon: Location01Icon,
},
{
id: "fulfillment-pickups",
to: "/fulfillment/pickups",
label: "自提核销",
label: navigationMessages["fulfillment-pickups"],
icon: Store01Icon,
},
{
id: "fulfillment-freight-rules",
to: "/fulfillment/freight-rules",
label: "运费规则",
label: navigationMessages["fulfillment-freight-rules"],
icon: RoadLocation01Icon,
},
],
@@ -290,25 +292,25 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
{
id: "after-sales-center",
to: "/after-sales",
label: "售后中心",
label: navigationMessages["after-sales-center"],
icon: CallMissed01Icon,
items: [
{
id: "after-sales-returns",
to: "/after-sales/returns",
label: "退货申请",
label: navigationMessages["after-sales-returns"],
icon: Refresh01Icon,
},
{
id: "after-sales-refunds",
to: "/after-sales/refunds",
label: "退款审核",
label: navigationMessages["after-sales-refunds"],
icon: MoneySendSquareIcon,
},
{
id: "after-sales-disputes",
to: "/after-sales/disputes",
label: "交易纠纷",
label: navigationMessages["after-sales-disputes"],
icon: SecurityWarningIcon,
},
],
@@ -317,36 +319,36 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
},
{
id: "customers-and-marketing",
label: "客户与营销",
label: navigationMessages["customers-and-marketing"],
items: [
{
id: "customer-center",
to: "/customers",
label: "客户中心",
label: navigationMessages["customer-center"],
icon: IdentityCardIcon,
items: [
{
id: "customer-profiles",
to: "/customers/profiles",
label: "客户档案",
label: navigationMessages["customer-profiles"],
icon: IdentityCardIcon,
},
{
id: "customer-segments",
to: "/customers/segments",
label: "客户分群",
label: navigationMessages["customer-segments"],
icon: UserGroupIcon,
},
{
id: "customer-tiers",
to: "/customers/tiers",
label: "客户等级",
label: navigationMessages["customer-tiers"],
icon: CrownIcon,
},
{
id: "customer-risk-list",
to: "/customers/risk-list",
label: "风险名单",
label: navigationMessages["customer-risk-list"],
icon: SecurityWarningIcon,
},
],
@@ -354,31 +356,31 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
{
id: "marketing-campaigns",
to: "/marketing/campaigns",
label: "营销活动",
label: navigationMessages["marketing-campaigns"],
icon: Discount01Icon,
items: [
{
id: "marketing-coupons",
to: "/marketing/coupons",
label: "优惠券管理",
label: navigationMessages["marketing-coupons"],
icon: Ticket01Icon,
},
{
id: "marketing-flash-sales",
to: "/marketing/flash-sales",
label: "限时折扣",
label: navigationMessages["marketing-flash-sales"],
icon: Clock01Icon,
},
{
id: "marketing-gift-promotions",
to: "/marketing/gift-promotions",
label: "满赠活动",
label: navigationMessages["marketing-gift-promotions"],
icon: GiftIcon,
},
{
id: "marketing-referrals",
to: "/marketing/referrals",
label: "邀请有礼",
label: navigationMessages["marketing-referrals"],
icon: Share01Icon,
},
],
@@ -386,25 +388,25 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
{
id: "loyalty-center",
to: "/loyalty",
label: "会员权益",
label: navigationMessages["loyalty-center"],
icon: Medal01Icon,
items: [
{
id: "loyalty-points",
to: "/loyalty/points",
label: "积分规则",
label: navigationMessages["loyalty-points"],
icon: StarCircleIcon,
},
{
id: "loyalty-stored-value",
to: "/loyalty/stored-value",
label: "储值账户",
label: navigationMessages["loyalty-stored-value"],
icon: Wallet01Icon,
},
{
id: "loyalty-benefits",
to: "/loyalty/benefits",
label: "权益配置",
label: navigationMessages["loyalty-benefits"],
icon: CheckmarkBadge01Icon,
},
],
@@ -413,36 +415,36 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
},
{
id: "data-and-finance",
label: "数据与财务",
label: navigationMessages["data-and-finance"],
items: [
{
id: "report-center",
to: "/reports",
label: "数据报表",
label: navigationMessages["report-center"],
icon: Chart01Icon,
items: [
{
id: "report-sales",
to: "/reports/sales",
label: "销售分析",
label: navigationMessages["report-sales"],
icon: AnalyticsUpIcon,
},
{
id: "report-products",
to: "/reports/products",
label: "商品分析",
label: navigationMessages["report-products"],
icon: ShoppingBag01Icon,
},
{
id: "report-customers",
to: "/reports/customers",
label: "客群分析",
label: navigationMessages["report-customers"],
icon: UserGroupIcon,
},
{
id: "report-channels",
to: "/reports/channels",
label: "渠道分析",
label: navigationMessages["report-channels"],
icon: ChartIncreaseIcon,
},
],
@@ -450,31 +452,31 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
{
id: "finance-center",
to: "/finance",
label: "财务中心",
label: navigationMessages["finance-center"],
icon: WalletCardsIcon,
items: [
{
id: "finance-settlements",
to: "/finance/settlements",
label: "结算账单",
label: navigationMessages["finance-settlements"],
icon: Invoice03Icon,
},
{
id: "finance-reconciliation",
to: "/finance/reconciliation",
label: "对账管理",
label: navigationMessages["finance-reconciliation"],
icon: Calculator01Icon,
},
{
id: "finance-invoices",
to: "/finance/invoices",
label: "发票管理",
label: navigationMessages["finance-invoices"],
icon: File01Icon,
},
{
id: "finance-payouts",
to: "/finance/payouts",
label: "打款记录",
label: navigationMessages["finance-payouts"],
icon: MoneySendSquareIcon,
},
],
@@ -483,30 +485,30 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
},
{
id: "system-management",
label: "系统管理",
label: navigationMessages["system-management"],
items: [
{
id: "system-members",
to: "/system/members",
label: "组织成员",
label: navigationMessages["system-members"],
icon: UserShield01Icon,
items: [
{
id: "system-members-admins",
to: "/system/members/admins",
label: "管理员账号",
label: navigationMessages["system-members-admins"],
icon: UserShield01Icon,
},
{
id: "system-members-roles",
to: "/system/members/roles",
label: "角色管理",
label: navigationMessages["system-members-roles"],
icon: UserGroupIcon,
},
{
id: "system-members-permissions",
to: "/system/members/permissions",
label: "权限策略",
label: navigationMessages["system-members-permissions"],
icon: LockKeyIcon,
},
],
@@ -514,19 +516,19 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
{
id: "system-notifications",
to: "/system/notifications",
label: "消息中心",
label: navigationMessages["system-notifications"],
icon: BubbleChatIcon,
items: [
{
id: "system-notification-templates",
to: "/system/notifications/templates",
label: "通知模板",
label: navigationMessages["system-notification-templates"],
icon: File01Icon,
},
{
id: "system-notification-logs",
to: "/system/notifications/logs",
label: "发送记录",
label: navigationMessages["system-notification-logs"],
icon: HistoryIcon,
},
],
@@ -534,25 +536,25 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
{
id: "system-audit",
to: "/system/audit",
label: "操作审计",
label: navigationMessages["system-audit"],
icon: Note01Icon,
},
{
id: "system-integrations",
to: "/system/integrations",
label: "开放集成",
label: navigationMessages["system-integrations"],
icon: CodeSquareIcon,
items: [
{
id: "system-integration-api-keys",
to: "/system/integrations/api-keys",
label: "API 密钥",
label: navigationMessages["system-integration-api-keys"],
icon: Key01Icon,
},
{
id: "system-integration-webhooks",
to: "/system/integrations/webhooks",
label: "Webhook 配置",
label: navigationMessages["system-integration-webhooks"],
icon: LinkCircleIcon,
},
],
@@ -560,25 +562,25 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
{
id: "system-settings",
to: "/system-configs",
label: "系统配置",
label: navigationMessages["system-settings"],
icon: Settings01Icon,
items: [
{
id: "system-settings-store",
to: "/system-configs/store",
label: "门店资料",
label: navigationMessages["system-settings-store"],
icon: Store01Icon,
},
{
id: "system-settings-payments",
to: "/system-configs/payments",
label: "支付渠道",
label: navigationMessages["system-settings-payments"],
icon: CreditCardIcon,
},
{
id: "system-settings-shipping",
to: "/system-configs/shipping",
label: "配送设置",
label: navigationMessages["system-settings-shipping"],
icon: DeliveryTruck01Icon,
},
],
@@ -586,7 +588,7 @@ export const primaryNavigationGroups: readonly NavigationGroup[] = [
{
id: "system-recycle-bin",
to: "/system/recycle-bin",
label: "回收站",
label: navigationMessages["system-recycle-bin"],
icon: Delete02Icon,
},
],
+21
View File
@@ -0,0 +1,21 @@
import { lazy, Suspense } from "react"
const LazyI18nDevtool = import.meta.env.DEV
? lazy(() =>
import("@workspace/i18n/devtool").then((module) => ({
default: module.I18nDevtool,
}))
)
: null
export function AppI18nDevtool() {
if (!LazyI18nDevtool) {
return null
}
return (
<Suspense fallback={null}>
<LazyI18nDevtool dark={"ui\\:dark"} />
</Suspense>
)
}
+90 -36
View File
@@ -1,51 +1,105 @@
import { lazy, Suspense, type PropsWithChildren } from "react"
import { I18nProvider, type LocaleDefinition } from "@workspace/i18n"
import {
createContext,
startTransition,
use,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type PropsWithChildren,
} from "react"
import {
I18nProvider,
useLocaleDefinition,
useTranslate,
} from "@workspace/i18n"
import { loadMessageCatalog } from "@workspace/i18n/catalogs"
import {
I18nProvider as UiI18nProvider,
type CalendarLocale,
} from "@workspace/ui/components/i18n"
import { calendarLocale as englishCalendarLocale } from "@workspace/ui/locales/en"
import { calendarLocale as simplifiedChineseCalendarLocale } from "@workspace/ui/locales/zh-Hans"
import { messages as englishMessages } from "@/locales/en/messages"
import { messages as simplifiedChineseMessages } from "@/locales/zh-Hans/messages"
import { appLocales, DEFAULT_APP_LOCALE, type AppLocale } from "./app-locales"
import { AppI18nDevtool } from "./app-i18n-devtool"
import { setAppLocale as persistAppLocale } from "@/server/app-locale.functions"
const I18nDevtool = import.meta.env.DEV
? lazy(() =>
import("@workspace/i18n/devtool").then((module) => ({
default: module.I18nDevtool,
}))
)
: null
export const appLocales = [
{
label: "English",
locale: "en",
},
{
label: "简体中文",
locale: "zh-Hans",
},
] as const satisfies readonly LocaleDefinition[]
export type AppLocale = (typeof appLocales)[number]["locale"]
const appCatalogs = {
en: englishMessages,
"zh-Hans": simplifiedChineseMessages,
const appCalendarLocales: Partial<Record<AppLocale, CalendarLocale>> = {
en: englishCalendarLocale,
"zh-Hans": simplifiedChineseCalendarLocale,
}
export interface AppI18nProviderProps extends PropsWithChildren {
locale?: AppLocale
}
interface AppLocaleContextValue {
locale: AppLocale
setLocale: (locale: AppLocale) => void
}
const AppLocaleContext = createContext<AppLocaleContextValue | null>(null)
export function AppI18nProvider({
children,
locale = "zh-Hans",
locale: initialLocale = DEFAULT_APP_LOCALE,
}: AppI18nProviderProps) {
const [locale, setLocaleState] = useState(initialLocale)
const messages = use(loadMessageCatalog(locale))
const catalogs = useMemo(() => ({ [locale]: messages }), [locale, messages])
const setLocale = useCallback((nextLocale: AppLocale) => {
startTransition(() => {
setLocaleState(nextLocale)
})
void persistAppLocale({ data: nextLocale })
}, [])
const localeContext = useMemo(
() => ({ locale, setLocale }),
[locale, setLocale]
)
useEffect(() => {
const definition = appLocales.find(
(candidate) => candidate.locale === locale
)
document.documentElement.lang = definition?.languageTag ?? locale
document.documentElement.dir = definition?.direction ?? "ltr"
}, [locale])
return (
<I18nProvider catalogs={appCatalogs} locale={locale} locales={appLocales}>
{children}
{I18nDevtool && (
<Suspense fallback={null}>
<I18nDevtool dark={"ui\\:dark"} />
</Suspense>
)}
<AppLocaleContext.Provider value={localeContext}>
<I18nProvider catalogs={catalogs} locale={locale} locales={appLocales}>
<AppUiI18nProvider>{children}</AppUiI18nProvider>
<AppI18nDevtool />
</I18nProvider>
</AppLocaleContext.Provider>
)
}
export function useAppLocale() {
const context = useContext(AppLocaleContext)
if (!context) {
throw new Error("useAppLocale must be used within AppI18nProvider")
}
return context
}
function AppUiI18nProvider({ children }: PropsWithChildren) {
const locale = useLocaleDefinition()
const translate = useTranslate()
return (
<UiI18nProvider
calendarLocale={appCalendarLocales[locale.locale as AppLocale]}
locale={locale.languageTag ?? locale.locale}
t={translate}
>
{children}
</UiI18nProvider>
)
}
@@ -0,0 +1,45 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from "@testing-library/react"
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"
import { AppLanguageSwitcher } from "./app-language-switcher"
vi.mock("./app-i18n-provider", () => ({
useAppLocale: () => ({
locale: "en",
setLocale: vi.fn(),
}),
}))
vi.mock("country-flag-icons/react/1x1", () => ({
CN: () => <svg />,
US: () => <svg />,
}))
beforeAll(() => {
vi.stubGlobal(
"ResizeObserver",
class {
disconnect() {}
observe() {}
unobserve() {}
}
)
})
afterEach(cleanup)
describe("AppLanguageSwitcher", () => {
it("opens its grouped locale choices", async () => {
render(<AppLanguageSwitcher />)
fireEvent.click(
screen.getByRole("button", {
name: "Change language, currently English",
})
)
expect(await screen.findByText("简体中文")).not.toBeNull()
})
})
@@ -0,0 +1,94 @@
import { CN, US } from "country-flag-icons/react/1x1"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@workspace/ui/components/dropdown-menu"
import { cn } from "@workspace/ui/lib/utils"
import { HeaderActionButton } from "@workspace/blocks/layout"
import { appLocales, isAppLocale, type AppLocaleCountry } from "./app-locales"
import { useAppLocale } from "./app-i18n-provider"
const localeFlags = {
CN,
US,
} satisfies Record<AppLocaleCountry, typeof CN>
function LocaleFlag({
country,
className,
}: {
className?: string
country: AppLocaleCountry
}) {
const Flag = localeFlags[country]
return (
<span
aria-hidden="true"
className={cn(
"inline-flex shrink-0 overflow-hidden rounded-full ring-1 ring-foreground/15",
className
)}
>
<Flag className="size-full" />
</span>
)
}
export function AppLanguageSwitcher() {
const { locale, setLocale } = useAppLocale()
const activeLocale =
appLocales.find((definition) => definition.locale === locale) ??
appLocales[0]
const menuLabel = locale === "zh-Hans" ? "语言" : "Language"
const triggerLabel =
locale === "zh-Hans"
? `切换语言,当前为${activeLocale.label}`
: `Change language, currently ${activeLocale.label}`
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<HeaderActionButton
aria-label={triggerLabel}
className="rounded-full"
/>
}
>
<LocaleFlag country={activeLocale.country} className="size-6" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-44"
showArrow={{ cornerRadius: "lg", tailSize: "sm" }}
>
<DropdownMenuRadioGroup
value={locale}
onValueChange={(value) => {
if (isAppLocale(value)) {
setLocale(value)
}
}}
>
<DropdownMenuLabel>{menuLabel}</DropdownMenuLabel>
{appLocales.map((definition) => (
<DropdownMenuRadioItem
key={definition.locale}
className="py-2"
value={definition.locale}
>
<LocaleFlag country={definition.country} className="size-5" />
<span lang={definition.languageTag}>{definition.label}</span>
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
+32
View File
@@ -0,0 +1,32 @@
import type { LocaleDefinition } from "@workspace/i18n"
export type AppLocale = "en" | "zh-Hans"
export type AppLocaleCountry = "CN" | "US"
export interface AppLocaleDefinition extends LocaleDefinition {
country: AppLocaleCountry
locale: AppLocale
}
export const DEFAULT_APP_LOCALE: AppLocale = "zh-Hans"
export const appLocales = [
{
country: "US",
direction: "ltr",
languageTag: "en-US",
label: "English",
locale: "en",
},
{
country: "CN",
direction: "ltr",
languageTag: "zh-CN",
label: "简体中文",
locale: "zh-Hans",
},
] as const satisfies readonly AppLocaleDefinition[]
export function isAppLocale(value: unknown): value is AppLocale {
return appLocales.some((definition) => definition.locale === value)
}
+840
View File
@@ -12,3 +12,843 @@ msgstr ""
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:4
msgid "blocks.userMenu.accountPassword"
msgstr "Account password"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:4
msgid "blocks.appearance.active"
msgstr "Active"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:276
msgid "app.navigation.system-members-admins"
msgstr "Administrator accounts"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:148
msgid "app.navigation.after-sales-center"
msgstr "After-sales"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:91
msgid "blocks.appearance.color.amber"
msgstr "Amber"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:308
msgid "app.navigation.system-integration-api-keys"
msgstr "API keys"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:8
msgid "blocks.userMenu.appearance"
msgstr "Appearance"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:8
msgid "blocks.appearance.title"
msgstr "Appearance"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:92
msgid "app.navigation.content-articles"
msgstr "Articles"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:300
msgid "app.navigation.system-audit"
msgstr "Audit log"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:112
msgid "app.navigation.trade-orders-awaiting-payment"
msgstr "Awaiting payment"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:116
msgid "app.navigation.trade-orders-awaiting-shipment"
msgstr "Awaiting shipment"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:16
msgid "blocks.appearance.baseColor"
msgstr "Base color"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:220
msgid "app.navigation.loyalty-benefits"
msgstr "Benefits configuration"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:95
msgid "blocks.appearance.color.blue"
msgstr "Blue"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:48
msgid "app.navigation.catalog-product-brands"
msgstr "Brands"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:25
msgid "ui.breadcrumb.label"
msgstr "Breadcrumb"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:8
msgid "app.navigation.overview-dashboard"
msgstr "Business overview"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:128
msgid "app.navigation.trade-orders-canceled"
msgstr "Canceled orders"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:52
msgid "app.navigation.catalog-categories"
msgstr "Categories"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:56
msgid "app.navigation.catalog-category-tree"
msgstr "Category tree"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:244
msgid "app.navigation.report-channels"
msgstr "Channel analysis"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:4
msgid "blocks.chats.title"
msgstr "Chats"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:16
msgid "blocks.notifications.error.description"
msgstr "Check your network connection and try again."
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:29
msgid "ui.common.close"
msgstr "Close"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:33
msgid "ui.toast.close"
msgstr "Close notification"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:24
msgid "blocks.appearance.compact.title"
msgstr "Compact layout"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:124
msgid "app.navigation.trade-orders-completed"
msgstr "Completed orders"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:88
msgid "app.navigation.content-assets"
msgstr "Content"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:192
msgid "app.navigation.marketing-coupons"
msgstr "Coupons"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:100
msgid "app.navigation.content-pages"
msgstr "Custom pages"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:240
msgid "app.navigation.report-customers"
msgstr "Customer analysis"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:172
msgid "app.navigation.customer-profiles"
msgstr "Customer profiles"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:176
msgid "app.navigation.customer-segments"
msgstr "Customer segments"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:180
msgid "app.navigation.customer-tiers"
msgstr "Customer tiers"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:168
msgid "app.navigation.customer-center"
msgstr "Customers"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:164
msgid "app.navigation.customers-and-marketing"
msgstr "Customers and marketing"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:99
msgid "blocks.appearance.color.cyan"
msgstr "Cyan"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:28
msgid "blocks.appearance.mode.dark"
msgstr "Dark"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:36
msgid "blocks.appearance.theme.dark"
msgstr "Dark theme"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:224
msgid "app.navigation.data-and-finance"
msgstr "Data and finance"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:296
msgid "app.navigation.system-notification-logs"
msgstr "Delivery logs"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:136
msgid "app.navigation.fulfillment-deliveries"
msgstr "Delivery tasks"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:81
msgid "ui.sidebar.description"
msgstr "Displays the mobile sidebar."
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:103
msgid "blocks.appearance.color.emerald"
msgstr "Emerald"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:248
msgid "app.navigation.finance-center"
msgstr "Finance"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:196
msgid "app.navigation.marketing-flash-sales"
msgstr "Flash sales"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:107
msgid "blocks.appearance.color.fuchsia"
msgstr "Fuchsia"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:132
msgid "app.navigation.fulfillment-center"
msgstr "Fulfillment"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:200
msgid "app.navigation.marketing-gift-promotions"
msgstr "Gift promotions"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:53
msgid "ui.pagination.nextPage"
msgstr "Go to next page"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:69
msgid "ui.pagination.previousPage"
msgstr "Go to previous page"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:111
msgid "blocks.appearance.color.green"
msgstr "Green"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:120
msgid "app.navigation.trade-orders-shipped"
msgstr "In transit"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:72
msgid "app.navigation.inventory-inbound"
msgstr "Inbound orders"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:115
msgid "blocks.appearance.color.indigo"
msgstr "Indigo"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:304
msgid "app.navigation.system-integrations"
msgstr "Integrations"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:64
msgid "app.navigation.inventory-center"
msgstr "Inventory"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:84
msgid "app.navigation.inventory-alerts"
msgstr "Inventory alerts"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:68
msgid "app.navigation.inventory-overview"
msgstr "Inventory overview"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:260
msgid "app.navigation.finance-invoices"
msgstr "Invoices"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:24
msgid "blocks.notifications.justNow"
msgstr "just now"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:16
msgid "blocks.userMenu.keybindings"
msgstr "Keybindings"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:52
msgid "blocks.appearance.mode.light"
msgstr "Light"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:60
msgid "blocks.appearance.theme.light"
msgstr "Light theme"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:119
msgid "blocks.appearance.color.lime"
msgstr "Lime"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:37
msgid "ui.common.loading"
msgstr "Loading"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:28
msgid "blocks.notifications.loading"
msgstr "Loading notifications…"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:20
msgid "blocks.userMenu.logout"
msgstr "Log out"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:32
msgid "blocks.notifications.markAllAsRead"
msgstr "Mark all as read"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:188
msgid "app.navigation.marketing-campaigns"
msgstr "Marketing campaigns"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:123
msgid "blocks.appearance.color.mauve"
msgstr "Mauve"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:96
msgid "app.navigation.content-media"
msgstr "Media"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:208
msgid "app.navigation.loyalty-center"
msgstr "Membership benefits"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:127
msgid "blocks.appearance.color.mist"
msgstr "Mist"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:41
msgid "ui.common.more"
msgstr "More"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:45
msgid "ui.pagination.more"
msgstr "More pages"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/navigation/messages.ts:4
msgid "blocks.navigation.description"
msgstr "Navigate through the app."
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/navigation/messages.ts:12
msgid "blocks.navigation.title"
msgstr "Navigation"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:60
msgid "app.navigation.catalog-category-navigation"
msgstr "Navigation categories"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:131
msgid "blocks.appearance.color.neutral"
msgstr "Neutral"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:8
msgid "blocks.notifications.empty.description"
msgstr "New activity will appear here."
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:49
msgid "ui.pagination.next"
msgstr "Next"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:57
msgid "ui.carousel.nextSlide"
msgstr "Next slide"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:12
msgid "blocks.notifications.empty.title"
msgstr "No notifications"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:292
msgid "app.navigation.system-notification-templates"
msgstr "Notification templates"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:40
msgid "blocks.notifications.title"
msgstr "Notifications"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:288
msgid "app.navigation.system-notifications"
msgstr "Notifications"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:135
msgid "blocks.appearance.color.olive"
msgstr "Olive"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:12
msgid "blocks.breadcrumb.menuLabel"
msgstr "Open breadcrumb menu"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/navigation/messages.ts:8
msgid "blocks.navigation.open"
msgstr "Open navigation"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:12
msgid "blocks.appearance.command.description"
msgstr "Open theme and layout preferences"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:24
msgid "blocks.userMenu.open"
msgstr "Open user menu"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:139
msgid "blocks.appearance.color.orange"
msgstr "Orange"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:108
msgid "app.navigation.trade-orders"
msgstr "Orders"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:104
msgid "app.navigation.trade-and-fulfillment"
msgstr "Orders and fulfillment"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:272
msgid "app.navigation.system-members"
msgstr "Organization members"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:76
msgid "app.navigation.inventory-outbound"
msgstr "Outbound orders"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:4
msgid "app.navigation.overview"
msgstr "Overview"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:61
msgid "ui.pagination.label"
msgstr "Pagination"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:324
msgid "app.navigation.system-settings-payments"
msgstr "Payment channels"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:264
msgid "app.navigation.finance-payouts"
msgstr "Payout history"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:284
msgid "app.navigation.system-members-permissions"
msgstr "Permission policies"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:140
msgid "app.navigation.fulfillment-pickups"
msgstr "Pickup verification"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:143
msgid "blocks.appearance.color.pink"
msgstr "Pink"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:212
msgid "app.navigation.loyalty-points"
msgstr "Points rules"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:65
msgid "ui.pagination.previous"
msgstr "Previous"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:73
msgid "ui.carousel.previousSlide"
msgstr "Previous slide"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:68
msgid "blocks.appearance.primaryColor"
msgstr "Primary color"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:236
msgid "app.navigation.report-products"
msgstr "Product analysis"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:40
msgid "app.navigation.catalog-products-attributes"
msgstr "Product attributes"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:36
msgid "app.navigation.catalog-products-bundles"
msgstr "Product bundles"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:32
msgid "app.navigation.catalog-products-drafts"
msgstr "Product drafts"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:28
msgid "app.navigation.catalog-products-list"
msgstr "Product list"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:24
msgid "app.navigation.catalog-products"
msgstr "Products"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:20
msgid "app.navigation.catalog-and-inventory"
msgstr "Products and inventory"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:147
msgid "blocks.appearance.color.purple"
msgstr "Purple"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:12
msgid "app.navigation.overview-realtime"
msgstr "Real-time monitoring"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:256
msgid "app.navigation.finance-reconciliation"
msgstr "Reconciliation"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:332
msgid "app.navigation.system-recycle-bin"
msgstr "Recycle bin"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:151
msgid "blocks.appearance.color.red"
msgstr "Red"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:204
msgid "app.navigation.marketing-referrals"
msgstr "Referral rewards"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:32
msgid "blocks.refresh.label"
msgstr "Refresh current page data"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:28
msgid "blocks.refresh.description"
msgstr "Refresh the current page data"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:156
msgid "app.navigation.after-sales-refunds"
msgstr "Refund review"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:228
msgid "app.navigation.report-center"
msgstr "Reports"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:152
msgid "app.navigation.after-sales-returns"
msgstr "Return requests"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:184
msgid "app.navigation.customer-risk-list"
msgstr "Risk list"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:280
msgid "app.navigation.system-members-roles"
msgstr "Roles"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:155
msgid "blocks.appearance.color.rose"
msgstr "Rose"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:232
msgid "app.navigation.report-sales"
msgstr "Sales analysis"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:252
msgid "app.navigation.finance-settlements"
msgstr "Settlement statements"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:144
msgid "app.navigation.fulfillment-freight-rules"
msgstr "Shipping rules"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:328
msgid "app.navigation.system-settings-shipping"
msgstr "Shipping settings"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:77
msgid "ui.sidebar.title"
msgstr "Sidebar"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:159
msgid "blocks.appearance.color.sky"
msgstr "Sky"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:44
msgid "app.navigation.catalog-product-specifications"
msgstr "Specification templates"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:80
msgid "app.navigation.inventory-transfers"
msgstr "Stock transfers"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:163
msgid "blocks.appearance.color.stone"
msgstr "Stone"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:320
msgid "app.navigation.system-settings-store"
msgstr "Store information"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:216
msgid "app.navigation.loyalty-stored-value"
msgstr "Stored-value accounts"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:84
msgid "blocks.appearance.toggleTheme.description"
msgstr "Switch between light and dark themes"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:44
msgid "blocks.appearance.mode.system"
msgstr "System"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:268
msgid "app.navigation.system-management"
msgstr "System management"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:316
msgid "app.navigation.system-settings"
msgstr "System settings"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:16
msgid "app.navigation.overview-tasks"
msgstr "Tasks"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:167
msgid "blocks.appearance.color.taupe"
msgstr "Taupe"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:171
msgid "blocks.appearance.color.teal"
msgstr "Teal"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:48
msgid "blocks.appearance.mode.system.description"
msgstr "The interface theme will follow the system appearance"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:32
msgid "blocks.appearance.mode.dark.description"
msgstr "The interface will always use the dark theme"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:56
msgid "blocks.appearance.mode.light.description"
msgstr "The interface will always use the light theme"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:72
msgid "blocks.appearance.description"
msgstr "Theme and layout preferences"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:76
msgid "blocks.appearance.mode.title"
msgstr "Theme mode"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:40
msgid "blocks.appearance.theme.dark.description"
msgstr "This theme is used when the system is in dark mode"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:64
msgid "blocks.appearance.theme.light.description"
msgstr "This theme is used when the system is in light mode"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:85
msgid "ui.sidebar.toggle"
msgstr "Toggle sidebar"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:80
msgid "blocks.appearance.toggleTheme"
msgstr "Toggle theme"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:160
msgid "app.navigation.after-sales-disputes"
msgstr "Transaction disputes"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:36
msgid "blocks.notifications.retry"
msgstr "Try again"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:20
msgid "blocks.notifications.error.title"
msgstr "Unable to load notifications"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:20
msgid "blocks.appearance.compact.description"
msgstr "Use a compact width for the main page content"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:36
msgid "blocks.userMenu.title"
msgstr "User menu"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:44
msgid "blocks.notifications.viewAll"
msgstr "View all"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:4
msgid "blocks.notifications.description"
msgstr "View and manage notifications"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:175
msgid "blocks.appearance.color.violet"
msgstr "Violet"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:312
msgid "app.navigation.system-integration-webhooks"
msgstr "Webhook configuration"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:179
msgid "blocks.appearance.color.yellow"
msgstr "Yellow"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:183
msgid "blocks.appearance.color.zinc"
msgstr "Zinc"
File diff suppressed because one or more lines are too long
+840
View File
@@ -12,3 +12,843 @@ msgstr ""
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:4
msgid "blocks.userMenu.accountPassword"
msgstr "账号密码"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:4
msgid "blocks.appearance.active"
msgstr "使用中"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:276
msgid "app.navigation.system-members-admins"
msgstr "管理员账号"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:148
msgid "app.navigation.after-sales-center"
msgstr "售后中心"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:91
msgid "blocks.appearance.color.amber"
msgstr "琥珀色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:308
msgid "app.navigation.system-integration-api-keys"
msgstr "API 密钥"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:8
msgid "blocks.userMenu.appearance"
msgstr "界面外观"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:8
msgid "blocks.appearance.title"
msgstr "界面外观"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:92
msgid "app.navigation.content-articles"
msgstr "内容文章"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:300
msgid "app.navigation.system-audit"
msgstr "操作审计"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:112
msgid "app.navigation.trade-orders-awaiting-payment"
msgstr "待付款订单"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:116
msgid "app.navigation.trade-orders-awaiting-shipment"
msgstr "待发货订单"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:16
msgid "blocks.appearance.baseColor"
msgstr "基础颜色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:220
msgid "app.navigation.loyalty-benefits"
msgstr "权益配置"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:95
msgid "blocks.appearance.color.blue"
msgstr "蓝色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:48
msgid "app.navigation.catalog-product-brands"
msgstr "品牌资料"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:25
msgid "ui.breadcrumb.label"
msgstr "面包屑导航"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:8
msgid "app.navigation.overview-dashboard"
msgstr "经营总览"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:128
msgid "app.navigation.trade-orders-canceled"
msgstr "已取消订单"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:52
msgid "app.navigation.catalog-categories"
msgstr "分类目录"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:56
msgid "app.navigation.catalog-category-tree"
msgstr "分类树"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:244
msgid "app.navigation.report-channels"
msgstr "渠道分析"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/chats/messages.ts:4
msgid "blocks.chats.title"
msgstr "聊天"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:16
msgid "blocks.notifications.error.description"
msgstr "请检查网络连接后重试。"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:29
msgid "ui.common.close"
msgstr "关闭"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:33
msgid "ui.toast.close"
msgstr "关闭通知"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:24
msgid "blocks.appearance.compact.title"
msgstr "紧凑布局"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:124
msgid "app.navigation.trade-orders-completed"
msgstr "已完成订单"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:88
msgid "app.navigation.content-assets"
msgstr "内容素材"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:192
msgid "app.navigation.marketing-coupons"
msgstr "优惠券管理"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:100
msgid "app.navigation.content-pages"
msgstr "自定义页面"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:240
msgid "app.navigation.report-customers"
msgstr "客群分析"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:172
msgid "app.navigation.customer-profiles"
msgstr "客户档案"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:176
msgid "app.navigation.customer-segments"
msgstr "客户分群"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:180
msgid "app.navigation.customer-tiers"
msgstr "客户等级"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:168
msgid "app.navigation.customer-center"
msgstr "客户中心"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:164
msgid "app.navigation.customers-and-marketing"
msgstr "客户与营销"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:99
msgid "blocks.appearance.color.cyan"
msgstr "青色"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:28
msgid "blocks.appearance.mode.dark"
msgstr "深色"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:36
msgid "blocks.appearance.theme.dark"
msgstr "深色主题"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:224
msgid "app.navigation.data-and-finance"
msgstr "数据与财务"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:296
msgid "app.navigation.system-notification-logs"
msgstr "发送记录"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:136
msgid "app.navigation.fulfillment-deliveries"
msgstr "配送任务"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:81
msgid "ui.sidebar.description"
msgstr "显示移动端侧栏。"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:103
msgid "blocks.appearance.color.emerald"
msgstr "翠绿色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:248
msgid "app.navigation.finance-center"
msgstr "财务中心"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:196
msgid "app.navigation.marketing-flash-sales"
msgstr "限时折扣"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:107
msgid "blocks.appearance.color.fuchsia"
msgstr "紫红色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:132
msgid "app.navigation.fulfillment-center"
msgstr "配送履约"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:200
msgid "app.navigation.marketing-gift-promotions"
msgstr "满赠活动"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:53
msgid "ui.pagination.nextPage"
msgstr "前往下一页"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:69
msgid "ui.pagination.previousPage"
msgstr "前往上一页"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:111
msgid "blocks.appearance.color.green"
msgstr "绿色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:120
msgid "app.navigation.trade-orders-shipped"
msgstr "配送中订单"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:72
msgid "app.navigation.inventory-inbound"
msgstr "入库单"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:115
msgid "blocks.appearance.color.indigo"
msgstr "靛蓝色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:304
msgid "app.navigation.system-integrations"
msgstr "开放集成"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:64
msgid "app.navigation.inventory-center"
msgstr "库存中心"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:84
msgid "app.navigation.inventory-alerts"
msgstr "库存预警"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:68
msgid "app.navigation.inventory-overview"
msgstr "库存总览"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:260
msgid "app.navigation.finance-invoices"
msgstr "发票管理"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:24
msgid "blocks.notifications.justNow"
msgstr "刚刚"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:16
msgid "blocks.userMenu.keybindings"
msgstr "按键映射"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:52
msgid "blocks.appearance.mode.light"
msgstr "浅色"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:60
msgid "blocks.appearance.theme.light"
msgstr "浅色主题"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:119
msgid "blocks.appearance.color.lime"
msgstr "青柠色"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:37
msgid "ui.common.loading"
msgstr "正在加载"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:28
msgid "blocks.notifications.loading"
msgstr "正在加载通知…"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:20
msgid "blocks.userMenu.logout"
msgstr "退出登录"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:32
msgid "blocks.notifications.markAllAsRead"
msgstr "全部标记为已读"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:188
msgid "app.navigation.marketing-campaigns"
msgstr "营销活动"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:123
msgid "blocks.appearance.color.mauve"
msgstr "紫灰"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:96
msgid "app.navigation.content-media"
msgstr "媒体资源"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:208
msgid "app.navigation.loyalty-center"
msgstr "会员权益"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:127
msgid "blocks.appearance.color.mist"
msgstr "雾灰"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:41
msgid "ui.common.more"
msgstr "更多"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:45
msgid "ui.pagination.more"
msgstr "更多页面"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/navigation/messages.ts:4
msgid "blocks.navigation.description"
msgstr "浏览应用中的页面。"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/navigation/messages.ts:12
msgid "blocks.navigation.title"
msgstr "导航"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:60
msgid "app.navigation.catalog-category-navigation"
msgstr "导航分类"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:131
msgid "blocks.appearance.color.neutral"
msgstr "中性灰"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:8
msgid "blocks.notifications.empty.description"
msgstr "新的动态会显示在这里。"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:49
msgid "ui.pagination.next"
msgstr "下一页"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:57
msgid "ui.carousel.nextSlide"
msgstr "下一张"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:12
msgid "blocks.notifications.empty.title"
msgstr "暂无通知"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:292
msgid "app.navigation.system-notification-templates"
msgstr "通知模板"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:40
msgid "blocks.notifications.title"
msgstr "通知"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:288
msgid "app.navigation.system-notifications"
msgstr "消息中心"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:135
msgid "blocks.appearance.color.olive"
msgstr "橄榄灰"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:12
msgid "blocks.breadcrumb.menuLabel"
msgstr "打开面包屑菜单"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/navigation/messages.ts:8
msgid "blocks.navigation.open"
msgstr "打开导航"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:12
msgid "blocks.appearance.command.description"
msgstr "打开主题与布局偏好"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:24
msgid "blocks.userMenu.open"
msgstr "打开用户菜单"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:139
msgid "blocks.appearance.color.orange"
msgstr "橙色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:108
msgid "app.navigation.trade-orders"
msgstr "订单中心"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:104
msgid "app.navigation.trade-and-fulfillment"
msgstr "交易与履约"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:272
msgid "app.navigation.system-members"
msgstr "组织成员"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:76
msgid "app.navigation.inventory-outbound"
msgstr "出库单"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:4
msgid "app.navigation.overview"
msgstr "概览"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:61
msgid "ui.pagination.label"
msgstr "分页导航"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:324
msgid "app.navigation.system-settings-payments"
msgstr "支付渠道"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:264
msgid "app.navigation.finance-payouts"
msgstr "打款记录"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:284
msgid "app.navigation.system-members-permissions"
msgstr "权限策略"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:140
msgid "app.navigation.fulfillment-pickups"
msgstr "自提核销"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:143
msgid "blocks.appearance.color.pink"
msgstr "粉色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:212
msgid "app.navigation.loyalty-points"
msgstr "积分规则"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:65
msgid "ui.pagination.previous"
msgstr "上一页"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:73
msgid "ui.carousel.previousSlide"
msgstr "上一张"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:68
msgid "blocks.appearance.primaryColor"
msgstr "主要颜色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:236
msgid "app.navigation.report-products"
msgstr "商品分析"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:40
msgid "app.navigation.catalog-products-attributes"
msgstr "商品属性"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:36
msgid "app.navigation.catalog-products-bundles"
msgstr "组合商品"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:32
msgid "app.navigation.catalog-products-drafts"
msgstr "商品草稿"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:28
msgid "app.navigation.catalog-products-list"
msgstr "商品列表"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:24
msgid "app.navigation.catalog-products"
msgstr "商品中心"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:20
msgid "app.navigation.catalog-and-inventory"
msgstr "商品与库存"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:147
msgid "blocks.appearance.color.purple"
msgstr "紫色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:12
msgid "app.navigation.overview-realtime"
msgstr "实时监控"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:256
msgid "app.navigation.finance-reconciliation"
msgstr "对账管理"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:332
msgid "app.navigation.system-recycle-bin"
msgstr "回收站"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:151
msgid "blocks.appearance.color.red"
msgstr "红色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:204
msgid "app.navigation.marketing-referrals"
msgstr "邀请有礼"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:32
msgid "blocks.refresh.label"
msgstr "刷新当前页面数据"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:28
msgid "blocks.refresh.description"
msgstr "刷新当前页面的数据"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:156
msgid "app.navigation.after-sales-refunds"
msgstr "退款审核"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:228
msgid "app.navigation.report-center"
msgstr "数据报表"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:152
msgid "app.navigation.after-sales-returns"
msgstr "退货申请"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:184
msgid "app.navigation.customer-risk-list"
msgstr "风险名单"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:280
msgid "app.navigation.system-members-roles"
msgstr "角色管理"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:155
msgid "blocks.appearance.color.rose"
msgstr "玫红色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:232
msgid "app.navigation.report-sales"
msgstr "销售分析"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:252
msgid "app.navigation.finance-settlements"
msgstr "结算账单"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:144
msgid "app.navigation.fulfillment-freight-rules"
msgstr "运费规则"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:328
msgid "app.navigation.system-settings-shipping"
msgstr "配送设置"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:77
msgid "ui.sidebar.title"
msgstr "侧栏"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:159
msgid "blocks.appearance.color.sky"
msgstr "天蓝色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:44
msgid "app.navigation.catalog-product-specifications"
msgstr "规格模板"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:80
msgid "app.navigation.inventory-transfers"
msgstr "调拨单"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:163
msgid "blocks.appearance.color.stone"
msgstr "石灰"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:320
msgid "app.navigation.system-settings-store"
msgstr "门店资料"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:216
msgid "app.navigation.loyalty-stored-value"
msgstr "储值账户"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:84
msgid "blocks.appearance.toggleTheme.description"
msgstr "在浅色与深色主题之间切换"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:44
msgid "blocks.appearance.mode.system"
msgstr "系统"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:268
msgid "app.navigation.system-management"
msgstr "系统管理"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:316
msgid "app.navigation.system-settings"
msgstr "系统配置"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:16
msgid "app.navigation.overview-tasks"
msgstr "待办中心"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:167
msgid "blocks.appearance.color.taupe"
msgstr "灰褐"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:171
msgid "blocks.appearance.color.teal"
msgstr "蓝绿色"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:48
msgid "blocks.appearance.mode.system.description"
msgstr "界面主题将跟随系统外观设置"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:32
msgid "blocks.appearance.mode.dark.description"
msgstr "界面将始终使用深色主题"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:56
msgid "blocks.appearance.mode.light.description"
msgstr "界面将始终使用浅色主题"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:72
msgid "blocks.appearance.description"
msgstr "主题与布局偏好"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:76
msgid "blocks.appearance.mode.title"
msgstr "主题模式"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:40
msgid "blocks.appearance.theme.dark.description"
msgstr "当系统设置为深色模式时,将使用此主题"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:64
msgid "blocks.appearance.theme.light.description"
msgstr "当系统设置为浅色模式时,将使用此主题"
#. js-lingui-explicit-id
#: ../../packages/ui/src/components/i18n.tsx:85
msgid "ui.sidebar.toggle"
msgstr "切换侧栏"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:80
msgid "blocks.appearance.toggleTheme"
msgstr "切换主题"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:160
msgid "app.navigation.after-sales-disputes"
msgstr "交易纠纷"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:36
msgid "blocks.notifications.retry"
msgstr "重新加载"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:20
msgid "blocks.notifications.error.title"
msgstr "无法加载通知"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:20
msgid "blocks.appearance.compact.description"
msgstr "启用后,页面主内容将使用紧凑宽度"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/layout/messages.ts:36
msgid "blocks.userMenu.title"
msgstr "用户菜单"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:44
msgid "blocks.notifications.viewAll"
msgstr "查看全部"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/notifications/messages.ts:4
msgid "blocks.notifications.description"
msgstr "查看并处理通知"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:175
msgid "blocks.appearance.color.violet"
msgstr "紫罗兰色"
#. js-lingui-explicit-id
#: src/data/navigation-messages.ts:312
msgid "app.navigation.system-integration-webhooks"
msgstr "Webhook 配置"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:179
msgid "blocks.appearance.color.yellow"
msgstr "黄色"
#. js-lingui-explicit-id
#: ../../packages/blocks/src/blocks/appearance/messages.ts:183
msgid "blocks.appearance.color.zinc"
msgstr "锌灰"
File diff suppressed because one or more lines are too long
+16 -5
View File
@@ -24,10 +24,19 @@ import { HotkeysProvider } from "@tanstack/react-hotkeys"
import { TooltipProvider } from "@workspace/ui/components/tooltip"
import { QueryClientProvider } from "@tanstack/react-query"
import { AppI18nProvider } from "@/i18n/app-i18n-provider"
import { getAppLocale } from "@/server/app-locale.functions"
import type { AppLocale } from "@/i18n/app-locales"
import { queryClient } from "@/lib/query-client"
export const Route = createRootRoute({
loader: () => getUiState(),
loader: async () => {
const [initialState, initialLocale] = await Promise.all([
getUiState(),
getAppLocale(),
])
return { initialLocale, initialState }
},
head: () => ({
meta: [
{ charSet: "utf-8" },
@@ -48,11 +57,11 @@ export const Route = createRootRoute({
})
function RootComponent() {
const initialState = Route.useLoaderData()
const { initialLocale, initialState } = Route.useLoaderData()
return (
<RootDocument initialState={initialState}>
<AppI18nProvider>
<RootDocument initialLocale={initialLocale} initialState={initialState}>
<AppI18nProvider locale={initialLocale}>
<BreakpointReactor />
<UiStateProvider
initialState={initialState}
@@ -94,9 +103,11 @@ function getUiInitializationScript(state: UiState) {
function RootDocument({
children,
initialLocale,
initialState,
}: Readonly<{
children: ReactNode
initialLocale: AppLocale
initialState: UiState
}>) {
const themeMode = initialState["theme-mode"]
@@ -110,7 +121,7 @@ function RootDocument({
return (
<html
lang="zh-CN"
lang={initialLocale === "zh-Hans" ? "zh-CN" : "en-US"}
suppressHydrationWarning
className={className || undefined}
style={themeMode === "system" ? undefined : { colorScheme: themeMode }}
+2
View File
@@ -8,6 +8,7 @@ import {
fetchMockNotifications,
notificationsQueryKey,
} from "@/data/notifications"
import { AppLanguageSwitcher } from "@/i18n/app-language-switcher"
export const Route = createFileRoute("/_app")({
component: ApplicationLayoutRoute,
@@ -17,6 +18,7 @@ function ApplicationLayoutRoute() {
return (
<AppLayout
chatThreads={chatThreads}
headerActions={<AppLanguageSwitcher />}
navigationGroups={primaryNavigationGroups}
notifications={{
queryFn: fetchMockNotifications,
@@ -0,0 +1,20 @@
import { createServerFn } from "@tanstack/react-start"
import { isAppLocale } from "@/i18n/app-locales"
import { readAppLocale, writeAppLocale } from "./app-locale.server"
export const getAppLocale = createServerFn({ method: "GET" }).handler(() => {
return readAppLocale()
})
export const setAppLocale = createServerFn({ method: "POST" })
.validator((value: unknown) => {
if (!isAppLocale(value)) {
throw new Error("Invalid app locale")
}
return value
})
.handler(({ data }) => {
writeAppLocale(data)
})
+25
View File
@@ -0,0 +1,25 @@
import "@tanstack/react-start/server-only"
import { getCookie, setCookie } from "@tanstack/react-start/server"
import {
DEFAULT_APP_LOCALE,
isAppLocale,
type AppLocale,
} from "@/i18n/app-locales"
const APP_LOCALE_COOKIE = "ui-locale"
const COOKIE_MAX_AGE = 60 * 60 * 24 * 365
export function readAppLocale(): AppLocale {
const locale = getCookie(APP_LOCALE_COOKIE)
return isAppLocale(locale) ? locale : DEFAULT_APP_LOCALE
}
export function writeAppLocale(locale: AppLocale) {
setCookie(APP_LOCALE_COOKIE, locale, {
maxAge: COOKIE_MAX_AGE,
path: "/",
sameSite: "lax",
})
}
+1 -2
View File
@@ -10,7 +10,6 @@
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
@@ -21,5 +20,5 @@
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts", "lingui.config.ts"]
"include": ["vite.config.ts"]
}
+16
View File
@@ -0,0 +1,16 @@
import path from "node:path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vitest/config"
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
tsconfigPaths: true,
},
test: {
environment: "jsdom",
},
})
+18
View File
@@ -30,8 +30,10 @@
"@workspace/blocks": "workspace:*",
"@workspace/i18n": "workspace:*",
"@workspace/ui": "workspace:*",
"country-flag-icons": "^1.6.20",
"lucide-react": "^1.27.0",
"react": "^19.2.6",
"react-day-picker": "^10.0.1",
"react-dom": "^19.2.6",
"sonner": "^2.0.7",
},
@@ -42,6 +44,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^6",
"jsdom": "^30.0.1",
"nitro": "^3.0.260610-beta",
"typescript": "~6",
"vite": "^8",
@@ -60,6 +63,7 @@
"@tanstack/react-hotkeys": "^0.10.0",
"@tanstack/react-query": "^5.101.4",
"@tanstack/react-router": "^1.170.18",
"@workspace/i18n": "workspace:*",
"@workspace/ui": "workspace:*",
"lucide-react": "^1.27.0",
"react": "^19.2.6",
@@ -86,7 +90,9 @@
"@lingui/core": "^6",
"@lingui/react": "^6",
"@vitejs/plugin-react": "^6",
"ansi-to-react": "^6.2.6",
"cac": "^7.0.0",
"lucide-react": "^1.27.0",
"open": "^11.0.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
@@ -687,12 +693,16 @@
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"anser": ["anser@2.3.5", "", {}, "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ=="],
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"ansi-to-react": ["ansi-to-react@6.2.6", "", { "dependencies": { "anser": "^2.3.2", "escape-carriage": "^1.3.1", "linkify-it": "^3.0.3" }, "peerDependencies": { "react": "^16.3.2 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.3.2 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Eqi0iaMK5OZ3jsVFxWvU2B74UZBnGuHlkflKMX6wTOeH+luy9KE2O0gUkc2PxhIP1R4IO0xohv62UMFInQOSeg=="],
"ansis": ["ansis@4.3.1", "", {}, "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
@@ -795,6 +805,8 @@
"cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="],
"country-flag-icons": ["country-flag-icons@1.6.20", "", {}, "sha512-py8JiEKzjhYw6HPJ0L7SxLgCYim36UPRTZX43/kqGueUCZLSvnrqAiwW8HtQibur7mdkFQUkjOgdK+o/9FBtaw=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"crossws": ["crossws@0.4.10", "", { "peerDependencies": { "srvx": ">=0.11.5" }, "optionalPeers": ["srvx"] }, "sha512-pz3oubH/dt12KjqsUB0IuXW4nwRDQ583iDsP4555Cpdqx0NoU7pGlWBcayyFI8f/l/idRpgjMEfwuOxSWJYlIA=="],
@@ -913,6 +925,8 @@
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-carriage": ["escape-carriage@1.3.1", "", {}, "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
@@ -1127,6 +1141,8 @@
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"linkify-it": ["linkify-it@3.0.3", "", { "dependencies": { "uc.micro": "^1.0.1" } }, "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ=="],
"local-pkg": ["local-pkg@1.2.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q=="],
"locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
@@ -1453,6 +1469,8 @@
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"uc.micro": ["uc.micro@1.0.6", "", {}, "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="],
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
"undici": ["undici@8.9.0", "", {}, "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA=="],
+12 -1
View File
@@ -17,6 +17,7 @@
"@tanstack/react-hotkeys": "^0.10.0",
"@tanstack/react-query": "^5.101.4",
"@tanstack/react-router": "^1.170.18",
"@workspace/i18n": "workspace:*",
"@workspace/ui": "workspace:*",
"lucide-react": "^1.27.0",
"react": "^19.2.6",
@@ -33,12 +34,22 @@
"exports": {
"./globals.css": "./src/styles/globals.css",
"./appearance": "./src/blocks/appearance/index.ts",
"./appearance/locales": "./src/blocks/appearance/locales/catalogs.ts",
"./appearance/locales/*": "./src/blocks/appearance/locales/*.ts",
"./chats": "./src/blocks/chats/index.ts",
"./chats/locales": "./src/blocks/chats/locales/catalogs.ts",
"./chats/locales/*": "./src/blocks/chats/locales/*.ts",
"./components/*": "./src/components/*.tsx",
"./hooks/*": "./src/hooks/*.ts",
"./lib/*": "./src/lib/*.tsx",
"./layout": "./src/blocks/layout/index.ts",
"./layout/locales": "./src/blocks/layout/locales/catalogs.ts",
"./layout/locales/*": "./src/blocks/layout/locales/*.ts",
"./navigation": "./src/blocks/navigation/index.ts",
"./notifications": "./src/blocks/notifications/index.ts"
"./navigation/locales": "./src/blocks/navigation/locales/catalogs.ts",
"./navigation/locales/*": "./src/blocks/navigation/locales/*.ts",
"./notifications": "./src/blocks/notifications/index.ts",
"./notifications/locales": "./src/blocks/notifications/locales/catalogs.ts",
"./notifications/locales/*": "./src/blocks/notifications/locales/*.ts"
}
}
@@ -1,5 +1,6 @@
import { useCallback, useState } from "react"
import { useHotkey } from "@tanstack/react-hotkeys"
import { useMessage } from "@workspace/i18n"
import { useResolvedTheme, useUiState } from "./ui-state"
import {
Dialog,
@@ -15,6 +16,62 @@ import {
import { appearanceDialogHandle } from "./dialog"
import { CompactPreference, ModePreference } from "./preferences"
import { ThemeConfigPanel } from "./theme-config-panel"
import { LocalizedText } from "../../components/localized-text"
import { appearanceMessages } from "./messages"
interface AppearanceHotkeysProps {
onToggleDialog: VoidFunction
onToggleTheme: VoidFunction
}
function AppearanceHotkeys({
onToggleDialog,
onToggleTheme,
}: AppearanceHotkeysProps) {
const appearanceTitle = useMessage(appearanceMessages.appearance)
const appearanceDescription = useMessage(
appearanceMessages.appearanceCommandDescription
)
const toggleThemeTitle = useMessage(appearanceMessages.toggleTheme)
const toggleThemeDescription = useMessage(
appearanceMessages.toggleThemeDescription
)
useHotkey("Mod+,", onToggleDialog, {
ignoreInputs: true,
preventDefault: true,
stopPropagation: false,
meta: {
name: appearanceTitle,
description: appearanceDescription,
},
})
useHotkey("Mod+D", onToggleTheme, {
ignoreInputs: true,
preventDefault: true,
stopPropagation: false,
meta: {
name: toggleThemeTitle,
description: toggleThemeDescription,
},
})
return null
}
function AppearanceDialogHeader() {
return (
<DialogHeader>
<DialogTitle>
<LocalizedText message={appearanceMessages.appearance} />
</DialogTitle>
<DialogDescription>
<LocalizedText message={appearanceMessages.themeAndLayoutPreferences} />
</DialogDescription>
</DialogHeader>
)
}
export function AppearanceController() {
const [isOpen, setIsOpen] = useState(false)
@@ -33,37 +90,18 @@ export function AppearanceController() {
setThemeMode(resolvedTheme === "dark" ? "light" : "dark")
}, [resolvedTheme, setThemeMode])
useHotkey("Mod+,", toggleDialog, {
ignoreInputs: true,
preventDefault: true,
stopPropagation: false,
meta: {
name: "界面外观",
description: "打开主题与布局偏好",
},
})
useHotkey("Mod+D", toggleTheme, {
ignoreInputs: true,
preventDefault: true,
stopPropagation: false,
meta: {
name: "切换主题",
description: "在浅色与深色主题之间切换",
},
})
return (
<Dialog handle={appearanceDialogHandle} onOpenChange={setIsOpen}>
<AppearanceHotkeys
onToggleDialog={toggleDialog}
onToggleTheme={toggleTheme}
/>
<NavigationActionTarget
action={navigationActions.appearance}
onInvoke={() => appearanceDialogHandle.open(null)}
/>
<DialogContent className="sm:max-w-max">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<AppearanceDialogHeader />
<div className="-mx-6 max-h-[75vh] space-y-10 overflow-y-auto px-6">
<div className="flex flex-col gap-6 lg:flex-row">
<ModePreference />
@@ -73,13 +111,8 @@ export function AppearanceController() {
<ThemeConfigPanel
scheme="light"
active={resolvedTheme === "light"}
description="当系统设置为浅色模式时,将使用此主题"
/>
<ThemeConfigPanel
scheme="dark"
active={resolvedTheme === "dark"}
description="当系统设置为深色模式时,将使用此主题"
/>
<ThemeConfigPanel scheme="dark" active={resolvedTheme === "dark"} />
</div>
</div>
</DialogContent>
@@ -0,0 +1,35 @@
import { describe, it } from "vitest"
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
import { appearanceMessages, themeColorMessages } from "../messages"
import { appearanceCatalogLocales } from "./catalogs"
import { messages as de } from "./de"
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 zhHant } from "./zh-Hant"
describe("appearance locale catalogs", () => {
it("ship every appearance message in every built-in locale", () => {
expectCompleteCatalogs({
catalogs: {
de,
en,
es,
fr,
ja,
ko,
"zh-Hans": zhHans,
"zh-Hant": zhHant,
},
locales: appearanceCatalogLocales,
messageIds: [
...Object.values(appearanceMessages),
...Object.values(themeColorMessages),
].map((descriptor) => descriptor.id),
})
})
})
@@ -0,0 +1,12 @@
import type { appearanceMessages, themeColorMessages } from "../messages"
import {
blockCatalogLocales,
type BlockCatalogLocale,
type BlockMessageCatalog,
} from "../../../i18n/catalogs"
export { blockCatalogLocales as appearanceCatalogLocales }
export type AppearanceCatalogLocale = BlockCatalogLocale
export type AppearanceMessageCatalog = BlockMessageCatalog<
typeof appearanceMessages & typeof themeColorMessages
>
@@ -0,0 +1,53 @@
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
@@ -0,0 +1,25 @@
import type { MessageDescriptor } from "@workspace/i18n"
import { appearanceMessages, themeColorMessages } from "../messages.ts"
import type { AppearanceMessageCatalog } from "./catalogs"
export const locale = "en"
export const languageTag = "en-US"
function createSourceCatalog(
...groups: readonly Record<string, MessageDescriptor>[]
): AppearanceMessageCatalog {
return Object.fromEntries(
groups.flatMap((group) =>
Object.values(group).map((descriptor) => [
descriptor.id,
descriptor.message,
])
)
) as AppearanceMessageCatalog
}
export const messages = createSourceCatalog(
appearanceMessages,
themeColorMessages
) satisfies AppearanceMessageCatalog
@@ -0,0 +1,52 @@
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
@@ -0,0 +1,52 @@
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
@@ -0,0 +1,52 @@
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
@@ -0,0 +1,52 @@
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
@@ -0,0 +1,52 @@
import type { AppearanceMessageCatalog } from "./catalogs"
export const locale = "zh-Hans"
export const languageTag = "zh-CN"
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
@@ -0,0 +1,52 @@
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
@@ -0,0 +1,187 @@
import type { MessageDescriptor } from "@workspace/i18n"
export const appearanceMessages = {
active: /* i18n */ {
id: "blocks.appearance.active",
message: "Active",
},
appearance: /* i18n */ {
id: "blocks.appearance.title",
message: "Appearance",
},
appearanceCommandDescription: /* i18n */ {
id: "blocks.appearance.command.description",
message: "Open theme and layout preferences",
},
baseColor: /* i18n */ {
id: "blocks.appearance.baseColor",
message: "Base color",
},
compactDescription: /* i18n */ {
id: "blocks.appearance.compact.description",
message: "Use a compact width for the main page content",
},
compactLayout: /* i18n */ {
id: "blocks.appearance.compact.title",
message: "Compact layout",
},
darkMode: /* i18n */ {
id: "blocks.appearance.mode.dark",
message: "Dark",
},
darkModeDescription: /* i18n */ {
id: "blocks.appearance.mode.dark.description",
message: "The interface will always use the dark theme",
},
darkTheme: /* i18n */ {
id: "blocks.appearance.theme.dark",
message: "Dark theme",
},
darkThemeDescription: /* i18n */ {
id: "blocks.appearance.theme.dark.description",
message: "This theme is used when the system is in dark mode",
},
followSystem: /* i18n */ {
id: "blocks.appearance.mode.system",
message: "System",
},
followSystemDescription: /* i18n */ {
id: "blocks.appearance.mode.system.description",
message: "The interface theme will follow the system appearance",
},
lightMode: /* i18n */ {
id: "blocks.appearance.mode.light",
message: "Light",
},
lightModeDescription: /* i18n */ {
id: "blocks.appearance.mode.light.description",
message: "The interface will always use the light theme",
},
lightTheme: /* i18n */ {
id: "blocks.appearance.theme.light",
message: "Light theme",
},
lightThemeDescription: /* i18n */ {
id: "blocks.appearance.theme.light.description",
message: "This theme is used when the system is in light mode",
},
primaryColor: /* i18n */ {
id: "blocks.appearance.primaryColor",
message: "Primary color",
},
themeAndLayoutPreferences: /* i18n */ {
id: "blocks.appearance.description",
message: "Theme and layout preferences",
},
themeMode: /* i18n */ {
id: "blocks.appearance.mode.title",
message: "Theme mode",
},
toggleTheme: /* i18n */ {
id: "blocks.appearance.toggleTheme",
message: "Toggle theme",
},
toggleThemeDescription: /* i18n */ {
id: "blocks.appearance.toggleTheme.description",
message: "Switch between light and dark themes",
},
} as const satisfies Record<string, MessageDescriptor>
export const themeColorMessages = {
amber: /* i18n */ {
id: "blocks.appearance.color.amber",
message: "Amber",
},
blue: /* i18n */ {
id: "blocks.appearance.color.blue",
message: "Blue",
},
cyan: /* i18n */ {
id: "blocks.appearance.color.cyan",
message: "Cyan",
},
emerald: /* i18n */ {
id: "blocks.appearance.color.emerald",
message: "Emerald",
},
fuchsia: /* i18n */ {
id: "blocks.appearance.color.fuchsia",
message: "Fuchsia",
},
green: /* i18n */ {
id: "blocks.appearance.color.green",
message: "Green",
},
indigo: /* i18n */ {
id: "blocks.appearance.color.indigo",
message: "Indigo",
},
lime: /* i18n */ {
id: "blocks.appearance.color.lime",
message: "Lime",
},
mauve: /* i18n */ {
id: "blocks.appearance.color.mauve",
message: "Mauve",
},
mist: /* i18n */ {
id: "blocks.appearance.color.mist",
message: "Mist",
},
neutral: /* i18n */ {
id: "blocks.appearance.color.neutral",
message: "Neutral",
},
olive: /* i18n */ {
id: "blocks.appearance.color.olive",
message: "Olive",
},
orange: /* i18n */ {
id: "blocks.appearance.color.orange",
message: "Orange",
},
pink: /* i18n */ {
id: "blocks.appearance.color.pink",
message: "Pink",
},
purple: /* i18n */ {
id: "blocks.appearance.color.purple",
message: "Purple",
},
red: /* i18n */ {
id: "blocks.appearance.color.red",
message: "Red",
},
rose: /* i18n */ {
id: "blocks.appearance.color.rose",
message: "Rose",
},
sky: /* i18n */ {
id: "blocks.appearance.color.sky",
message: "Sky",
},
stone: /* i18n */ {
id: "blocks.appearance.color.stone",
message: "Stone",
},
taupe: /* i18n */ {
id: "blocks.appearance.color.taupe",
message: "Taupe",
},
teal: /* i18n */ {
id: "blocks.appearance.color.teal",
message: "Teal",
},
violet: /* i18n */ {
id: "blocks.appearance.color.violet",
message: "Violet",
},
yellow: /* i18n */ {
id: "blocks.appearance.color.yellow",
message: "Yellow",
},
zinc: /* i18n */ {
id: "blocks.appearance.color.zinc",
message: "Zinc",
},
} as const satisfies Record<string, MessageDescriptor>
@@ -1,4 +1,5 @@
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react"
import { useMessage } from "@workspace/i18n"
import {
Select,
SelectContent,
@@ -10,12 +11,8 @@ import { Switch } from "@workspace/ui/components/switch"
import type { ThemeMode } from "./state"
import { useUiState } from "./ui-state"
const modeItems: ReadonlyArray<{ label: string; value: ThemeMode }> = [
{ label: "使用浅色", value: "light" },
{ label: "使用深色", value: "dark" },
{ label: "跟随系统", value: "system" },
]
import { LocalizedText } from "../../components/localized-text"
import { appearanceMessages } from "./messages"
function ModeIcon({ mode }: { mode: ThemeMode | null }) {
if (mode === "light") return <SunIcon />
@@ -25,15 +22,29 @@ function ModeIcon({ mode }: { mode: ThemeMode | null }) {
export function ModePreference() {
const [themeMode, setThemeMode] = useUiState("theme-mode")
const lightLabel = useMessage(appearanceMessages.lightMode)
const darkLabel = useMessage(appearanceMessages.darkMode)
const systemLabel = useMessage(appearanceMessages.followSystem)
const lightDescription = useMessage(appearanceMessages.lightModeDescription)
const darkDescription = useMessage(appearanceMessages.darkModeDescription)
const systemDescription = useMessage(
appearanceMessages.followSystemDescription
)
const title = useMessage(appearanceMessages.themeMode)
const modeItems: ReadonlyArray<{ label: string; value: ThemeMode }> = [
{ label: lightLabel, value: "light" },
{ label: darkLabel, value: "dark" },
{ label: systemLabel, value: "system" },
]
const description = {
light: "界面将始终使用浅色主题",
dark: "界面将始终使用深色主题",
system: "界面主题将跟随系统外观设置",
light: lightDescription,
dark: darkDescription,
system: systemDescription,
}[themeMode]
return (
<div className="lg:flex-1">
<h2 className="font-semibold"></h2>
<h2 className="font-semibold">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
<Select
items={modeItems}
@@ -47,9 +58,7 @@ export function ModePreference() {
{(value: ThemeMode | null) => (
<>
<ModeIcon mode={value} />
{modeItems
.find((item) => item.value === value)
?.label.slice(-2)}
{modeItems.find((item) => item.value === value)?.label}
</>
)}
</SelectValue>
@@ -72,9 +81,11 @@ export function CompactPreference() {
return (
<div className="lg:flex-1">
<h2 className="font-semibold"></h2>
<h2 className="font-semibold">
<LocalizedText message={appearanceMessages.compactLayout} />
</h2>
<p className="mt-1 text-sm text-muted-foreground">
使
<LocalizedText message={appearanceMessages.compactDescription} />
</p>
<Switch
className="mt-2"
@@ -1,5 +1,7 @@
import type { CSSProperties } from "react"
import type { CSSProperties, ReactNode } from "react"
import { useMessage } from "@workspace/i18n"
import { cn } from "@workspace/ui/lib/utils"
import { themeColorMessages } from "./messages"
import {
accentColors,
baseColors,
@@ -12,7 +14,6 @@ type ThemeColor = BaseColor | AccentColor
type ThemeColorSwatch<T extends ThemeColor> = {
name: T
title: string
light: {
background: string
foreground: string
@@ -31,7 +32,6 @@ function createSwatches<T extends ThemeColor>(
return {
name,
title: theme.title,
light: {
background: theme.cssVars.light.primary,
foreground: theme.cssVars.light["primary-foreground"],
@@ -54,7 +54,7 @@ export function ThemeColorPicker<T extends ThemeColor>({
onChange,
}: {
swatches: readonly ThemeColorSwatch<T>[]
title: string
title: ReactNode
value: T
onChange: (color: T) => void
}) {
@@ -86,6 +86,7 @@ function ThemeColorButton({
active: boolean
onClick: () => void
}) {
const swatchTitle = useMessage(themeColorMessages[swatch.name])
const style = {
"--light-bg": swatch.light.background,
"--light-fg": swatch.light.foreground,
@@ -96,7 +97,7 @@ function ThemeColorButton({
return (
<button
type="button"
aria-label={swatch.title}
aria-label={swatchTitle}
aria-pressed={active}
className={cn(
"h-8 min-w-8 appearance-none truncate overflow-hidden rounded-4xl border-2 px-2 text-xs whitespace-nowrap ring-popover ring-inset",
@@ -108,7 +109,7 @@ function ThemeColorButton({
style={style}
onClick={onClick}
>
{swatch.title}
{swatchTitle}
</button>
)
}
@@ -8,18 +8,26 @@ import {
} from "./theme-color-picker"
import { ThemePreview } from "./theme-preview"
import type { ThemeScheme } from "./theme"
import { LocalizedText } from "../../components/localized-text"
import { appearanceMessages } from "./messages"
export function ThemeConfigPanel({
scheme,
active,
description,
}: {
scheme: ThemeScheme
active: boolean
description: string
}) {
const stateKey = scheme === "light" ? "theme-light" : "theme-dark"
const [config, setConfig] = useUiState(stateKey)
const titleMessage =
scheme === "light"
? appearanceMessages.lightTheme
: appearanceMessages.darkTheme
const descriptionMessage =
scheme === "light"
? appearanceMessages.lightThemeDescription
: appearanceMessages.darkThemeDescription
return (
<div className="w-md rounded-xl border">
@@ -29,15 +37,23 @@ export function ThemeConfigPanel({
) : (
<MoonIcon className="size-5" />
)}
<span>{scheme === "light" ? "浅色主题" : "深色主题"}</span>
{active && <Badge className="ml-auto">使</Badge>}
<span>
<LocalizedText message={titleMessage} />
</span>
{active && (
<Badge className="ml-auto">
<LocalizedText message={appearanceMessages.active} />
</Badge>
)}
</div>
<div className="space-y-4 p-4">
<p>{description}</p>
<p>
<LocalizedText message={descriptionMessage} />
</p>
<ThemePreview scheme={scheme} config={config} />
<ThemeColorPicker
swatches={baseColorSwatches}
title="基础颜色"
title={<LocalizedText message={appearanceMessages.baseColor} />}
value={config.baseColor}
onChange={(baseColor) =>
setConfig((current) => ({ ...current, baseColor }))
@@ -45,7 +61,7 @@ export function ThemeConfigPanel({
/>
<ThemeColorPicker
swatches={accentColorSwatches}
title="主要颜色"
title={<LocalizedText message={appearanceMessages.primaryColor} />}
value={config.accentColor}
onChange={(accentColor) =>
setConfig((current) => ({ ...current, accentColor }))
@@ -14,6 +14,7 @@ describe("appearance themes", () => {
expect(themes.map((theme) => theme.name)).toEqual(selectableColors)
expect(new Set(selectableColors).size).toBe(selectableColors.length)
expect(new Set(themes.map((theme) => theme.name)).size).toBe(themes.length)
expect(themes.every((theme) => theme.title.length > 0)).toBe(true)
})
+24 -24
View File
@@ -69,7 +69,7 @@ export type Theme = {
export const themes: Theme[] = [
{
name: "neutral",
title: "中性灰",
title: "Neutral",
cssVars: {
light: {
background: "oklch(1 0 0)",
@@ -142,7 +142,7 @@ export const themes: Theme[] = [
},
{
name: "stone",
title: "石灰",
title: "Stone",
cssVars: {
light: {
background: "oklch(1 0 0)",
@@ -215,7 +215,7 @@ export const themes: Theme[] = [
},
{
name: "zinc",
title: "锌灰",
title: "Zinc",
cssVars: {
light: {
background: "oklch(1 0 0)",
@@ -288,7 +288,7 @@ export const themes: Theme[] = [
},
{
name: "mauve",
title: "紫灰",
title: "Mauve",
cssVars: {
light: {
background: "oklch(1 0 0)",
@@ -361,7 +361,7 @@ export const themes: Theme[] = [
},
{
name: "olive",
title: "橄榄灰",
title: "Olive",
cssVars: {
light: {
background: "oklch(1 0 0)",
@@ -434,7 +434,7 @@ export const themes: Theme[] = [
},
{
name: "mist",
title: "雾灰",
title: "Mist",
cssVars: {
light: {
background: "oklch(1 0 0)",
@@ -507,7 +507,7 @@ export const themes: Theme[] = [
},
{
name: "taupe",
title: "灰褐",
title: "Taupe",
cssVars: {
light: {
background: "oklch(1 0 0)",
@@ -580,7 +580,7 @@ export const themes: Theme[] = [
},
{
name: "amber",
title: "琥珀色",
title: "Amber",
cssVars: {
light: {
primary: "oklch(0.555 0.163 48.998)",
@@ -612,7 +612,7 @@ export const themes: Theme[] = [
},
{
name: "blue",
title: "蓝色",
title: "Blue",
cssVars: {
light: {
primary: "oklch(0.488 0.243 264.376)",
@@ -644,7 +644,7 @@ export const themes: Theme[] = [
},
{
name: "cyan",
title: "青色",
title: "Cyan",
cssVars: {
light: {
primary: "oklch(0.52 0.105 223.128)",
@@ -676,7 +676,7 @@ export const themes: Theme[] = [
},
{
name: "emerald",
title: "翠绿色",
title: "Emerald",
cssVars: {
light: {
primary: "oklch(0.508 0.118 165.612)",
@@ -708,7 +708,7 @@ export const themes: Theme[] = [
},
{
name: "fuchsia",
title: "紫红色",
title: "Fuchsia",
cssVars: {
light: {
primary: "oklch(0.518 0.253 323.949)",
@@ -740,7 +740,7 @@ export const themes: Theme[] = [
},
{
name: "green",
title: "绿色",
title: "Green",
cssVars: {
light: {
primary: "oklch(0.527 0.154 150.069)",
@@ -772,7 +772,7 @@ export const themes: Theme[] = [
},
{
name: "indigo",
title: "靛蓝色",
title: "Indigo",
cssVars: {
light: {
primary: "oklch(0.457 0.24 277.023)",
@@ -804,7 +804,7 @@ export const themes: Theme[] = [
},
{
name: "lime",
title: "青柠色",
title: "Lime",
cssVars: {
light: {
primary: "oklch(0.841 0.238 128.85)",
@@ -836,7 +836,7 @@ export const themes: Theme[] = [
},
{
name: "orange",
title: "橙色",
title: "Orange",
cssVars: {
light: {
primary: "oklch(0.553 0.195 38.402)",
@@ -868,7 +868,7 @@ export const themes: Theme[] = [
},
{
name: "pink",
title: "粉色",
title: "Pink",
cssVars: {
light: {
primary: "oklch(0.525 0.223 3.958)",
@@ -900,7 +900,7 @@ export const themes: Theme[] = [
},
{
name: "purple",
title: "紫色",
title: "Purple",
cssVars: {
light: {
primary: "oklch(0.496 0.265 301.924)",
@@ -932,7 +932,7 @@ export const themes: Theme[] = [
},
{
name: "red",
title: "红色",
title: "Red",
cssVars: {
light: {
primary: "oklch(0.505 0.213 27.518)",
@@ -964,7 +964,7 @@ export const themes: Theme[] = [
},
{
name: "rose",
title: "玫红色",
title: "Rose",
cssVars: {
light: {
primary: "oklch(0.514 0.222 16.935)",
@@ -997,7 +997,7 @@ export const themes: Theme[] = [
},
{
name: "sky",
title: "天蓝色",
title: "Sky",
cssVars: {
light: {
primary: "oklch(0.5 0.134 242.749)",
@@ -1029,7 +1029,7 @@ export const themes: Theme[] = [
},
{
name: "teal",
title: "蓝绿色",
title: "Teal",
cssVars: {
light: {
primary: "oklch(0.511 0.096 186.391)",
@@ -1061,7 +1061,7 @@ export const themes: Theme[] = [
},
{
name: "violet",
title: "紫罗兰色",
title: "Violet",
cssVars: {
light: {
primary: "oklch(0.491 0.27 292.581)",
@@ -1093,7 +1093,7 @@ export const themes: Theme[] = [
},
{
name: "yellow",
title: "黄色",
title: "Yellow",
cssVars: {
light: {
primary: "oklch(0.852 0.199 91.936)",
@@ -1,3 +1,4 @@
import { useMessage } from "@workspace/i18n"
import { ItemGroup } from "@workspace/ui/components/item"
import {
Popover,
@@ -8,6 +9,7 @@ import {
} from "@workspace/ui/components/popover"
import { useOnScroll } from "../../hooks/use-on-scroll"
import { chatMessages } from "./messages"
import { chatPopoverHandle } from "./chat-popover-handle"
import { ChatThreadItem } from "./chat-thread-item"
@@ -18,7 +20,9 @@ export interface ChatPopoverProps {
title?: string
}
export function ChatPopover({ threads, title = "Chats" }: ChatPopoverProps) {
export function ChatPopover({ threads, title }: ChatPopoverProps) {
const defaultTitle = useMessage(chatMessages.title)
useOnScroll(() => {
chatPopoverHandle.close()
})
@@ -28,7 +32,7 @@ export function ChatPopover({ threads, title = "Chats" }: ChatPopoverProps) {
<PopoverContent className="h-120 p-2" alignOffset={-50} showArrow>
<PopoverHeader className="px-4 pt-4">
<PopoverTitle className="text-lg">
{title}({threads.length})
{title ?? defaultTitle}({threads.length})
</PopoverTitle>
<PopoverDescription />
</PopoverHeader>
@@ -0,0 +1,34 @@
import { describe, it } from "vitest"
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
import { chatMessages } from "../messages"
import { chatCatalogLocales } from "./catalogs"
import { messages as de } from "./de"
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 zhHant } from "./zh-Hant"
describe("chat locale catalogs", () => {
it("ship every chat message in every built-in locale", () => {
expectCompleteCatalogs({
catalogs: {
de,
en,
es,
fr,
ja,
ko,
"zh-Hans": zhHans,
"zh-Hant": zhHant,
},
locales: chatCatalogLocales,
messageIds: Object.values(chatMessages).map(
(descriptor) => descriptor.id
),
})
})
})
@@ -0,0 +1,10 @@
import type { chatMessages } from "../messages"
import {
blockCatalogLocales,
type BlockCatalogLocale,
type BlockMessageCatalog,
} from "../../../i18n/catalogs"
export { blockCatalogLocales as chatCatalogLocales }
export type ChatCatalogLocale = BlockCatalogLocale
export type ChatMessageCatalog = BlockMessageCatalog<typeof chatMessages>
@@ -0,0 +1,7 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "de"
export const languageTag = "de-DE"
export const messages = {
"blocks.chats.title": "Chats",
} as const satisfies ChatMessageCatalog
@@ -0,0 +1,7 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "en"
export const languageTag = "en-US"
export const messages = {
"blocks.chats.title": "Chats",
} as const satisfies ChatMessageCatalog
@@ -0,0 +1,7 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "es"
export const languageTag = "es-ES"
export const messages = {
"blocks.chats.title": "Chats",
} as const satisfies ChatMessageCatalog
@@ -0,0 +1,7 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "fr"
export const languageTag = "fr-FR"
export const messages = {
"blocks.chats.title": "Discussions",
} as const satisfies ChatMessageCatalog
@@ -0,0 +1,7 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "ja"
export const languageTag = "ja-JP"
export const messages = {
"blocks.chats.title": "チャット",
} as const satisfies ChatMessageCatalog
@@ -0,0 +1,7 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "ko"
export const languageTag = "ko-KR"
export const messages = {
"blocks.chats.title": "채팅",
} as const satisfies ChatMessageCatalog
@@ -0,0 +1,7 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "zh-Hans"
export const languageTag = "zh-CN"
export const messages = {
"blocks.chats.title": "聊天",
} as const satisfies ChatMessageCatalog
@@ -0,0 +1,7 @@
import type { ChatMessageCatalog } from "./catalogs"
export const locale = "zh-Hant"
export const languageTag = "zh-TW"
export const messages = {
"blocks.chats.title": "聊天",
} as const satisfies ChatMessageCatalog
@@ -0,0 +1,8 @@
import type { MessageDescriptor } from "@workspace/i18n"
export const chatMessages = {
title: /* i18n */ {
id: "blocks.chats.title",
message: "Chats",
},
} as const satisfies Record<string, MessageDescriptor>
@@ -1,5 +1,6 @@
import { Link } from "@tanstack/react-router"
import * as React from "react"
import { useMessage } from "@workspace/i18n"
import {
Breadcrumb,
BreadcrumbEllipsis,
@@ -21,8 +22,13 @@ import {
DropdownMenuTrigger,
} from "@workspace/ui/components/dropdown-menu"
import { type NavigationInfo, useNavigationStack } from "../navigation"
import {
NavigationLabel,
type NavigationInfo,
useNavigationStack,
} from "../navigation"
import { Icon } from "../../components/icon"
import { layoutMessages } from "./messages"
const MAX_VISIBLE_ENTRIES = 3
@@ -185,7 +191,7 @@ function BreadcrumbEntryContent({ entry }: { entry: BreadcrumbEntry }) {
return (
<>
{entry.icon && <Icon data={entry.icon} className="size-4 shrink-0" />}
{entry.label}
<NavigationLabel label={entry.label} />
</>
)
}
@@ -303,11 +309,13 @@ function BreadcrumbRouteLink({
}
function BreadcrumbMenu({ entries }: { entries: readonly BreadcrumbEntry[] }) {
const menuLabel = useMessage(layoutMessages.breadcrumbMenuLabel)
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button aria-label="打开面包屑菜单" size="icon-sm" variant="ghost" />
<Button aria-label={menuLabel} size="icon-sm" variant="ghost" />
}
>
<BreadcrumbEllipsis />
@@ -320,7 +328,7 @@ function BreadcrumbMenu({ entries }: { entries: readonly BreadcrumbEntry[] }) {
{entry.icon && (
<Icon data={entry.icon} className="size-4 shrink-0" />
)}
{entry.label}
<NavigationLabel label={entry.label} />
</>
)
@@ -405,7 +413,9 @@ export function AppBreadcrumb() {
className="size-4 shrink-0"
/>
)}
<span className="truncate">{item.entry.label}</span>
<span className="truncate">
<NavigationLabel label={item.entry.label} />
</span>
</BreadcrumbPage>
) : (
<BreadcrumbRouteLink
@@ -432,7 +442,7 @@ export function AppBreadcrumb() {
{entry.icon && (
<Icon data={entry.icon} className="size-4 shrink-0" />
)}
{entry.label}
<NavigationLabel label={entry.label} />
</span>
))}
<span data-breadcrumb-measure-menu>
@@ -38,6 +38,7 @@ import { AppQueryIndicator } from "./app-query-indicator"
export interface AppLayoutProps {
chatThreads: readonly ChatThread[]
children: React.ReactNode
headerActions?: React.ReactNode
navigationGroups: readonly NavigationGroup[]
notifications: UseNotificationsOptions
}
@@ -45,6 +46,7 @@ export interface AppLayoutProps {
export function AppLayout({
chatThreads,
children,
headerActions,
navigationGroups,
notifications,
}: AppLayoutProps) {
@@ -55,7 +57,7 @@ export function AppLayout({
<NotificationCenterSheet {...notifications} />
<ChatPopover threads={chatThreads} />
<div className="isolate min-h-svh w-svw">
<AppHeader />
<AppHeader actions={headerActions} />
<div className="relative z-1 flex min-h-svh w-svw items-start">
<AppHeaderBackground />
<AppSidebar groups={navigationGroups} />
@@ -66,7 +68,7 @@ export function AppLayout({
)
}
function AppHeader() {
function AppHeader({ actions }: { actions?: React.ReactNode }) {
return (
<header className="sticky top-0 z-3 h-0">
<div className="flex h-(--ui-header-height) items-center">
@@ -157,6 +159,7 @@ function AppHeader() {
</div>
<div className="ml-auto flex shrink-0 items-center gap-2">
<GlobalSearchTrigger />
{actions}
<NotificationCenterTrigger
render={<HeaderActionButton icon={BellIcon} showIndicator />}
/>
@@ -1,3 +1,4 @@
import { useMessage } from "@workspace/i18n"
import { useRefresh } from "../../hooks/use-refresh"
import {
Tooltip,
@@ -7,9 +8,12 @@ import {
import { HeaderActionButton } from "./header-action-button"
import { ReloadIcon } from "@hugeicons/core-free-icons"
import { cn } from "@workspace/ui/lib/utils"
import { layoutMessages } from "./messages"
export function AppQueryIndicator({ className }: { className?: string }) {
const { canRefresh, isRefreshing, refresh } = useRefresh()
const refreshLabel = useMessage(layoutMessages.refreshLabel)
const refreshDescription = useMessage(layoutMessages.refreshDescription)
if (isRefreshing) {
return (
@@ -29,7 +33,7 @@ export function AppQueryIndicator({ className }: { className?: string }) {
<TooltipTrigger
render={
<HeaderActionButton
aria-label="刷新当前页面数据"
aria-label={refreshLabel}
className={cn("[&_svg]:size-4", className)}
disabled={!canRefresh}
icon={ReloadIcon}
@@ -37,11 +41,7 @@ export function AppQueryIndicator({ className }: { className?: string }) {
/>
}
/>
<TooltipContent showArrow>
<br />
</TooltipContent>
<TooltipContent showArrow>{refreshDescription}</TooltipContent>
</Tooltip>
)
}
@@ -0,0 +1,34 @@
import { describe, it } from "vitest"
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
import { layoutMessages } from "../messages"
import { layoutCatalogLocales } from "./catalogs"
import { messages as de } from "./de"
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 zhHant } from "./zh-Hant"
describe("layout locale catalogs", () => {
it("ship every layout message in every built-in locale", () => {
expectCompleteCatalogs({
catalogs: {
de,
en,
es,
fr,
ja,
ko,
"zh-Hans": zhHans,
"zh-Hant": zhHant,
},
locales: layoutCatalogLocales,
messageIds: Object.values(layoutMessages).map(
(descriptor) => descriptor.id
),
})
})
})
@@ -0,0 +1,10 @@
import type { layoutMessages } from "../messages"
import {
blockCatalogLocales,
type BlockCatalogLocale,
type BlockMessageCatalog,
} from "../../../i18n/catalogs"
export { blockCatalogLocales as layoutCatalogLocales }
export type LayoutCatalogLocale = BlockCatalogLocale
export type LayoutMessageCatalog = BlockMessageCatalog<typeof layoutMessages>
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,15 @@
import type { LayoutMessageCatalog } from "./catalogs"
export const locale = "en"
export const languageTag = "en-US"
export const messages = {
"blocks.breadcrumb.menuLabel": "Open breadcrumb menu",
"blocks.refresh.description": "Refresh the current page data",
"blocks.refresh.label": "Refresh current page data",
"blocks.userMenu.accountPassword": "Account password",
"blocks.userMenu.appearance": "Appearance",
"blocks.userMenu.keybindings": "Keybindings",
"blocks.userMenu.logout": "Log out",
"blocks.userMenu.open": "Open user menu",
"blocks.userMenu.title": "User menu",
} as const satisfies LayoutMessageCatalog
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,15 @@
import type { LayoutMessageCatalog } from "./catalogs"
export const locale = "zh-Hans"
export const languageTag = "zh-CN"
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,15 @@
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,40 @@
import type { MessageDescriptor } from "@workspace/i18n"
export const layoutMessages = {
accountPassword: /* i18n */ {
id: "blocks.userMenu.accountPassword",
message: "Account password",
},
appearance: /* i18n */ {
id: "blocks.userMenu.appearance",
message: "Appearance",
},
breadcrumbMenuLabel: /* i18n */ {
id: "blocks.breadcrumb.menuLabel",
message: "Open breadcrumb menu",
},
keybindings: /* i18n */ {
id: "blocks.userMenu.keybindings",
message: "Keybindings",
},
logout: /* i18n */ {
id: "blocks.userMenu.logout",
message: "Log out",
},
openUserMenu: /* i18n */ {
id: "blocks.userMenu.open",
message: "Open user menu",
},
refreshDescription: /* i18n */ {
id: "blocks.refresh.description",
message: "Refresh the current page data",
},
refreshLabel: /* i18n */ {
id: "blocks.refresh.label",
message: "Refresh current page data",
},
userMenuTitle: /* i18n */ {
id: "blocks.userMenu.title",
message: "User menu",
},
} as const satisfies Record<string, MessageDescriptor>
+37 -22
View File
@@ -1,4 +1,5 @@
import { formatForDisplay } from "@tanstack/react-hotkeys"
import { useMessage } from "@workspace/i18n"
import { Button, type ButtonProps } from "@workspace/ui/components/button"
import {
createDialogHandle,
@@ -41,8 +42,10 @@ import {
ShieldKeyIcon,
SwatchIcon,
} from "@hugeicons/core-free-icons"
import { layoutMessages } from "./messages"
interface UserMenuItem {
id: string
action?: NavigationAction
shortcut?: string
icon: IconData
@@ -53,24 +56,6 @@ interface UserMenuTriggerProps {
className?: string
}
const userMenuItems: readonly UserMenuItem[] = [
{
action: navigationActions.appearance,
icon: SwatchIcon,
label: "界面外观",
shortcut: "Mod+,",
},
{
icon: KeyboardIcon,
label: "按键映射",
},
{
icon: ShieldKeyIcon,
label: "账号密码",
shortcut: "Mod+P",
},
]
let userMenuHandles:
| {
dialog: DialogHandle<void>
@@ -123,8 +108,11 @@ function UserMenuPopoverTrigger({ className }: UserMenuTriggerProps) {
}
function UserMenuTriggerButton({ className, ...props }: ButtonProps) {
const openUserMenuLabel = useMessage(layoutMessages.openUserMenu)
return (
<HeaderActionButton
aria-label={openUserMenuLabel}
className={cn(
"overflow-hidden after:pointer-events-none after:absolute after:inset-0 after:z-999 after:rounded-md after:border after:border-foreground/35",
className
@@ -140,6 +128,31 @@ function UserMenuTriggerButton({ className, ...props }: ButtonProps) {
}
function UserMenuContent({ onItemSelect }: { onItemSelect: VoidFunction }) {
const appearanceLabel = useMessage(layoutMessages.appearance)
const keybindingsLabel = useMessage(layoutMessages.keybindings)
const accountPasswordLabel = useMessage(layoutMessages.accountPassword)
const logoutLabel = useMessage(layoutMessages.logout)
const userMenuItems: readonly UserMenuItem[] = [
{
id: "appearance",
action: navigationActions.appearance,
icon: SwatchIcon,
label: appearanceLabel,
shortcut: "Mod+,",
},
{
id: "keybindings",
icon: KeyboardIcon,
label: keybindingsLabel,
},
{
id: "account-password",
icon: ShieldKeyIcon,
label: accountPasswordLabel,
shortcut: "Mod+P",
},
]
return (
<div className="w-72">
<div className="flex flex-col items-center pt-10 pb-6">
@@ -153,7 +166,7 @@ function UserMenuContent({ onItemSelect }: { onItemSelect: VoidFunction }) {
<div className="border-t border-dashed border-border p-2">
<ul className="space-y">
{userMenuItems.map((item) => (
<li key={item.label}>
<li key={item.id}>
<Button
className="w-full justify-start font-normal"
size="lg"
@@ -176,7 +189,7 @@ function UserMenuContent({ onItemSelect }: { onItemSelect: VoidFunction }) {
<div className="rounded-b-md border-t p-2">
<Button className="w-full font-normal" variant="destructive" size="lg">
<Icon data={LogoutCircle02Icon} size="sm" />
退
{logoutLabel}
<MenuShortcutSequence shortcuts={["Q", "Q", "Q"]} />
</Button>
</div>
@@ -213,12 +226,14 @@ function UserMenuDialogTrigger({ className }: UserMenuTriggerProps) {
function UserMenuDialog() {
const { dialog } = getOrCreateUserMenuHandles()
const title = useMessage(layoutMessages.userMenuTitle)
return (
<Dialog handle={dialog}>
<DialogContent className="w-fit p-0">
<DialogHeader className="sr-only">
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{title}</DialogDescription>
</DialogHeader>
<UserMenuContent onItemSelect={() => dialog.close()} />
</DialogContent>
@@ -9,6 +9,7 @@ import { Icon } from "../../components/icon"
import { resolveNavigationItemIcon } from "./item-icon"
import { NavigationBadge, NavigationLink } from "./navigation-button"
import { NavigationLabel } from "./navigation-label"
import type { GetNavigationRouteState } from "./route-state"
import type { NavigationItem } from "./types"
@@ -37,7 +38,9 @@ export function SidebarFlyoutMenuItems({
className="h-9 gap-3 px-3 py-1 data-route-active:bg-primary/10 data-route-active:text-primary! data-route-active:**:text-primary!"
>
{iconElement}
<span className="min-w-0 flex-1 truncate">{item.label}</span>
<span className="min-w-0 flex-1 truncate">
<NavigationLabel label={item.label} />
</span>
<NavigationBadge className="ms-auto" itemId={item.id} />
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="grid w-fit max-w-(--available-width) min-w-44 gap-0.5">
@@ -59,7 +62,9 @@ export function SidebarFlyoutMenuItems({
render={item.to ? <NavigationLink to={item.to} /> : undefined}
>
{iconElement}
<span className="min-w-0 flex-1 truncate">{item.label}</span>
<span className="min-w-0 flex-1 truncate">
<NavigationLabel label={item.label} />
</span>
<NavigationBadge className="ms-auto" itemId={item.id} />
</DropdownMenuItem>
)
@@ -4,6 +4,7 @@ export {
} from "./mobile-navigation"
export { resolveNavigationItemIcon } from "./item-icon"
export { PrimaryNavigation } from "./primary-navigation"
export { NavigationLabel, TruncatedNavigationLabel } from "./navigation-label"
export { resolveNavigationRouteState } from "./route-state"
export {
NavigationProvider,
@@ -16,4 +17,8 @@ export type {
NavigationRouteState,
} from "./route-state"
export type { NavigationInfo, NavigationKey } from "./use-navigation"
export type { NavigationGroup, NavigationItem } from "./types"
export type {
NavigationGroup,
NavigationItem,
NavigationLabelValue,
} from "./types"
@@ -0,0 +1,34 @@
import { describe, it } from "vitest"
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
import { navigationMessages } from "../messages"
import { navigationCatalogLocales } from "./catalogs"
import { messages as de } from "./de"
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 zhHant } from "./zh-Hant"
describe("navigation locale catalogs", () => {
it("ship every navigation message in every built-in locale", () => {
expectCompleteCatalogs({
catalogs: {
de,
en,
es,
fr,
ja,
ko,
"zh-Hans": zhHans,
"zh-Hant": zhHant,
},
locales: navigationCatalogLocales,
messageIds: Object.values(navigationMessages).map(
(descriptor) => descriptor.id
),
})
})
})
@@ -0,0 +1,12 @@
import type { navigationMessages } from "../messages"
import {
blockCatalogLocales,
type BlockCatalogLocale,
type BlockMessageCatalog,
} from "../../../i18n/catalogs"
export { blockCatalogLocales as navigationCatalogLocales }
export type NavigationCatalogLocale = BlockCatalogLocale
export type NavigationMessageCatalog = BlockMessageCatalog<
typeof navigationMessages
>
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,9 @@
import type { NavigationMessageCatalog } from "./catalogs"
export const locale = "en"
export const languageTag = "en-US"
export const messages = {
"blocks.navigation.description": "Navigate through the app.",
"blocks.navigation.open": "Open navigation",
"blocks.navigation.title": "Navigation",
} as const satisfies NavigationMessageCatalog
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,9 @@
import type { NavigationMessageCatalog } from "./catalogs"
export const locale = "zh-Hans"
export const languageTag = "zh-CN"
export const messages = {
"blocks.navigation.description": "浏览应用中的页面。",
"blocks.navigation.open": "打开导航",
"blocks.navigation.title": "导航",
} as const satisfies NavigationMessageCatalog
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,16 @@
import type { MessageDescriptor } from "@workspace/i18n"
export const navigationMessages = {
description: /* i18n */ {
id: "blocks.navigation.description",
message: "Navigate through the app.",
},
open: /* i18n */ {
id: "blocks.navigation.open",
message: "Open navigation",
},
title: /* i18n */ {
id: "blocks.navigation.title",
message: "Navigation",
},
} as const satisfies Record<string, MessageDescriptor>
@@ -1,4 +1,5 @@
import { useLocation } from "@tanstack/react-router"
import { useMessage } from "@workspace/i18n"
import type { ButtonProps } from "@workspace/ui/components/button"
import { Separator } from "@workspace/ui/components/separator"
import {
@@ -15,6 +16,8 @@ import { cn, separate } from "@workspace/ui/lib/utils"
import { ChevronRightIcon } from "lucide-react"
import { HeaderActionButton } from "../layout/header-action-button"
import { navigationMessages } from "./messages"
import { NavigationLabel } from "./navigation-label"
import {
NavigationBadge,
@@ -37,11 +40,15 @@ export function MobileNavigationSheetTrigger({
className,
...props
}: ButtonProps) {
const openNavigationLabel = useMessage(navigationMessages.open)
return (
<SheetTrigger
handle={mobileNavigationSheetHandle}
className={cn("-ml-2 hidden mobile:inline-flex", className)}
render={<HeaderActionButton aria-label="打开导航" {...props} />}
render={
<HeaderActionButton aria-label={openNavigationLabel} {...props} />
}
>
<Icon
data={Menu02Icon}
@@ -57,6 +64,9 @@ export interface MobileNavigationSheetProps {
}
export function MobileNavigationSheet({ groups }: MobileNavigationSheetProps) {
const title = useMessage(navigationMessages.title)
const description = useMessage(navigationMessages.description)
useOnBreakpoint((breakpoint) => {
if (breakpoint !== "mobile" && mobileNavigationSheetHandle.isOpen) {
mobileNavigationSheetHandle.close()
@@ -75,8 +85,8 @@ export function MobileNavigationSheet({ groups }: MobileNavigationSheetProps) {
<Sheet handle={mobileNavigationSheetHandle}>
<SheetContent className="max-w-72 gap-0 bg-popover/90" side="left">
<SheetHeader className="text-left">
<SheetTitle>Navigation</SheetTitle>
<SheetDescription>Navigate through the app.</SheetDescription>
<SheetTitle>{title}</SheetTitle>
<SheetDescription>{description}</SheetDescription>
</SheetHeader>
<div
className={cn(
@@ -124,7 +134,7 @@ function FlatNavigationGroupList({
{items.map((group) => (
<div key={group.id}>
<div className="mx-3 mb-2 truncate text-xs whitespace-nowrap tablet:hidden not-mobile:collapsed:hidden">
{group.label}
<NavigationLabel label={group.label} />
</div>
<div className="rounded-md ring ring-sidebar-border">
{separate({
@@ -8,6 +8,8 @@ import { cn } from "@workspace/ui/lib/utils"
import { ChevronRightIcon } from "lucide-react"
import { Icon, type IconData } from "../../components/icon"
import { NavigationLabel, TruncatedNavigationLabel } from "./navigation-label"
import type { NavigationLabelValue } from "./types"
export interface NavigationButtonProps extends Omit<
useRender.ComponentProps<"button", {}>,
@@ -16,7 +18,7 @@ export interface NavigationButtonProps extends Omit<
icon?: IconData
isActive: boolean
isCurrent: boolean
label: string
label: NavigationLabelValue
layout?: "inline" | "stacked" | "responsive"
className?: string | (() => string | undefined) | undefined
style?:
@@ -105,21 +107,22 @@ export function NavigationButton({
)}
<span
className={cn(
"flex-1 origin-top truncate whitespace-nowrap",
"min-w-0 flex-1 origin-top overflow-hidden whitespace-nowrap",
layout === "stacked"
? [
"col-start-1 col-end-5 row-start-2 justify-self-center",
"scale-85 text-center text-xs",
"col-start-1 col-end-5 row-start-2 w-full max-w-full",
"scale-85 justify-center text-xs",
]
: layout === "responsive"
? [
"tablet:col-start-1 tablet:col-end-5 tablet:row-start-2",
"tablet:justify-self-center tablet:text-center tablet:text-xs",
"tablet:w-full tablet:max-w-full tablet:justify-center tablet:text-xs",
"not-mobile:collapsed:col-start-1",
"not-mobile:collapsed:col-end-5",
"not-mobile:collapsed:row-start-2",
"not-mobile:collapsed:justify-self-center",
"not-mobile:collapsed:text-center",
"not-mobile:collapsed:w-full",
"not-mobile:collapsed:max-w-full",
"not-mobile:collapsed:justify-center",
"not-mobile:collapsed:text-xs",
"tablet:scale-85",
"not-mobile:collapsed:scale-85",
@@ -127,7 +130,11 @@ export function NavigationButton({
: undefined
)}
>
{label}
{layout === "inline" ? (
<NavigationLabel label={label} />
) : (
<TruncatedNavigationLabel label={label} />
)}
</span>
{hasTrailingContent && (
<span
@@ -158,7 +165,7 @@ export function NavigationButton({
)}
</React.Fragment>
),
"aria-label": label,
"aria-label": typeof label === "string" ? label : undefined,
}
return useRender({
@@ -0,0 +1,44 @@
import { cn } from "@workspace/ui/lib/utils"
import { LocalizedText } from "../../components/localized-text"
import type { NavigationLabelValue } from "./types"
export interface NavigationLabelProps {
label: NavigationLabelValue
}
/**
* Resolves a navigation label without subscribing the surrounding navigation
* item to locale changes.
*/
export function NavigationLabel({ label }: NavigationLabelProps) {
if (typeof label === "string") {
return label
}
return <LocalizedText message={label} />
}
export interface TruncatedNavigationLabelProps extends NavigationLabelProps {
className?: string
}
/**
* Centers labels that fit while keeping overflowing labels anchored to the
* inline start edge and truncating their inline end.
*/
export function TruncatedNavigationLabel({
className,
label,
}: TruncatedNavigationLabelProps) {
return (
<span
className={cn(
"inline-block max-w-full truncate text-start align-bottom",
className
)}
>
<NavigationLabel label={label} />
</span>
)
}
@@ -17,6 +17,7 @@ import {
type NavigationButtonProps,
} from "./navigation-button"
import { InlineNestedNavigationList } from "./nested-navigation"
import { NavigationLabel } from "./navigation-label"
import type { GetNavigationRouteState } from "./route-state"
import type { NavigationGroup, NavigationItem } from "./types"
@@ -172,7 +173,7 @@ export function PrimaryNavigation({
{items.map((group) => (
<div key={group.id}>
<div className="mx-3 mb-2 truncate text-xs whitespace-nowrap tablet:hidden not-mobile:collapsed:hidden">
{group.label}
<NavigationLabel label={group.label} />
</div>
<div className="tablet:space-y-1 not-mobile:collapsed:space-y-1">
{group.items.map((item) => (
@@ -1,9 +1,13 @@
import type { MessageDescriptor } from "@workspace/i18n"
import type { IconData } from "../../components/icon"
export type NavigationLabelValue = string | MessageDescriptor
export interface NavigationItem {
id: string
to?: string
label: string
label: NavigationLabelValue
icon?: IconData
showIcon?: boolean
items?: readonly NavigationItem[]
@@ -11,7 +15,7 @@ export interface NavigationItem {
export interface NavigationGroup {
id: string
label: string
label: NavigationLabelValue
showIcon?: boolean
items: readonly NavigationItem[]
}
@@ -3,7 +3,11 @@ import * as React from "react"
import type { IconData } from "../../components/icon"
import { resolveNavigationItemIcon } from "./item-icon"
import type { NavigationGroup, NavigationItem } from "./types"
import type {
NavigationGroup,
NavigationItem,
NavigationLabelValue,
} from "./types"
export type NavigationKey = NavigationItem["id"]
@@ -11,7 +15,7 @@ export interface NavigationInfo {
id: NavigationKey
icon?: IconData
items?: readonly NavigationInfo[]
label: string
label: NavigationLabelValue
to?: string
}
@@ -0,0 +1,34 @@
import { describe, it } from "vitest"
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
import { notificationMessages } from "../messages"
import { notificationCatalogLocales } from "./catalogs"
import { messages as de } from "./de"
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 zhHant } from "./zh-Hant"
describe("notification locale catalogs", () => {
it("ship every notification message in every built-in locale", () => {
expectCompleteCatalogs({
catalogs: {
de,
en,
es,
fr,
ja,
ko,
"zh-Hans": zhHans,
"zh-Hant": zhHant,
},
locales: notificationCatalogLocales,
messageIds: Object.values(notificationMessages).map(
(descriptor) => descriptor.id
),
})
})
})
@@ -0,0 +1,12 @@
import type { notificationMessages } from "../messages"
import {
blockCatalogLocales,
type BlockCatalogLocale,
type BlockMessageCatalog,
} from "../../../i18n/catalogs"
export { blockCatalogLocales as notificationCatalogLocales }
export type NotificationCatalogLocale = BlockCatalogLocale
export type NotificationMessageCatalog = BlockMessageCatalog<
typeof notificationMessages
>
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,18 @@
import type { NotificationMessageCatalog } from "./catalogs"
export const locale = "en"
export const languageTag = "en-US"
export const messages = {
"blocks.notifications.description": "View and manage notifications",
"blocks.notifications.empty.description": "New activity will appear here.",
"blocks.notifications.empty.title": "No notifications",
"blocks.notifications.error.description":
"Check your network connection and try again.",
"blocks.notifications.error.title": "Unable to load notifications",
"blocks.notifications.justNow": "just now",
"blocks.notifications.loading": "Loading notifications…",
"blocks.notifications.markAllAsRead": "Mark all as read",
"blocks.notifications.retry": "Try again",
"blocks.notifications.title": "Notifications",
"blocks.notifications.viewAll": "View all",
} as const satisfies NotificationMessageCatalog
@@ -0,0 +1,20 @@
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
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,17 @@
import type { NotificationMessageCatalog } from "./catalogs"
export const locale = "zh-Hans"
export const languageTag = "zh-CN"
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
@@ -0,0 +1,17 @@
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
@@ -0,0 +1,48 @@
import type { MessageDescriptor } from "@workspace/i18n"
export const notificationMessages = {
description: /* i18n */ {
id: "blocks.notifications.description",
message: "View and manage notifications",
},
emptyDescription: /* i18n */ {
id: "blocks.notifications.empty.description",
message: "New activity will appear here.",
},
emptyTitle: /* i18n */ {
id: "blocks.notifications.empty.title",
message: "No notifications",
},
errorDescription: /* i18n */ {
id: "blocks.notifications.error.description",
message: "Check your network connection and try again.",
},
errorTitle: /* i18n */ {
id: "blocks.notifications.error.title",
message: "Unable to load notifications",
},
justNow: /* i18n */ {
id: "blocks.notifications.justNow",
message: "just now",
},
loading: /* i18n */ {
id: "blocks.notifications.loading",
message: "Loading notifications…",
},
markAllAsRead: /* i18n */ {
id: "blocks.notifications.markAllAsRead",
message: "Mark all as read",
},
retry: /* i18n */ {
id: "blocks.notifications.retry",
message: "Try again",
},
title: /* i18n */ {
id: "blocks.notifications.title",
message: "Notifications",
},
viewAll: /* i18n */ {
id: "blocks.notifications.viewAll",
message: "View all",
},
} as const satisfies Record<string, MessageDescriptor>
@@ -1,5 +1,6 @@
import * as React from "react"
import { CheckCheckIcon } from "lucide-react"
import { useMessage } from "@workspace/i18n"
import { Button } from "@workspace/ui/components/button"
import {
Empty,
@@ -15,6 +16,7 @@ import {
} from "@workspace/ui/components/tooltip"
import { cn } from "@workspace/ui/lib/utils"
import { notificationMessages } from "./messages"
import { NotificationItem } from "./notification-item"
import type { Notification, NotificationActionHandler } from "./types"
@@ -37,17 +39,30 @@ export interface NotificationCenterProps extends Omit<
export function NotificationCenter({
className,
description = "查看并处理通知",
description,
notifications,
onAction,
onMarkAllAsRead,
onRefetch,
onViewAll,
status,
title = "Notifications",
viewAllLabel = "查看全部",
title,
viewAllLabel,
...props
}: NotificationCenterProps) {
const defaultDescription = useMessage(notificationMessages.description)
const defaultTitle = useMessage(notificationMessages.title)
const defaultViewAllLabel = useMessage(notificationMessages.viewAll)
const markAllAsReadLabel = useMessage(notificationMessages.markAllAsRead)
const loadingLabel = useMessage(notificationMessages.loading)
const errorTitle = useMessage(notificationMessages.errorTitle)
const errorDescription = useMessage(notificationMessages.errorDescription)
const retryLabel = useMessage(notificationMessages.retry)
const emptyTitle = useMessage(notificationMessages.emptyTitle)
const emptyDescription = useMessage(notificationMessages.emptyDescription)
const resolvedDescription = description ?? defaultDescription
const resolvedTitle = title ?? defaultTitle
const resolvedViewAllLabel = viewAllLabel ?? defaultViewAllLabel
const unreadCount = notifications.filter(
(notification) => !notification.isRead
).length
@@ -59,14 +74,16 @@ export function NotificationCenter({
>
<header className="grid grid-cols-[1fr_auto] p-4">
<h2 className="order-1 font-heading font-medium text-foreground">
{title}
{resolvedTitle}
{unreadCount > 0 && (
<span className="ms-1.5 text-sm font-normal text-muted-foreground">
({unreadCount})
</span>
)}
</h2>
{description && <p className="sr-only">{description}</p>}
{resolvedDescription && (
<p className="sr-only">{resolvedDescription}</p>
)}
<Tooltip>
<TooltipTrigger
delay={0}
@@ -74,7 +91,7 @@ export function NotificationCenter({
disabled={unreadCount === 0}
render={
<Button
aria-label="全部标记为已读"
aria-label={markAllAsReadLabel}
variant="ghost"
size="icon-sm"
/>
@@ -84,7 +101,7 @@ export function NotificationCenter({
<CheckCheckIcon />
</TooltipTrigger>
<TooltipContent align="end" showArrow>
{markAllAsReadLabel}
</TooltipContent>
</Tooltip>
</header>
@@ -94,23 +111,23 @@ export function NotificationCenter({
role="status"
className="flex min-h-0 flex-1 items-center justify-center text-sm text-muted-foreground"
>
{loadingLabel}
</div>
) : status === "error" ? (
<Empty>
<EmptyContent>
<EmptyTitle></EmptyTitle>
<EmptyDescription></EmptyDescription>
<EmptyTitle>{errorTitle}</EmptyTitle>
<EmptyDescription>{errorDescription}</EmptyDescription>
<Button variant="outline" onClick={onRefetch}>
{retryLabel}
</Button>
</EmptyContent>
</Empty>
) : notifications.length === 0 ? (
<Empty>
<EmptyContent>
<EmptyTitle></EmptyTitle>
<EmptyDescription></EmptyDescription>
<EmptyTitle>{emptyTitle}</EmptyTitle>
<EmptyDescription>{emptyDescription}</EmptyDescription>
</EmptyContent>
</Empty>
) : (
@@ -137,7 +154,7 @@ export function NotificationCenter({
<footer className="mt-auto flex flex-col gap-2 p-4">
<Button size="lg" variant="ghost" className="h-12" onClick={onViewAll}>
{viewAllLabel}
{resolvedViewAllLabel}
</Button>
</footer>
</section>
@@ -1,5 +1,6 @@
import * as React from "react"
import { Link } from "@tanstack/react-router"
import { useFormatters, useMessage } from "@workspace/i18n"
import {
Avatar,
AvatarFallback,
@@ -17,6 +18,7 @@ import {
import { cn } from "@workspace/ui/lib/utils"
import { Icon } from "../../components/icon"
import { notificationMessages } from "./messages"
import type {
Notification,
@@ -43,10 +45,6 @@ const ICON_TONE_CLASSES: Record<NotificationTone, string> = {
warning: "bg-amber-500/10 text-amber-600 dark:text-amber-400",
}
const RELATIVE_TIME_FORMAT = new Intl.RelativeTimeFormat("zh-CN", {
numeric: "auto",
})
const RELATIVE_TIME_UNITS: ReadonlyArray<
readonly [seconds: number, unit: Intl.RelativeTimeFormatUnit]
> = [
@@ -71,7 +69,11 @@ function getInitials(media: Extract<NotificationMedia, { kind: "avatar" }>) {
.toUpperCase()
}
function formatRelativeTime(createdAt: string) {
function formatNotificationTime(
createdAt: string,
formatter: (value: number, unit: Intl.RelativeTimeFormatUnit) => string,
justNow: string
) {
const createdAtTime = new Date(createdAt).getTime()
if (!Number.isFinite(createdAtTime)) {
@@ -83,14 +85,11 @@ function formatRelativeTime(createdAt: string) {
for (const [seconds, unit] of RELATIVE_TIME_UNITS) {
if (absoluteDifference >= seconds) {
return RELATIVE_TIME_FORMAT.format(
Math.round(differenceInSeconds / seconds),
unit
)
return formatter(Math.round(differenceInSeconds / seconds), unit)
}
}
return "刚刚"
return justNow
}
function NotificationItemMedia({ media }: { media: NotificationMedia }) {
@@ -148,6 +147,9 @@ export function NotificationItem({
ref,
...props
}: NotificationItemProps) {
const { formatRelativeTime } = useFormatters()
const justNow = useMessage(notificationMessages.justNow)
return (
<BaseItem
ref={ref}
@@ -181,7 +183,12 @@ export function NotificationItem({
)}
<ItemDescription className="flex items-center gap-1">
<time dateTime={notification.createdAt}>
{formatRelativeTime(notification.createdAt)}
{formatNotificationTime(
notification.createdAt,
(value, unit) =>
formatRelativeTime(value, unit, { numeric: "auto" }),
justNow
)}
</time>
{notification.category && (
<>
@@ -1,4 +1,5 @@
import * as React from "react"
import { useMessage } from "@workspace/i18n"
import {
createSheetHandle,
Sheet,
@@ -8,6 +9,7 @@ import {
SheetTrigger,
} from "@workspace/ui/components/sheet"
import { notificationMessages } from "./messages"
import { NotificationCenter } from "./notification-center"
import type { Notification, NotificationActionHandler } from "./types"
import {
@@ -41,9 +43,12 @@ export function NotificationCenterSheet({
onViewAll,
queryFn,
queryKey,
title = "Notifications",
title,
}: NotificationCenterSheetProps) {
const notifications = useNotifications({ queryFn, queryKey })
const defaultDescription = useMessage(notificationMessages.description)
const defaultTitle = useMessage(notificationMessages.title)
const resolvedTitle = title ?? defaultTitle
const handleAction = React.useCallback<NotificationActionHandler>(
(event) => {
@@ -65,8 +70,10 @@ export function NotificationCenterSheet({
className="w-105 gap-0 bg-popover/90"
showCloseButton={false}
>
<SheetTitle className="sr-only">{title}</SheetTitle>
<SheetDescription className="sr-only"></SheetDescription>
<SheetTitle className="sr-only">{resolvedTitle}</SheetTitle>
<SheetDescription className="sr-only">
{defaultDescription}
</SheetDescription>
<NotificationCenter
notifications={notifications.notifications}
onAction={handleAction}
@@ -74,7 +81,7 @@ export function NotificationCenterSheet({
onRefetch={notifications.refetch}
onViewAll={onViewAll}
status={notifications.status}
title={title}
title={resolvedTitle}
/>
</SheetContent>
</Sheet>

Some files were not shown because too many files have changed in this diff Show More