UNPKG

@ui18n/selector-react

Version:

🎨 Beautiful, accessible language selector component for React with auto-discovery, custom styling, and zero dependencies

221 lines (193 loc) 7.72 kB
# @ui18n/selector-react 极简、可访问的语言选择下拉(React)。默认仅中/英,支持自动发现 /.ui18n/languages.json 扩展语言。无统计、无后端依赖,开放而轻量。 特性 - 直接上手:仅一个 UI 组件 + 若干工具函数 - 自动发现:可静默读取 /.ui18n/languages.json(数组或 {languages:[]}) - A11y 友好:键盘导航、ARIA 标注 - 受控/非受控均可,支持自定义渲染项 - 零供应商锁定:只做语言入口,不绑定翻译后端 - 可选“预览能力”:开发/演示态下可注入回调显示一句话的翻译预览(默认关闭,纯可选) 安装 - Monorepo(本仓库)内 demo 通过 file: 依赖引入 - 外部项目请使用包管理器安装(发布后): - npm i @ui18n/selector-react - peer: react/react-dom >= 18 快速上手 import { UI18nLanguageDropdown } from "@ui18n/selector-react"; function Example() { const [lang, setLang] = React.useState("en"); return ( <UI18nLanguageDropdown value={lang} onChange={setLang} // 默认仅 ["en","zh-CN"],可传入扩展列表 languages={["en","zh-CN","ja-JP"]} // 自动发现(默认 true);可关闭或自定义路径 autoDiscover discoverPath="/.ui18n/languages.json" placeholder="搜索语言…" className="w-full max-w-sm" /> ); } 自动发现协议 - 支持两种返回格式: - ["en","zh-CN","ja-JP"] - { "languages": ["en","zh-CN","ja-JP"] } - fetch 失败或未发现时自动降级为默认中英 API - 组件 UI18nLanguageDropdown - value?: string 受控值 - defaultValue?: string 非受控初始值(默认 "en") - onChange?: (lang: string) => void 变更回调 - languages?: string[] 预置语言(默认 ["en","zh-CN"]) - autoDiscover?: boolean 是否自动发现(默认 true) - discoverPath?: string 自动发现路径(默认 "/.ui18n/languages.json") - placeholder?: string | (ctx => string) 占位符(默认:系统语言本地化名称 + " - ui18n");可传函数以生成 - className?: string 自定义类名 - renderItem?: (lang: string, label: string) => React.ReactNode 自定义项渲染 - showBrandSuffix?: boolean 是否显示品牌后缀(默认 true) - brandSuffix?: string 品牌后缀字符串(默认 "- ui18n") - showSearchBox?: boolean 是否显示内置搜索框(默认 true) - renderSearchInput?: (props) => React.ReactNode 自定义搜索输入完整渲染 - filter?: (lang, query, meta) => boolean 自定义过滤逻辑 - selectFirstMatchOnEnter?: boolean 回车时若无聚焦项选中首个匹配(默认 true) - notFoundText?: string | (query => string) 无匹配提示文案(默认“无法找到该语言,请重新输入”) - debounceMs?: number 输入去抖毫秒数(默认 0) - 预览(可选,仅开发/演示用) - previewText?: string 要预览的一句话(如 "Hello world") - onPreviewRequest?: (args: { text: string; to: string; from?: string }) => Promise<string> - previewRender?: (state: { status: 'idle'|'loading'|'success'|'error'; result?: string; error?: unknown }) => React.ReactNode - Hooks/工具(按需引入) - useLanguageDropdown(options): 提供 headless 状态与 props 构造器 - 返回:{ status, list, filtered, current, open, query, activeIndex, refs, setOpen, setQuery, setActiveIndex, commitSelect, onKeyDown, getTriggerProps, getSearchInputProps, getListboxProps, getOptionProps } - useDiscoveredLanguages({ autoDiscover, discoverPath, initial }) - normalizeLocale(lang: string): string - labelForLang(lang: string): string 预览能力(可选) - 默认不显示。仅当同时提供 previewText 与 onPreviewRequest 时,下拉面板底部会出现一行“预览”区块。 - 组件不会直接请求任何后端;仅调用你提供的 onPreviewRequest 回调。 - 示例:通过你的后端代理 /api/translate 进行预览(前端不包含密钥) function Demo() { async function preview({ text, to, from }: { text: string; to: string; from?: string }) { const res = await fetch("/api/translate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text, to, from, model: "glm-4.5-flash" }), }); if (!res.ok) throw new Error(String(res.status)); const data = await res.json(); if (data?.ok) return String(data.data ?? ""); throw new Error(String(data?.error ?? "preview_failed")); } return ( <UI18nLanguageDropdown value={"en"} onChange={() => {}} previewText="Hello world" onPreviewRequest={preview} /> ); } 样式 - 默认极简样式(原生元素 + 少量 Tailwind 类) - 如果需要更精致的外观,可参考 demo 中的 shadcn 风格示例组件,自行组合 UI 库 可访问性 - 触发按钮具备 aria-haspopup/expanded/controls - 列表使用 role=listbox / option,支持键盘上下/回车/ESC 建议集成方式 - 词典/翻译来源由你的应用决定(本组件只提供语言入口) - 将所选语言与应用状态管理(如 context/store/URL)打通 - 对大体量词典建议懒加载 + 预取 + 缓存 开源协议 - MIT 最小集成片段 Vite(React + Vite) ```tsx // main.tsx import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; ReactDOM.createRoot(document.getElementById("root")!).render(<App />); ``` ```tsx // App.tsx import * as React from "react"; import { UI18nLanguageDropdown } from "@ui18n/selector-react"; export default function App() { const [lang, setLang] = React.useState("en"); return ( <div style={{ padding: 16 }}> <UI18nLanguageDropdown value={lang} onChange={setLang} languages={["en", "zh-CN"]} autoDiscover discoverPath="/.ui18n/languages.json" placeholder="搜索语言…" /> </div> ); } ``` Next.js(App Router) ```tsx // app/page.tsx (服务端) // 推荐在 Client 组件中使用语言下拉 export default function Page() { return <ClientHome />; } ``` ```tsx // app/client-home.tsx "use client"; import * as React from "react"; import { UI18nLanguageDropdown } from "@ui18n/selector-react"; export default function ClientHome() { const [lang, setLang] = React.useState("en"); return ( <UI18nLanguageDropdown value={lang} onChange={setLang} autoDiscover placeholder="搜索语言…" /> ); } ``` Create React App ```tsx // App.tsx import * as React from "react"; import { UI18nLanguageDropdown } from "@ui18n/selector-react"; function App() { const [lang, setLang] = React.useState("en"); return ( <UI18nLanguageDropdown value={lang} onChange={setLang} languages={["en", "zh-CN", "ja-JP"]} /> ); } export default App; ``` A11y 键盘导航清单 - 触发区(按钮) - Enter / Space:打开下拉 - Escape:关闭下拉并聚焦回按钮 - ArrowDown(在触发上):打开并聚焦首项 - 列表(role="listbox") - ArrowUp / ArrowDown:在选项间移动 - Home / End:跳转到首/尾项 - Enter / Space:选择当前聚焦项 - Escape:关闭下拉 - ARIA 语义 - 触发:aria-haspopup="listbox"、aria-expanded、aria-controls - 列表:role="listbox";选项:role="option"、aria-selected - 活跃项:aria-activedescendant(或 roving tabindex) - 屏幕阅读器 - 提供可读标签:aria-label 或与可见文本关联 - 状态变化(选择、错误)可用 aria-live(若在预览区块中提示) - 焦点可见性 - 明确的 focus 样式(轮廓或阴影),Tab 路径完整、可回退