@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
Markdown
# @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 路径完整、可回退