diff --git a/.storybook/locales/langs/en-US.ts b/.storybook/locales/langs/en-US.ts index 8d365d4b..773bd4be 100644 --- a/.storybook/locales/langs/en-US.ts +++ b/.storybook/locales/langs/en-US.ts @@ -231,6 +231,22 @@ const enUS = { 'storybook.stories.PluginReactI18n.mode.matchCaseSensitive': 'isMatchCaseSensitive', 'storybook.stories.PluginReactI18n.defaultSearch.zhCN': '审', 'storybook.stories.PluginReactI18n.defaultSearch.enUS': 're', + 'storybook.stories.CoreWhyEnumPlus.metaDescription': + 'Shows the business motivation before the API details: replace duplicated constants, label maps, Select options, table filters, and badge maps with one enum source.', + 'storybook.stories.CoreUiOutputs.metaDescription': + 'Focuses on the practical frontend payoff: one enum can feed select options, status cards, table badges, and static maps without rebuilding UI-specific data repeatedly.', + 'storybook.stories.PluginI18next.metaDescription': + 'Demonstrates the plain i18next integration: enum labels resolve through i18next, while UI refresh still depends on the host framework rerendering.', + 'storybook.stories.PluginReactI18next.metaDescription': + 'Shows the lightweight react-i18next integration package: it reads translations from the shared React i18n instance, but automatic UI refresh still belongs to plugin-react.', + 'storybook.stories.PluginNextInternational.metaDescription': + 'Shows the client-side next-international integration: PatchedI18nProviderClient wires runtime translation into enum-plus so labels and search can react to locale changes.', + 'storybook.stories.PluginVueI18n.metaDescription': + 'Demonstrates the vue-i18n integration from a React shell by using the plugin fallback path with a provided vue-i18n instance.', + 'storybook.stories.PluginI18nextVue.metaDescription': + 'Shows the i18next-vue integration through its non-component fallback path so the plugin contract can still be demonstrated from Storybook React.', + 'storybook.stories.PluginSample.metaDescription': + 'Explains the plugin authoring model itself by showing how the sample plugin adds one extension method to every enum instance through Enum.extends.', } as const; export default enUS; diff --git a/.storybook/locales/langs/zh-CN.ts b/.storybook/locales/langs/zh-CN.ts index 11d05c2e..36ab5865 100644 --- a/.storybook/locales/langs/zh-CN.ts +++ b/.storybook/locales/langs/zh-CN.ts @@ -229,6 +229,22 @@ const zhCN = { 'storybook.stories.PluginReactI18n.mode.matchCaseSensitive': 'isMatchCaseSensitive', 'storybook.stories.PluginReactI18n.defaultSearch.zhCN': '审', 'storybook.stories.PluginReactI18n.defaultSearch.enUS': 're', + 'storybook.stories.CoreWhyEnumPlus.metaDescription': + '先把业务动机讲清楚:用一份 enum 来源替代重复的常量、labelMap、Select options、表格筛选和 badge 映射。', + 'storybook.stories.CoreUiOutputs.metaDescription': + '聚焦前端最实际的收益:同一份 enum 可以同时驱动下拉 options、状态卡片、表格徽标和静态 map,不必反复重建 UI 专用结构。', + 'storybook.stories.PluginI18next.metaDescription': + '展示最基础的 i18next 接入:枚举 label 能通过 i18next 翻译,但界面刷新仍取决于宿主框架是否重渲染。', + 'storybook.stories.PluginReactI18next.metaDescription': + '展示轻量版 react-i18next 接入:它会读取共享的 React i18n 实例,但真正的自动界面刷新仍应交给 plugin-react。', + 'storybook.stories.PluginNextInternational.metaDescription': + '展示 next-international 的客户端接入:PatchedI18nProviderClient 会把运行时翻译能力注入 enum-plus,让 label 与搜索跟随 locale 自动变化。', + 'storybook.stories.PluginVueI18n.metaDescription': + '展示 vue-i18n 的回退接入路径:即使在 React Storybook 壳中,也能通过传入 vue-i18n instance 验证插件契约。', + 'storybook.stories.PluginI18nextVue.metaDescription': + '展示 i18next-vue 的非组件回退路径:即使没有真实 Vue 页面,也能在 React Storybook 中验证插件输出契约。', + 'storybook.stories.PluginSample.metaDescription': + '用 sample plugin 解释插件 authoring 机制:它通过 Enum.extends 给每个 enum 实例挂上一个新增方法。', } as const; export default zhCN; diff --git a/.storybook/stories/CoreApi.stories.tsx b/.storybook/stories/CoreApi.stories.tsx index 3a7e8637..7a8a612a 100644 --- a/.storybook/stories/CoreApi.stories.tsx +++ b/.storybook/stories/CoreApi.stories.tsx @@ -52,7 +52,7 @@ function createConflictEnum(t: ReturnType) { } const meta: Meta = { - title: 'Core/Query and Transform API', + title: 'Core/03 Query, Meta and Transform', parameters: { docs: { description: { diff --git a/.storybook/stories/CoreInitialization.stories.tsx b/.storybook/stories/CoreInitialization.stories.tsx index 725c299c..66b9dd52 100644 --- a/.storybook/stories/CoreInitialization.stories.tsx +++ b/.storybook/stories/CoreInitialization.stories.tsx @@ -120,7 +120,7 @@ const ChannelEnum = Enum(ReleaseChannelNative);`, } const meta: Meta = { - title: 'Core/Enum Initialization', + title: 'Core/02 Initialization Patterns', parameters: { docs: { description: { diff --git a/.storybook/stories/CorePatterns.stories.tsx b/.storybook/stories/CorePatterns.stories.tsx index f58aab08..9b8651b9 100644 --- a/.storybook/stories/CorePatterns.stories.tsx +++ b/.storybook/stories/CorePatterns.stories.tsx @@ -35,7 +35,7 @@ function ensureCustomExtension() { } const meta: Meta = { - title: 'Core/Localization, Composition and Extension', + title: 'Core/05 Localization, Composition and Extension', parameters: { docs: { description: { diff --git a/.storybook/stories/CoreUiOutputs.stories.tsx b/.storybook/stories/CoreUiOutputs.stories.tsx new file mode 100644 index 00000000..75ae333c --- /dev/null +++ b/.storybook/stories/CoreUiOutputs.stories.tsx @@ -0,0 +1,246 @@ +import { useMemo, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Card, Descriptions, Select, Space, Table, Tag, Typography } from 'antd'; +import { Enum } from '../../src'; +import { storyT, useStoryLocale } from '../locales'; +import { CodePreview, JsonPreview, StoryPage, StorySection, TwoColumn } from './shared/demo'; + +const { Text } = Typography; + +const meta: Meta = { + title: 'Core/04 UI Outputs and Derived Data', + parameters: { + docs: { + description: { + component: storyT('storybook.stories.CoreUiOutputs.metaDescription'), + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +function useCopy() { + const locale = useStoryLocale(); + + return locale === 'zh-CN' + ? { + pageTitle: '把同一份 enum 变成多个 UI 输出', + pageDescription: + '如果核心问题是“重复 UI enum plumbing”,那最应该单独展示的就是派生输出层:Select 需要 options,卡片需要 badge,表格需要 label + meta,静态配置又需要 map。这个页面专门演示一份 enum 如何同时喂给这些界面。', + highlights: ['toList()', 'items', 'toMap()', 'meta/raw'], + uiTitle: '一个运营面板,多处 UI,共用同一份 enum', + uiDescription: '筛选器、状态徽标、表格列和统计卡片全部来自同一份 status enum。', + selectLabel: '筛选状态', + allStatuses: '全部状态', + currentValue: '当前值', + currentLabel: '当前 label', + currentTone: '当前 tone', + currentPhase: '当前 phase', + cardTitle: '状态概览卡片', + cardDescription: 'items 天然适合驱动自定义卡片或 badge 列表。', + tableTitle: '表格渲染', + tableDescription: 'label 和 raw/meta 可以直接进入表格列渲染逻辑。', + derivedTitle: '派生结构校验', + derivedDescription: '对 UI 层来说,最常见的出口就是 toList / items / toMap。', + toListCard: 'toList()', + itemsCard: 'items', + toMapCard: 'toMap({ keySelector, valueSelector })', + codeTitle: '同一份 enum 派生多个 UI 结构', + article: '文章', + visits: '访问量', + status: '状态', + draft: '草稿', + review: '审核中', + published: '已发布', + archived: '已归档', + phaseEditing: '编辑中', + phaseOnline: '线上', + phaseArchive: '归档', + statusName: '发布状态', + } + : { + pageTitle: 'Turn one enum into multiple UI outputs', + pageDescription: + 'If the core frontend pain is duplicated enum plumbing, the derived-output layer deserves its own story. Select needs options, cards need badges, tables need labels plus meta, and config panels need maps. This page shows how one enum can feed all of them at once.', + highlights: ['toList()', 'items', 'toMap()', 'meta/raw'], + uiTitle: 'One operations panel, many UI surfaces, one enum source', + uiDescription: + 'The filter, status badges, table columns, and summary cards all come from the same status enum.', + selectLabel: 'Filter by status', + allStatuses: 'All statuses', + currentValue: 'Current value', + currentLabel: 'Current label', + currentTone: 'Current tone', + currentPhase: 'Current phase', + cardTitle: 'Status overview cards', + cardDescription: 'items are a natural data source for custom cards or badge groups.', + tableTitle: 'Table rendering', + tableDescription: 'label and raw/meta fields can go straight into table render logic.', + derivedTitle: 'Derived structure check', + derivedDescription: 'For UI work, the most common exits are toList, items, and toMap.', + toListCard: 'toList()', + itemsCard: 'items', + toMapCard: 'toMap({ keySelector, valueSelector })', + codeTitle: 'Derive multiple UI structures from one enum', + article: 'Article', + visits: 'Visits', + status: 'Status', + draft: 'Draft', + review: 'In Review', + published: 'Published', + archived: 'Archived', + phaseEditing: 'Editing', + phaseOnline: 'Online', + phaseArchive: 'Archive', + statusName: 'Publish Status', + }; +} + +function UiOutputsDemo() { + const copy = useCopy(); + const [selectedStatus, setSelectedStatus] = useState('all'); + + const statusEnum = useMemo( + () => + Enum( + { + Draft: { value: 'draft', label: copy.draft, phase: copy.phaseEditing, tone: 'default', count: 3 }, + Review: { value: 'review', label: copy.review, phase: copy.phaseEditing, tone: 'processing', count: 5 }, + Published: { value: 'published', label: copy.published, phase: copy.phaseOnline, tone: 'success', count: 8 }, + Archived: { value: 'archived', label: copy.archived, phase: copy.phaseArchive, tone: 'default', count: 2 }, + }, + { name: copy.statusName }, + ), + [copy], + ); + + const dataSource = useMemo( + () => [ + { id: 1, title: 'Enum-plus release notes', visits: 1280, status: 'published' }, + { id: 2, title: 'Migration guide draft', visits: 320, status: 'draft' }, + { id: 3, title: 'Frontend enum audit', visits: 760, status: 'review' }, + { id: 4, title: 'Archived benchmark notes', visits: 210, status: 'archived' }, + ], + [], + ); + + const filteredRows = + selectedStatus === 'all' ? dataSource : dataSource.filter((item) => item.status === selectedStatus); + const currentRaw = selectedStatus === 'all' ? undefined : statusEnum.raw(selectedStatus); + + return ( + + + + + setSelectedValue(value)} + /> + + + + + + + + + + { + const raw = statusEnum.raw(value); + return {statusEnum.label(value)}; + }, + }, + { + title: copy.phase, + dataIndex: 'status', + render: (value: string) => {statusEnum.raw(value)?.phase}, + }, + ]} + dataSource={articles} + /> + + + ); +} + +export const Playground: Story = { + render: function Render() { + return ; + }, +}; diff --git a/.storybook/stories/PluginAntd.stories.tsx b/.storybook/stories/PluginAntd.stories.tsx index 6aa08990..2aed504d 100644 --- a/.storybook/stories/PluginAntd.stories.tsx +++ b/.storybook/stories/PluginAntd.stories.tsx @@ -19,7 +19,7 @@ function ensureAntdPlugin() { } const meta: Meta = { - title: 'Plugins/Ant Design Integration', + title: 'Plugins/01 Ant Design', parameters: { docs: { description: { diff --git a/.storybook/stories/PluginI18next.stories.tsx b/.storybook/stories/PluginI18next.stories.tsx new file mode 100644 index 00000000..9e923448 --- /dev/null +++ b/.storybook/stories/PluginI18next.stories.tsx @@ -0,0 +1,199 @@ +import { useEffect, useMemo, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { createInstance, type i18n } from 'i18next'; +import { Button, Card, Descriptions, Space, Tag, Typography } from 'antd'; +import i18nextPlugin from '../../packages/plugin-i18next/src'; +import { Enum } from '../../src'; +import { storyT, useStoryLocale } from '../locales'; +import { CodePreview, JsonPreview, StoryPage, StorySection, TwoColumn } from './shared/demo'; + +const { Paragraph, Text } = Typography; + +const meta: Meta = { + title: 'Plugins/02 i18next', + parameters: { + docs: { + description: { + component: storyT('storybook.stories.PluginI18next.metaDescription'), + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +function createStoryI18next(locale: 'en-US' | 'zh-CN') { + const instance = createInstance(); + void instance.init({ + lng: locale, + fallbackLng: 'en-US', + initImmediate: false, + resources: { + 'en-US': { + translation: { + 'status.enumName': 'Publishing Status', + 'status.draft': 'Draft', + 'status.review': 'In Review', + 'status.published': 'Published', + }, + }, + 'zh-CN': { + translation: { + 'status.enumName': '发布状态', + 'status.draft': '草稿', + 'status.review': '审核中', + 'status.published': '已发布', + }, + }, + }, + }); + return instance; +} + +function useCopy() { + const locale = useStoryLocale(); + + return locale === 'zh-CN' + ? { + pageTitle: 'i18next:能本地化 enum,但不会替你驱动 UI 刷新', + pageDescription: + '@enum-plus/plugin-i18next 适合“先把枚举 label 接进 i18next”的场景。它解决的是翻译查找,不是 React/Vue 的响应式刷新。因此如果宿主界面没有重新渲染,页面上的 label 不会自动变化。', + highlights: ['i18next', '字符串本地化', '宿主控制刷新', '低耦合'], + runtimeTitle: '切换语言后,需要宿主界面自己触发重渲染', + runtimeDescription: + '下面这个 demo 故意只修改 i18next 的语言,不主动 setState。这样可以直观看到:翻译源已变,但 UI 要等下一次渲染才会更新。', + currentI18n: 'instance.language', + enumName: 'enum.name', + enumLabel: 'enum.label(value)', + rerenderTick: 'render tick', + switchZh: '只切到中文(不触发重渲染)', + switchEn: '只切到英文(不触发重渲染)', + rerender: '手动触发一次重渲染', + note: '这不是插件失效,而是它本来就只负责把 localize 接到 i18next。真正的 UI 自动刷新,应交给 React/Vue 专用插件。', + structureTitle: '派生结果仍然可用', + structureDescription: '即使不用 UI 专用插件,enum 依然可以输出 toList / toMap / raw 等稳定结构。', + listTitle: 'toList() 输出', + mapTitle: 'toMap() 输出', + codeTitle: '推荐安装方式', + code: `import i18nextPlugin from '@enum-plus/plugin-i18next';\nimport { Enum } from 'enum-plus';\n\nEnum.install(i18nextPlugin, {\n localize: {\n instance: i18next,\n tOptions: { ns: 'translation' },\n },\n});`, + } + : { + pageTitle: 'i18next: localize enum labels without owning UI refresh', + pageDescription: + '@enum-plus/plugin-i18next is useful when you want plain enum label localization through i18next. It solves translation lookup, not React/Vue reactivity. If the host UI does not rerender, the screen will not refresh automatically.', + highlights: ['i18next', 'String localization', 'Host-controlled refresh', 'Low coupling'], + runtimeTitle: 'After language changes, the host UI must rerender', + runtimeDescription: + 'This demo intentionally changes the i18next language without calling setState. The translation source changes immediately, but the visible UI waits for the next rerender.', + currentI18n: 'instance.language', + enumName: 'enum.name', + enumLabel: 'enum.label(value)', + rerenderTick: 'render tick', + switchZh: 'Switch to Chinese only (no rerender)', + switchEn: 'Switch to English only (no rerender)', + rerender: 'Force one rerender', + note: 'This is not a bug. The plugin only connects localize to i18next. Automatic UI refresh should be handled by a React/Vue specific plugin.', + structureTitle: 'Derived outputs still work well', + structureDescription: + 'Even without a UI-specific plugin, the enum can still expose stable outputs like toList, toMap, and raw.', + listTitle: 'toList() output', + mapTitle: 'toMap() output', + codeTitle: 'Recommended installation', + code: `import i18nextPlugin from '@enum-plus/plugin-i18next';\nimport { Enum } from 'enum-plus';\n\nEnum.install(i18nextPlugin, {\n localize: {\n instance: i18next,\n tOptions: { ns: 'translation' },\n },\n});`, + }; +} + +function I18nextStory() { + const storyLocale = useStoryLocale(); + const copy = useCopy(); + const [renderTick, setRenderTick] = useState(0); + const instance = useMemo(() => createStoryI18next(storyLocale), [storyLocale]); + + useEffect(() => { + const previousLocalize = Enum.localize; + i18nextPlugin({ localize: { instance } }, Enum as never); + return () => { + Enum.localize = previousLocalize; + }; + }, [instance]); + + const publishingStatus = useMemo( + () => + Enum( + { + Draft: { value: 'draft', label: 'status.draft', tone: 'default' }, + Review: { value: 'review', label: 'status.review', tone: 'processing' }, + Published: { value: 'published', label: 'status.published', tone: 'success' }, + }, + { name: 'status.enumName' }, + ), + [renderTick], + ); + + return ( + + + + + + + + + + + + + + {copy.note} + + + + } + right={} + /> + + + + + + {publishingStatus.items.map((item) => { + const raw = item.raw as { tone?: string }; + return ( + + {item.label} + + ); + })} + + } + right={} + /> + + {`renderTick = ${renderTick}`} + + + + + ); +} + +export const Playground: Story = { + render: function Render() { + return ; + }, +}; diff --git a/.storybook/stories/PluginI18nextVue.stories.tsx b/.storybook/stories/PluginI18nextVue.stories.tsx new file mode 100644 index 00000000..9f518e7a --- /dev/null +++ b/.storybook/stories/PluginI18nextVue.stories.tsx @@ -0,0 +1,228 @@ +import { useEffect, useMemo, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import i18next from 'i18next'; +import { Button, Card, Descriptions, Space, Tag, Typography } from 'antd'; +import i18nextVuePlugin from '../../packages/plugin-i18next-vue/src'; +import { Enum } from '../../src'; +import { storyT, useStoryLocale } from '../locales'; +import { CodePreview, JsonPreview, StoryPage, StorySection, TwoColumn } from './shared/demo'; + +const { Paragraph } = Typography; +const STORY_NAMESPACE = 'storybook-plugin-i18next-vue'; +const STORY_RESOURCES = { + 'en-US': { + delivery: { + enumName: 'Delivery Status', + pending: 'Pending', + review: 'In Review', + shipped: 'Shipped', + }, + }, + 'zh-CN': { + delivery: { + enumName: '交付状态', + pending: '待处理', + review: '审核中', + shipped: '已发出', + }, + }, +} as const; + +const meta: Meta = { + title: 'Plugins/07 i18next Vue', + parameters: { + docs: { + description: { + component: storyT('storybook.stories.PluginI18nextVue.metaDescription'), + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +function useCopy() { + const locale = useStoryLocale(); + + return locale === 'zh-CN' + ? { + pageTitle: 'i18next-vue:在 React Storybook 中验证 Vue 侧 fallback 行为', + pageDescription: + 'plugin-i18next-vue 本来面向 Vue + i18next-vue 组件环境,但它也内置了 fallback 逻辑。这里用 React 容器展示它的最小契约:enum 会走 i18next 翻译,真正的 UI 刷新仍要依赖宿主框架。', + highlights: ['i18next-vue', 'fallback 路径', 'keyPrefix', '宿主刷新'], + runtimeTitle: '无需 Vue 组件,也能检查插件输出', + runtimeDescription: + '这里通过 keyPrefix 把 label key 映射到 delivery.* 命名空间,并用 useTranslationOptions.lng 控制当前 story 的 locale,避免污染别的 i18next story。', + currentLanguage: '当前生效 locale', + enumName: 'enum.name', + currentLabel: 'enum.label(value)', + rerenderTick: 'render tick', + switchZh: '切到中文', + switchEn: '切到英文', + rerender: '手动重渲染', + derivedTitle: '派生输出结构仍然成立', + derivedDescription: + '这里虽然重点在 fallback 合同,但对界面层来说,toList / toMap / items 依然是可直接消费的稳定出口。', + listTitle: 'toList() 输出', + mapTitle: 'toMap() 输出', + note: '在真实 Vue 页面中,你会通过 i18next-vue 的 useTranslation 获取响应式更新;这里展示的是 fallback 合同是否成立。', + codeTitle: '推荐安装方式', + code: `import i18nextVuePlugin from '@enum-plus/plugin-i18next-vue';\nimport { Enum } from 'enum-plus';\n\nEnum.install(i18nextVuePlugin, {\n localize: {\n useTranslationOptions: { keyPrefix: 'delivery' },\n },\n});`, + } + : { + pageTitle: 'i18next-vue: validate the Vue-side fallback from React Storybook', + pageDescription: + 'plugin-i18next-vue primarily targets Vue + i18next-vue component environments, but it also includes a fallback path. This story uses a React shell to validate the minimum contract: enum labels resolve through i18next, while visible UI refresh still depends on the host framework.', + highlights: ['i18next-vue', 'Fallback path', 'keyPrefix', 'Host refresh'], + runtimeTitle: 'Inspect plugin output without a Vue component shell', + runtimeDescription: + 'This story maps enum label keys into a delivery.* prefix and drives the locale through useTranslationOptions.lng so it stays isolated from other i18next-based stories.', + currentLanguage: 'effective locale', + enumName: 'enum.name', + currentLabel: 'enum.label(value)', + rerenderTick: 'render tick', + switchZh: 'Switch to Chinese', + switchEn: 'Switch to English', + rerender: 'Manual rerender', + derivedTitle: 'Derived outputs still hold up', + derivedDescription: + 'The fallback contract is the focus here, but UI-facing outputs like toList, toMap, and items are still stable and reusable.', + listTitle: 'toList() output', + mapTitle: 'toMap() output', + note: 'In a real Vue page, you would use i18next-vue useTranslation for reactive updates. This page focuses on proving the fallback contract.', + codeTitle: 'Recommended installation', + code: `import i18nextVuePlugin from '@enum-plus/plugin-i18next-vue';\nimport { Enum } from 'enum-plus';\n\nEnum.install(i18nextVuePlugin, {\n localize: {\n useTranslationOptions: { keyPrefix: 'delivery' },\n },\n});`, + }; +} + +function I18nextVueStory() { + const storyLocale = useStoryLocale(); + const copy = useCopy(); + const [renderTick, setRenderTick] = useState(0); + const [activeLocale, setActiveLocale] = useState<'en-US' | 'zh-CN'>(storyLocale); + + useEffect(() => { + setActiveLocale(storyLocale); + }, [storyLocale]); + + useEffect(() => { + void (async () => { + if (!i18next.isInitialized) { + await i18next.init({ + lng: 'en-US', + fallbackLng: 'en-US', + initImmediate: false, + resources: {}, + }); + } + + (Object.entries(STORY_RESOURCES) as Array<['en-US' | 'zh-CN', (typeof STORY_RESOURCES)['en-US']]>).forEach( + ([locale, resource]) => { + if (i18next.hasResourceBundle(locale, STORY_NAMESPACE)) { + i18next.removeResourceBundle(locale, STORY_NAMESPACE); + } + i18next.addResourceBundle(locale, STORY_NAMESPACE, resource, true, true); + }, + ); + })(); + + return () => { + (Object.keys(STORY_RESOURCES) as Array<'en-US' | 'zh-CN'>).forEach((locale) => { + if (i18next.hasResourceBundle(locale, STORY_NAMESPACE)) { + i18next.removeResourceBundle(locale, STORY_NAMESPACE); + } + }); + }; + }, []); + + useEffect(() => { + const previousLocalize = Enum.localize; + i18nextVuePlugin( + { + localize: { + useTranslationOptions: { keyPrefix: 'delivery', lng: activeLocale }, + tOptions: { ns: STORY_NAMESPACE }, + }, + }, + Enum as never, + ); + return () => { + Enum.localize = previousLocalize; + }; + }, [activeLocale]); + + const deliveryEnum = useMemo( + () => + Enum( + { + Pending: { value: 'pending', label: 'pending', tone: 'default' }, + Review: { value: 'review', label: 'review', tone: 'processing' }, + Shipped: { value: 'shipped', label: 'shipped', tone: 'success' }, + }, + { name: 'enumName' }, + ), + [activeLocale, renderTick], + ); + + return ( + + + + + + + + + + + + {copy.note} + + + + } + right={} + /> + + + + + + {deliveryEnum.items.map((item) => { + const raw = item.raw as { tone?: string }; + return ( + + {item.label} + + ); + })} + + } + right={} + /> + + + + ); +} + +export const Playground: Story = { + render: function Render() { + return ; + }, +}; diff --git a/.storybook/stories/PluginNextInternational.stories.tsx b/.storybook/stories/PluginNextInternational.stories.tsx new file mode 100644 index 00000000..dd09769b --- /dev/null +++ b/.storybook/stories/PluginNextInternational.stories.tsx @@ -0,0 +1,302 @@ +import { type ReactNode, useEffect, useMemo, useState } from 'react'; +import { AppRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime'; +import { PathnameContext, PathParamsContext } from 'next/dist/shared/lib/hooks-client-context.shared-runtime'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { createI18nClient } from 'next-international/client'; +import { Button, Card, Descriptions, Input, Select, Space, Tag, Typography } from 'antd'; +import { clientI18nPlugin, PatchedI18nProviderClient } from '../../packages/plugin-next-international/src'; +import { Enum } from '../../src'; +import { storyT, useStoryLocale } from '../locales'; +import { CodePreview, StoryPage, StorySection, TwoColumn } from './shared/demo'; + +const { Paragraph, Text } = Typography; + +const meta: Meta = { + title: 'Plugins/05 Next International', + parameters: { + docs: { + description: { + component: storyT('storybook.stories.PluginNextInternational.metaDescription'), + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +type NextLocalizedEnum = ReturnType & { + isMatch(search: string | undefined, item: unknown): boolean; +}; + +type LocalizedOption = { + key: string; + value: string; + label: ReactNode; + raw?: { + tone?: string; + label?: string; + }; +}; + +let nextIntlPluginInstalled = false; + +function ensureNextIntlPlugin() { + if (nextIntlPluginInstalled) { + return; + } + + Enum.install(clientI18nPlugin as unknown as Parameters[0], { + localize: { mode: 'component' }, + isMatch: { defaultSearchField: 'label' }, + }); + nextIntlPluginInstalled = true; +} + +function useCopy() { + const locale = useStoryLocale(); + + return locale === 'zh-CN' + ? { + pageTitle: 'Next International:把 Next App Router 的语言上下文接进 enum-plus', + pageDescription: + '@enum-plus/plugin-next-international 的重点不只是翻译 label,而是把 next-international 的 client runtime 接到 enum-plus,并通过 PatchedI18nProviderClient 让枚举文本与搜索能力感知当前 locale。', + highlights: ['PatchedI18nProviderClient', 'clientI18nPlugin', 'App Router context', '自动刷新'], + runtimeTitle: '最小可运行的 Client Provider 闭环', + runtimeDescription: + '这个故事页复用了插件测试中的核心思路:构造 Next router context,再用 PatchedI18nProviderClient 把 runtime.t 注入给 enum-plus。', + currentLocale: '当前 locale', + enumName: 'enum.name', + selectedLabel: 'label(value)', + switchZh: '切到中文', + switchEn: '切到英文', + search: '搜索', + searchNote: 'isMatch 会基于当前翻译文本匹配,而不是原始 key。', + codeTitle: '推荐接入方式', + code: `import { clientI18nPlugin, PatchedI18nProviderClient } from '@enum-plus/plugin-next-international';\nimport { Enum } from 'enum-plus';\n\nEnum.install(clientI18nPlugin, {\n localize: { mode: 'component' },\n isMatch: { defaultSearchField: 'label' },\n});\n\n...`, + matchedTitle: '当前搜索命中项', + note: '这里的自动更新来自 Next International provider,而不是手动重建 enum。', + } + : { + pageTitle: 'Next International: connect Next App Router locale context to enum-plus', + pageDescription: + '@enum-plus/plugin-next-international does more than translate labels. It connects the next-international client runtime to enum-plus, and PatchedI18nProviderClient lets enum text and search follow the active locale.', + highlights: ['PatchedI18nProviderClient', 'clientI18nPlugin', 'App Router context', 'Auto refresh'], + runtimeTitle: 'A minimal client-provider loop', + runtimeDescription: + 'This page follows the same core idea used in the package tests: build a tiny Next router context, then inject runtime.t into enum-plus through PatchedI18nProviderClient.', + currentLocale: 'Current locale', + enumName: 'enum.name', + selectedLabel: 'label(value)', + switchZh: 'Switch to Chinese', + switchEn: 'Switch to English', + search: 'Search', + searchNote: 'isMatch works against the translated text, not the raw locale key.', + codeTitle: 'Recommended setup', + code: `import { clientI18nPlugin, PatchedI18nProviderClient } from '@enum-plus/plugin-next-international';\nimport { Enum } from 'enum-plus';\n\nEnum.install(clientI18nPlugin, {\n localize: { mode: 'component' },\n isMatch: { defaultSearchField: 'label' },\n});\n\n...`, + matchedTitle: 'Current matched items', + note: 'The automatic update here comes from the Next International provider, not from rebuilding the enum manually.', + }; +} + +function NextIntlProviderShell(props: { + locale: 'en-US' | 'zh-CN'; + I18n: ReturnType; + children: ReactNode; +}) { + const { locale, I18n, children } = props; + + return ( + undefined, + replace: async () => undefined, + prefetch: async () => undefined, + back: () => undefined, + forward: () => undefined, + refresh: () => undefined, + }} + > + + + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + + {children} + + + + + ); +} + +function NextIntlRuntimeBootstrap(props: { onReady: () => void }) { + const { onReady } = props; + + useEffect(() => { + onReady(); + }, [onReady]); + + return ( + + Initializing Next International runtime… + + ); +} + +function NextIntlLocalizedContent(props: { + locale: 'en-US' | 'zh-CN'; + selectedValue: string; + searchText: string; + copy: ReturnType; + onSelect: (value: string) => void; +}) { + const { locale, selectedValue, searchText, copy, onSelect } = props; + + const deliveryEnum = useMemo( + () => + Enum( + { + Pending: { value: 'pending', label: 'delivery.pending', tone: 'default' }, + Review: { value: 'review', label: 'delivery.review', tone: 'processing' }, + Published: { value: 'published', label: 'delivery.published', tone: 'success' }, + }, + { name: 'delivery.enumName' }, + ) as NextLocalizedEnum, + [], + ); + + const options = deliveryEnum.items.map((item) => ({ + key: item.key, + value: item.value, + label: item.label, + raw: item.raw as { tone?: string; label?: string } | undefined, + })) as LocalizedOption[]; + + const selectedItem = deliveryEnum.getByValue(selectedValue); + const matchedItems = options.filter((item) => deliveryEnum.isMatch(searchText, item)); + + return ( + <> + + + setSearchText(event.target.value)} /> + + + {runtimeReady ? ( + + ) : ( + setRuntimeReady(true)} /> + )} + + + + + ); +} + +export const Playground: Story = { + render: function Render() { + return ; + }, +}; diff --git a/.storybook/stories/PluginReactI18n.stories.tsx b/.storybook/stories/PluginReactI18n.stories.tsx index 8b0e9431..d9285297 100644 --- a/.storybook/stories/PluginReactI18n.stories.tsx +++ b/.storybook/stories/PluginReactI18n.stories.tsx @@ -27,7 +27,7 @@ const { Text } = Typography; const activeI18n = i18next; const meta: Meta = { - title: 'Plugins/React I18n', + title: 'Plugins/03 React', parameters: { docs: { description: { diff --git a/.storybook/stories/PluginReactI18next.stories.tsx b/.storybook/stories/PluginReactI18next.stories.tsx new file mode 100644 index 00000000..166c8aaa --- /dev/null +++ b/.storybook/stories/PluginReactI18next.stories.tsx @@ -0,0 +1,172 @@ +import { useEffect, useMemo, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Button, Card, Descriptions, Space, Tag, Typography } from 'antd'; +import reactI18nextPlugin from '../../packages/plugin-react-i18next/src'; +import { Enum } from '../../src'; +import { storyT, useStoryLocale } from '../locales'; +import { CodePreview, JsonPreview, StoryPage, StorySection, TwoColumn } from './shared/demo'; +import { ensureStoryI18n } from './shared/i18n'; + +const { Paragraph } = Typography; + +const meta: Meta = { + title: 'Plugins/04 react-i18next', + parameters: { + docs: { + description: { + component: storyT('storybook.stories.PluginReactI18next.metaDescription'), + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +function useCopy() { + const locale = useStoryLocale(); + + return locale === 'zh-CN' + ? { + pageTitle: 'react-i18next:共享 React i18n 实例,但不自动刷新界面', + pageDescription: + '@enum-plus/plugin-react-i18next 适合已经使用 react-i18next 的项目,它会直接读取当前 React i18n 实例进行翻译。但 README 也明确写了:这个包本身不负责切换语言后的自动 UI 更新,如果你需要真正的“界面跟着变”,应使用 plugin-react。', + highlights: ['react-i18next', '读取 getI18n()', '不自动刷新 UI', '适合轻量接入'], + comparisonTitle: '它解决的是“取翻译”,不是“推刷新”', + comparisonDescription: '下面的 demo 和 i18next 版很像:语言已经变了,但界面要等 React 重新渲染后才会更新。', + currentLanguage: 'i18n.language', + enumName: 'enum.name', + currentLabel: 'enum.label(value)', + rerenderTick: 'render tick', + switchZh: '只切到中文(不强制刷新)', + switchEn: '只切到英文(不强制刷新)', + rerender: '手动触发 React 重渲染', + note: '如果你希望下拉 options、当前已选值、甚至搜索匹配都能在切换语言后自动刷新,那应该看 Plugins/03 React。', + codeTitle: '轻量安装方式', + listTitle: 'toList() 输出', + mapTitle: 'toMap() 输出', + code: `import reactI18nextPlugin from '@enum-plus/plugin-react-i18next';\nimport { Enum } from 'enum-plus';\n\nEnum.install(reactI18nextPlugin, {\n localize: {\n tOptions: { ns: 'translation' },\n },\n});`, + derivedTitle: '依然可以产出稳定结构', + derivedDescription: '它同样适合作为 enum 标签翻译层,然后把结果喂给普通组件。', + } + : { + pageTitle: 'react-i18next: shared React i18n instance, but no automatic UI refresh', + pageDescription: + '@enum-plus/plugin-react-i18next fits projects that already use react-i18next. It reads translations from the current React i18n instance. But the README is explicit: this package alone does not auto-refresh the UI after language changes. For that, use plugin-react.', + highlights: ['react-i18next', 'Reads getI18n()', 'No auto UI refresh', 'Lightweight integration'], + comparisonTitle: 'It solves translation lookup, not refresh propagation', + comparisonDescription: + 'This behaves similarly to the plain i18next story: the language changes immediately, but visible labels wait for the next React rerender.', + currentLanguage: 'i18n.language', + enumName: 'enum.name', + currentLabel: 'enum.label(value)', + rerenderTick: 'render tick', + switchZh: 'Switch to Chinese only (no forced refresh)', + switchEn: 'Switch to English only (no forced refresh)', + rerender: 'Force one React rerender', + note: 'If you want select options, selected labels, and search helpers to refresh automatically on language changes, see Plugins/03 React.', + codeTitle: 'Lightweight installation', + listTitle: 'toList() output', + mapTitle: 'toMap() output', + code: `import reactI18nextPlugin from '@enum-plus/plugin-react-i18next';\nimport { Enum } from 'enum-plus';\n\nEnum.install(reactI18nextPlugin, {\n localize: {\n tOptions: { ns: 'translation' },\n },\n});`, + derivedTitle: 'Stable outputs still matter', + derivedDescription: + 'It still works well as the translation layer for enum labels before feeding regular UI components.', + }; +} + +function ReactI18nextStory() { + const copy = useCopy(); + const storyLocale = useStoryLocale(); + const instance = ensureStoryI18n(); + const [renderTick, setRenderTick] = useState(0); + + useEffect(() => { + void instance.changeLanguage(storyLocale); + }, [instance, storyLocale]); + + useEffect(() => { + const previousLocalize = Enum.localize; + reactI18nextPlugin({ localize: { tOptions: { ns: 'translation' } } }, Enum as never); + return () => { + Enum.localize = previousLocalize; + }; + }, []); + + const statusEnum = useMemo( + () => + Enum( + { + Draft: { value: 'draft', label: 'story.status.draft', tone: 'default' }, + Review: { value: 'review', label: 'story.status.review', tone: 'processing' }, + Published: { value: 'published', label: 'story.status.published', tone: 'success' }, + Archived: { value: 'archived', label: 'story.status.archived', tone: 'default' }, + }, + { + name: 'story.status.enumName', + }, + ), + [renderTick], + ); + + return ( + + + + + + + + + + + + {copy.note} + + + + } + right={} + /> + + + + + + {statusEnum.items.map((item) => { + const raw = item.raw as { tone?: string }; + return ( + + {item.label} + + ); + })} + + } + right={} + /> + + + + ); +} + +export const Playground: Story = { + render: function Render() { + return ; + }, +}; diff --git a/.storybook/stories/PluginSample.stories.tsx b/.storybook/stories/PluginSample.stories.tsx new file mode 100644 index 00000000..a2fea286 --- /dev/null +++ b/.storybook/stories/PluginSample.stories.tsx @@ -0,0 +1,171 @@ +import { useMemo, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Button, Card, Descriptions, Space, Typography } from 'antd'; +import samplePlugin from '../../packages/plugin-sample/src'; +import { Enum } from '../../src'; +import { storyT, useStoryLocale } from '../locales'; +import { CodePreview, JsonPreview, StoryPage, StorySection, TwoColumn } from './shared/demo'; + +const { Paragraph } = Typography; + +const meta: Meta = { + title: 'Plugins/08 Sample Plugin', + parameters: { + docs: { + description: { + component: storyT('storybook.stories.PluginSample.metaDescription'), + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +type SampleEnum = ReturnType & { + sample(): void; +}; + +let samplePluginInstalled = false; + +function ensureSamplePlugin() { + if (samplePluginInstalled) { + return; + } + + Enum.install(samplePlugin as unknown as Parameters[0], { foo: 'storybook-demo' }); + samplePluginInstalled = true; +} + +function useCopy() { + const locale = useStoryLocale(); + + return locale === 'zh-CN' + ? { + pageTitle: 'Sample Plugin:用最小例子解释 enum-plus 的插件机制', + pageDescription: + '这个插件并不解决业务问题,而是用来说明“插件到底怎么扩展 enum-plus”。它通过 Enum.extends 给每个 enum 实例新增了一个 sample() 方法,是理解自定义插件最直观的入口。', + highlights: ['Enum.extends', '自定义扩展方法', '插件 authoring', '最小闭环'], + runtimeTitle: '点击按钮,调用插件新增的实例方法', + runtimeDescription: + 'sample() 的实现很简单:读取安装时的 foo 选项,并把结果输出到 console。这里我们额外把日志捕获到了页面上。', + run: '运行 sample()', + enumName: 'enum.name', + methodName: '新增方法', + optionFoo: '安装参数 foo', + lastOutput: '最近一次输出', + statusName: '示例状态', + draft: '草稿', + review: '待审核', + snapshotTitle: '枚举原始快照', + rawTitle: 'enum.raw()', + note: '如果你要做的是 toBadgeMap、toStatCard、业务搜索帮助器,本质上也是同一套插件扩展机制。', + codeTitle: '插件安装方式', + code: `import samplePlugin from '@enum-plus/plugin-sample';\nimport { Enum } from 'enum-plus';\n\nEnum.install(samplePlugin, { foo: 'storybook-demo' });\n\nconst Status = Enum({ Draft: 'draft' });\nStatus.sample();`, + } + : { + pageTitle: 'Sample Plugin: explain enum-plus plugin authoring with the smallest example', + pageDescription: + 'This plugin is not about business features. It exists to show how plugin authoring works in enum-plus. It uses Enum.extends to add one sample() method to every enum instance, making it the clearest entry point for custom plugin design.', + highlights: ['Enum.extends', 'Custom instance method', 'Plugin authoring', 'Smallest loop'], + runtimeTitle: 'Click the button to call the plugin-added instance method', + runtimeDescription: + 'sample() is intentionally simple: it reads the foo option provided at installation time and prints a log message. This story also captures that output back into the page.', + run: 'Run sample()', + enumName: 'enum.name', + methodName: 'Added method', + optionFoo: 'Install option foo', + lastOutput: 'Latest output', + statusName: 'Sample Status', + draft: 'Draft', + review: 'Review', + snapshotTitle: 'Enum snapshot', + rawTitle: 'enum.raw()', + note: 'If you later build helpers like toBadgeMap, toStatCard, or business search adapters, they follow the same extension mechanism.', + codeTitle: 'Plugin installation', + code: `import samplePlugin from '@enum-plus/plugin-sample';\nimport { Enum } from 'enum-plus';\n\nEnum.install(samplePlugin, { foo: 'storybook-demo' });\n\nconst Status = Enum({ Draft: 'draft' });\nStatus.sample();`, + }; +} + +function SamplePluginStory() { + ensureSamplePlugin(); + const copy = useCopy(); + const [lastOutput, setLastOutput] = useState('-'); + + const statusEnum = useMemo( + () => + Enum( + { + Draft: { value: 'draft', label: copy.draft }, + Review: { value: 'review', label: copy.review }, + }, + { name: copy.statusName }, + ) as SampleEnum, + [copy.draft, copy.review, copy.statusName], + ); + + const runSample = () => { + const original = console.log; + const buffer: string[] = []; + + console.log = (...args: unknown[]) => { + buffer.push(args.map((item) => String(item)).join(' ')); + original(...args); + }; + + try { + statusEnum.sample(); + } finally { + console.log = original; + setLastOutput(buffer.join('\n') || '-'); + } + }; + + return ( + + + + + + + + {copy.note} + + + + } + right={} + /> + + + + + + + ); +} + +export const Playground: Story = { + render: function Render() { + return ; + }, +}; diff --git a/.storybook/stories/PluginVueI18n.stories.tsx b/.storybook/stories/PluginVueI18n.stories.tsx new file mode 100644 index 00000000..9ee247bf --- /dev/null +++ b/.storybook/stories/PluginVueI18n.stories.tsx @@ -0,0 +1,194 @@ +import { useEffect, useMemo, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { createI18n } from 'vue-i18n'; +import { Button, Card, Descriptions, Space, Tag, Typography } from 'antd'; +import vueI18nPlugin from '../../packages/plugin-vue-i18n/src'; +import { Enum } from '../../src'; +import { storyT, useStoryLocale } from '../locales'; +import { CodePreview, JsonPreview, StoryPage, StorySection, TwoColumn } from './shared/demo'; + +const { Paragraph } = Typography; + +const meta: Meta = { + title: 'Plugins/06 Vue I18n', + parameters: { + docs: { + description: { + component: storyT('storybook.stories.PluginVueI18n.metaDescription'), + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +function useCopy() { + const locale = useStoryLocale(); + + return locale === 'zh-CN' + ? { + pageTitle: 'Vue I18n:在非 Vue 容器里也能用实例回退路径验证输出', + pageDescription: + 'plugin-vue-i18n 的设计目标是给 Vue 组件环境用;但它也提供了 instance 回退能力。因此即使当前 Storybook 是 React 壳子,我们仍然可以验证:枚举 label 会通过 vue-i18n 实例翻译,UI 更新则由宿主重渲染负责。', + highlights: ['vue-i18n', 'instance fallback', '非 Vue 容器验证', '本地化输出'], + runtimeTitle: 'React 壳中的最小回退闭环', + runtimeDescription: + '这里故意不进入 Vue 组件上下文,而是直接把 vue-i18n instance 传给插件,验证 fallback 输出路径。', + currentLocale: 'i18n.locale', + enumName: 'enum.name', + currentLabel: 'enum.label(value)', + rerenderTick: 'render tick', + switchZh: '切到中文', + switchEn: '切到英文', + rerender: '手动重渲染', + derivedTitle: '派生输出依然稳定可用', + derivedDescription: + '即使这里主要验证的是本地化回退路径,toList / toMap / items 这些 UI 输出结构仍然可以继续复用。', + listTitle: 'toList() 输出', + mapTitle: 'toMap() 输出', + note: '在真实 Vue 组件里,plugin-vue-i18n 可以配合 useI18n 工作;这里展示的是独立于 Vue UI 的最小功能闭环。', + codeTitle: '推荐安装方式', + code: `import vueI18nPlugin from '@enum-plus/plugin-vue-i18n';\nimport { Enum } from 'enum-plus';\n\nEnum.install(vueI18nPlugin, {\n localize: {\n instance: i18n,\n },\n});`, + } + : { + pageTitle: 'Vue I18n: validate the instance fallback path outside a Vue shell', + pageDescription: + 'plugin-vue-i18n is designed for Vue component environments, but it also exposes an instance fallback. That lets this React-based Storybook still validate the core contract: enum labels resolve through a vue-i18n instance, while visible UI refresh depends on the host rerender.', + highlights: ['vue-i18n', 'Instance fallback', 'Non-Vue validation', 'Localized output'], + runtimeTitle: 'A minimal fallback loop inside a React shell', + runtimeDescription: + 'This intentionally avoids a Vue component context and passes a vue-i18n instance directly to the plugin so the fallback behavior becomes visible.', + currentLocale: 'i18n.locale', + enumName: 'enum.name', + currentLabel: 'enum.label(value)', + rerenderTick: 'render tick', + switchZh: 'Switch to Chinese', + switchEn: 'Switch to English', + rerender: 'Manual rerender', + derivedTitle: 'Derived outputs still stay stable', + derivedDescription: + 'Even though this story focuses on the localization fallback path, UI-friendly structures like toList, toMap, and items remain reusable.', + listTitle: 'toList() output', + mapTitle: 'toMap() output', + note: 'Inside real Vue components, plugin-vue-i18n can work with useI18n. This story focuses on the smallest framework-independent functional loop.', + codeTitle: 'Recommended installation', + code: `import vueI18nPlugin from '@enum-plus/plugin-vue-i18n';\nimport { Enum } from 'enum-plus';\n\nEnum.install(vueI18nPlugin, {\n localize: {\n instance: i18n,\n },\n});`, + }; +} + +function VueI18nStory() { + const storyLocale = useStoryLocale(); + const copy = useCopy(); + const [renderTick, setRenderTick] = useState(0); + + const instance = useMemo( + () => + createI18n({ + legacy: false, + locale: storyLocale, + fallbackLocale: 'en-US', + messages: { + 'en-US': { + 'delivery.enumName': 'Delivery Status', + 'delivery.pending': 'Pending', + 'delivery.review': 'In Review', + 'delivery.shipped': 'Shipped', + }, + 'zh-CN': { + 'delivery.enumName': '交付状态', + 'delivery.pending': '待处理', + 'delivery.review': '审核中', + 'delivery.shipped': '已发出', + }, + }, + }), + [], + ); + + useEffect(() => { + instance.global.locale.value = storyLocale; + }, [instance, storyLocale]); + + useEffect(() => { + const previousLocalize = Enum.localize; + vueI18nPlugin({ localize: { instance } }, Enum as never); + return () => { + Enum.localize = previousLocalize; + }; + }, [instance]); + + const deliveryEnum = useMemo( + () => + Enum( + { + Pending: { value: 'pending', label: 'delivery.pending', tone: 'default' }, + Review: { value: 'review', label: 'delivery.review', tone: 'processing' }, + Shipped: { value: 'shipped', label: 'delivery.shipped', tone: 'success' }, + }, + { name: 'delivery.enumName' }, + ), + [renderTick], + ); + + return ( + + + + + + + + + + + + {copy.note} + + + + } + right={} + /> + + + + + + {deliveryEnum.items.map((item) => { + const raw = item.raw as { tone?: string }; + return ( + + {item.label} + + ); + })} + + } + right={} + /> + + + + ); +} + +export const Playground: Story = { + render: function Render() { + return ; + }, +}; diff --git a/package-lock.json b/package-lock.json index bc4e9235..ed79d4ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,8 @@ "ts-node": "^10.9.2", "tsx": "^4.21.0", "typescript": "^5.9.3", - "vite": "^7.3.1" + "vite": "^7.3.1", + "vue-i18n": "^9.14.5" }, "engines": { "node": ">=7.10.1" @@ -2791,6 +2792,53 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@intlify/core-base": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz", + "integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@intlify/message-compiler": "9.14.5", + "@intlify/shared": "9.14.5" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.5.tgz", + "integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@intlify/shared": "9.14.5", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz", + "integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -8335,6 +8383,13 @@ "@vue/shared": "3.5.31" } }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@vue/reactivity": { "version": "3.5.31", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz", @@ -29870,6 +29925,28 @@ "node": ">=10" } }, + "node_modules/vue-i18n": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz", + "integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==", + "deprecated": "v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html", + "dev": true, + "license": "MIT", + "dependencies": { + "@intlify/core-base": "9.14.5", + "@intlify/shared": "9.14.5", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -30872,66 +30949,6 @@ "enum-plus": "^3.0.0", "vue-i18n": ">=9.2.0" } - }, - "packages/plugin-vue-i18n/node_modules/@intlify/core-base": { - "version": "9.14.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@intlify/message-compiler": "9.14.5", - "@intlify/shared": "9.14.5" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/kazupon" - } - }, - "packages/plugin-vue-i18n/node_modules/@intlify/message-compiler": { - "version": "9.14.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@intlify/shared": "9.14.5", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/kazupon" - } - }, - "packages/plugin-vue-i18n/node_modules/@intlify/shared": { - "version": "9.14.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/kazupon" - } - }, - "packages/plugin-vue-i18n/node_modules/vue-i18n": { - "version": "9.14.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@intlify/core-base": "9.14.5", - "@intlify/shared": "9.14.5", - "@vue/devtools-api": "^6.5.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/kazupon" - }, - "peerDependencies": { - "vue": "^3.0.0" - } } } } diff --git a/package.json b/package.json index 61981083..de4938aa 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,8 @@ "ts-node": "^10.9.2", "tsx": "^4.21.0", "typescript": "^5.9.3", - "vite": "^7.3.1" + "vite": "^7.3.1", + "vue-i18n": "^9.14.5" }, "packageManager": "npm@11.12.1", "engines": {