@dts-stn/ react-router-i18n-routes (英语)
类型安全, 翻译 URL for 反应路由器 框架模式.
申报每页一次,每种语言都有路径. 您可以得到 React 路由器路由配置, 本地化链接和 重新定向,语言检测,和一个语言切换器. 每个路由文件和语言都是字符串字元类型,所以 断开的链接是编译错误,而不是404.
const i18nRoutes = defineI18nRoutes([
{
id: 'CASE-0001',
file: 'routes/case.tsx',
paths: { en: '/en/case/:caseId', fr: '/fr/dossier/:caseId' },
},
]);
const i18nRouting = createI18nRouting(i18nRoutes);
i18nRouting.getPath('routes/case.tsx', 'fr', { params: { caseId: '42' } }); // '/fr/dossier/42'
i18nRouting.translatePath('/en/case/42', 'fr'); // '/fr/dossier/42'
i18nRouting.getPath('routes/case.tsx', 'de'); // ❌ type error: 'de' is not a supported language
i18nRouting.getPath('routes/cases.tsx', 'en'); // ❌ type error: no such route file
为什么?
A 类 /:lang/... 当只有语言段更改时,前缀就足够了。 许多地点,包括政府驻地
官方语文规则、需要 完全翻译路径 相反: /en/dashboard 财务报告和审定财务报表 /fr/tableau-de-bord进行中
通过手工方式,每个语言登记每一页,保持路径同步,并重复绘图
每一个链接、方向和语言切换。 这个软件包保留了一个路线表,并构建了它以外的一切.
特征
- 🗺️ 一个真理的来源. 每页声明一次,React路由器获得每个语言的一个路由.
- 🔒 完全输入, 不
as const。 。 。 . 路由文件、身份证和语言被推断为文字结合。 未知的文件, 排版文件,编译时拒绝不支持的语言. - 🔗 链接和重定向.
<I18nLink>替换<Link>,以及redirect()正在建设Responses 用于装载器和 动作,有参数,搜索和散列. - 🌐 语言切换.
<I18nLanguageSwitchLink>链接到其它语言的当前页面并保留其 parms,搜索,和散列. - 🧭 URL匹配 . URL 使用 React Router 自身的排名映射回路由和语言, 所以静态片段总是 击败动态的.
- 🗣️ 语言谈判. 语言来自 URL, 有可选的
Accept-Language倒计时. - ✅ 失败的验证. 重复的ID或文件,缺少翻译,以及启动时抛出的路由ID相撞.
- 🪶 小而可树. 仅限无害环境管理,
sideEffects: false,没有运行时间依赖 超出它的同行,它的工作 与回放18和19以及回放路由器7和8.
目录
安装
npm install @dts-stn/react-router-i18n-routes
# or
pnpm add @dts-stn/react-router-i18n-routes
# or
yarn add @dts-stn/react-router-i18n-routes
所需资源
| 软件包 | 版本 | 需要时间 |
|---|---|---|
react-router | ^7.0.0 || ^8.0.0 | 所有的东西 |
react | >=18.0.0 | @dts-stn/react-router-i18n-routes/react |
@react-router/dev | ^7.0.0 || ^8.0.0 | @dts-stn/react-router-i18n-routes/route-config |
react 财务报告和审定财务报表 @react-router/dev 是可选的同伴。 只有当您使用需要的切入点时才安装它们 .
软件包是 仅限无害环境管理 并且需要20号或以后的节点。 类型推论依赖
const 类型参数, (中文(简体) ),
因此,需要TypeScript 5.0或更晚.
入境点
| 导入路径 | 目录 | 从 |
|---|---|---|
@dts-stn/react-router-i18n-routes | 路线定义, createI18nRouting()类型和错误 | 任何地方(客户端和服务器) |
@dts-stn/react-router-i18n-routes/react | 钩和连接组件 | 反应组件 |
@dts-stn/react-router-i18n-routes/route-config | 转换为 React 路由器路由配置 | app/routes.ts |
快开始
1. 确定路线
将定义保留在自己的模块中这样 app/routes.ts,组件、装载器和动作都可以导入.
// app/i18n-routes.ts
import type { I18nLanguageOf, I18nRouteFileOf } from '@dts-stn/react-router-i18n-routes';
import { createI18nRouting, defineI18nRoutes } from '@dts-stn/react-router-i18n-routes';
export const i18nRoutes = defineI18nRoutes([
{
id: 'HOME-0001',
file: 'routes/index.tsx',
paths: { en: '/en/', fr: '/fr/' },
},
{
// A pathless layout route
file: 'routes/protected/layout.tsx',
children: [
{
id: 'DASH-0001',
file: 'routes/protected/dashboard.tsx',
paths: { en: '/en/dashboard', fr: '/fr/tableau-de-bord' },
},
{
id: 'CASE-0001',
file: 'routes/protected/case.tsx',
paths: { en: '/en/case/:caseId', fr: '/fr/dossier/:caseId' },
},
],
},
]);
export const i18nRouting = createI18nRouting(i18nRoutes);
export type I18nRouteFile = I18nRouteFileOf<typeof i18nRoutes>;
// 'routes/index.tsx' | 'routes/protected/dashboard.tsx' | 'routes/protected/case.tsx'
export type Language = I18nLanguageOf<typeof i18nRoutes>;
// 'en' | 'fr'
2. 用回放路由器登记路线
// app/routes.ts
import type { RouteConfig } from '@react-router/dev/routes';
import { route } from '@react-router/dev/routes';
import { toRouteConfigEntries } from '@dts-stn/react-router-i18n-routes/route-config';
import { i18nRoutes } from './i18n-routes';
export default [
route('/api/health', 'routes/api/health.ts'), // non-translated routes work as usual
...toRouteConfigEntries(i18nRoutes),
] satisfies RouteConfig;
[大 反应路由器的配置加载器不能解决路径的别名, 例如
~/,所以导入定义到app/routes.ts和相对路径.
3. 连接、重新定向和切换语言
import {
I18nLanguageSwitchLink,
I18nLink,
useIsCurrentRoute,
useIsNavigatingTo,
useLanguage,
} from '@dts-stn/react-router-i18n-routes/react';
import { i18nRouting } from '~/i18n-routes';
export function Navigation() {
const { alternateLanguage } = useLanguage(i18nRouting);
const isDashboard = useIsCurrentRoute(i18nRouting, 'routes/protected/dashboard.tsx');
const isLoadingCase = useIsNavigatingTo(i18nRouting, 'routes/protected/case.tsx', { params: { caseId: '42' } });
return (
<nav>
{/* Links to the current language by default */}
<I18nLink routing={i18nRouting} file="routes/protected/dashboard.tsx" aria-current={isDashboard ? 'page' : undefined}>
Dashboard
</I18nLink>
{/* Any React Router <Link> prop is forwarded */}
<I18nLink
routing={i18nRouting}
file="routes/protected/case.tsx"
params={{ caseId: '42' }}
search="tab=notes"
prefetch="intent"
>
Case 42 {isLoadingCase && '…'}
</I18nLink>
{/* The current page in the other language, with the same params, search, and hash */}
<I18nLanguageSwitchLink routing={i18nRouting}>
{alternateLanguage === 'fr' ? 'Français' : 'English'}
</I18nLanguageSwitchLink>
</nav>
);
}
// app/routes/protected/layout.tsx
import { i18nRouting } from '~/i18n-routes';
import type { Route } from './+types/layout';
export function loader({ request }: Route.LoaderArgs) {
if (!isSignedIn(request)) {
const language = i18nRouting.getLanguage(request) ?? 'en';
throw i18nRouting.redirect('routes/index.tsx', language);
}
}
就是这样。 每个 file, (中文(简体) ), language,以及 params 以上值的键入 app/i18n-routes.ts。 。 。 .
概念
页路由
一个页面路由有一个 id,路径模块 file,以及每种语言的路径:
{ id: 'DASH-0001', file: 'routes/protected/dashboard.tsx', paths: { en: '/en/dashboard', fr: '/fr/tableau-de-bord' } }
file是路由模块,相对于应用程序目录,如:app/routes.ts。这也是 查找密钥数字 : 链接、重定向和钩子是指其文件中的一页.paths使用 React 路由器路径语法:活性区段( R):caseId),可选部分(:tab?),和花板 (单位:千美元)*)所有作业. 每个页面路径必须声明 同一套语言。 。 。 .id必须是独一无二的。 React 路由器每个语言获得一条路由,并带有 id${id}-${LANGUAGE}(单位:千美元)DASH-0001-EN, (中文(简体) ),DASH-0001-FR) (中文(简体) ). 把这些身份传递给useRouteLoaderData(),或用getI18nRouteId()。 。 。 .
布局路线
一个布局路线有一个 file 财务报告和审定财务报表 children 但是没有 id 或者说 paths。它会变成一个无路径回放路由器 layout() 路径
让孩子度过难关 <Outlet />,用于认证卫士、共享铬和共享
装载机 布局可以筑巢. 排版不能成为链接目标, 因此将排版文件传递到 getPath() 是类型
错误.
路径实例
createI18nRouting(i18nRoutes) 验证定义一次并返回
I18nRouting 对象,每个查找和 URL 构建器。 在模块级别创建并导入一次
无论需要什么 反应钩和组件 把它当作他们的第一个论据或 routing 道具 (英语).
食谱
设定 <html lang> 在根布局中
// app/root.tsx
import type { ReactNode } from 'react';
import { Links, Meta, Scripts, ScrollRestoration } from 'react-router';
import { useCurrentLanguage } from '@dts-stn/react-router-i18n-routes/react';
import { i18nRouting } from '~/i18n-routes';
export function Layout({ children }: { children: ReactNode }) {
const language = useCurrentLanguage(i18nRouting) ?? 'en';
return (
<html lang={language}>
<head>
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
重定向 / 改为客家首选语言
在 i18n 路由旁注册一个平面索引路由,并谈判语言 Accept-Language 页眉 :
// app/routes.ts
export default [index('routes/language-redirect.ts'), ...toRouteConfigEntries(i18nRoutes)] satisfies RouteConfig;
// app/routes/language-redirect.ts
import { i18nRouting } from '~/i18n-routes';
import type { Route } from './+types/language-redirect';
export function loader({ request }: Route.LoaderArgs) {
// The language of the URL path, then the Accept-Language header, then English
const language = i18nRouting.getLanguage(request, { acceptLanguage: true }) ?? 'en';
return i18nRouting.redirect('routes/index.tsx', language);
}
用正确的语言显示404页
getLanguage() 当没有页面路由匹配时, 返回到第一个路径段, 所以一个覆盖所有路由仍然知道
/fr/page-inexistante 法语:
// app/routes.ts
export default [...toRouteConfigEntries(i18nRoutes), route('*', 'routes/not-found.tsx')] satisfies RouteConfig;
// app/routes/not-found.tsx
export default function NotFound() {
const language = useCurrentLanguage(i18nRouting) ?? 'en';
return <h1>{language === 'fr' ? 'Page introuvable' : 'Page not found'}</h1>;
}
添加 hreflang sEO 的替代链接
import { useLocation } from 'react-router';
import { i18nRouting } from '~/i18n-routes';
const ORIGIN = 'https://www.example.com';
export function AlternateLinks() {
const { pathname } = useLocation();
return i18nRouting.languages.map((language) => {
const path = i18nRouting.translatePath(pathname, language);
return path && <link key={language} rel="alternate" hrefLang={language} href={new URL(path, ORIGIN).href} />;
});
}
渲染器 <AlternateLinks /> 内部 <head> 内 app/root.tsx。 。 。 .
支持三种或三种以上语言
alternateLanguage 仅适用于双语应用程序。 更多语言,列表 alternateLanguages 并传递每个
I18nLanguageSwitchLink数字 :
const LANGUAGE_NAMES = { en: 'English', fr: 'Français', es: 'Español' } as const;
export function LanguageMenu() {
const { alternateLanguages } = useLanguage(i18nRouting);
return (
<ul>
{alternateLanguages.map((language) => (
<li key={language}>
<I18nLanguageSwitchLink routing={i18nRouting} language={language}>
{LANGUAGE_NAMES[language]}
</I18nLanguageSwitchLink>
</li>
))}
</ul>
);
}
I18nLanguageSwitchLink 套装 lang 财务报告和审定财务报表 hrefLang 编辑 <a>,所以屏幕阅读器宣告每个语言名称
对.
切换语言时重新装入文档
如果每个文档都装入过一次翻译( 例如, 服务器上创建的 i18next 实例), 则通过
reloadDocument 这样切换语言就可以完成一个完整的页面负载 :
<I18nLanguageSwitchLink routing={i18nRouting} reloadDocument>
Français
</I18nLanguageSwitchLink>
构建特定应用链接组件
捆绑 routing 和默认样式 :
import type { I18nLinkProps } from '@dts-stn/react-router-i18n-routes/react';
import { I18nLink } from '@dts-stn/react-router-i18n-routes/react';
import type { i18nRoutes } from '~/i18n-routes';
import { i18nRouting } from '~/i18n-routes';
export function AppLink(props: Omit<I18nLinkProps<typeof i18nRoutes>, 'routing'>) {
return <I18nLink routing={i18nRouting} className="underline" {...props} />;
}
<AppLink file="routes/protected/dashboard.tsx">Dashboard</AppLink> // ✅
<AppLink file="routes/protected/dashbord.tsx">Dashboard</AppLink> // ❌ type error
用于非链接导航( U)navigate(), (中文(简体) ), <Form action>, (中文(简体) ), fetcher.submit()),使用时
useI18nPath() 建造道路.
处理路由错误
包扔出的每一个错误都是 I18nRoutingError 有机器可读 code数字 :
import { I18nRoutingError } from '@dts-stn/react-router-i18n-routes';
try {
const { route, language, params } = i18nRouting.getRouteByPath(pathname);
} catch (error) {
if (error instanceof I18nRoutingError && error.code === 'ROUTE_NOT_FOUND') {
// ...
}
throw error;
}
这个 find* 方法返回 undefined 而不是扔, 这往往更简单.
API 参考
@dts-stn/react-router-i18n-routes
defineI18nRoutes(routes)
回返 routes 不变,按文字键入,无 as const 需要帮助。 使用它来声明路由表.
createI18nRouting(routes)
验证路径并返回一个 I18nRouting 举个例子 掷出 I18nRoutingError 有代码
INVALID_ROUTES 如果:
- 两个页面路由共享 an
id或一个file, (中文(简体) ), - 页面路由并非全部声明相同的语言,或者
- 两个路由/语言对生成相同的React Router路由id. 例如,语言
en财务报告和审定财务报表EN碰撞,这样 执行路由PAGE使用语言fr-CA和路线PAGE-FR使用语言ca,两者均生成PAGE-FR-CA。 。 。 .
I18nRouting
| 成员 | 回返 | 说明 |
|---|---|---|
routes | T | 路线定义. |
languages | Language[] | 支持的语言,按声明顺序排列. |
getPath(file, language, options?) | string | 本地化,根相通的路径. options数字 : params, (中文(简体) ), search (单位:千美元)string 或者说 URLSearchParams, (中文(简体) ), hash。 。 。 . |
redirect(file, language, options?) | Response | 重定向到 getPath()。 。 。 . options.init 设置状态( 默认) 302或全数 ResponseInit。 。 。 . |
translatePath(pathname, language, options?) | string | undefined | 匹配的页面路由在另一种语言中的路径,保留其参数(/en/case/42 → /fr/dossier/42) (中文(简体) ). options数字 : search, (中文(简体) ), hash。 。 。 . |
getLanguage(resource, options?) | Language | undefined | 路径名的语言, URL,或 Request。见 语言检测。 。 。 . |
getAlternateLanguages(language) | Language[] | 其他按声明顺序支持语言. |
findRouteByPath(pathname) | { route, language, params } | undefined | 将路径名匹配到页面路由和语言. |
getRouteByPath(pathname) | { route, language, params } | 与 findRouteByPath()但抛出 ROUTE_NOT_FOUND。 。 。 . |
findRouteByFile(file) | PageRoute | undefined | 文件的页面路由 。 接受任意字符串,以便测试未知值. |
getRouteByFile(file) | PageRoute | 已知文件的页面路由, 输入为准确路由 。 掷出 ROUTE_NOT_FOUND。 。 。 . |
getI18nRouteId(id, language)
页面路由中的一种语言的回放路由器路由ID: getI18nRouteId('DASH-0001', 'fr') → 'DASH-0001-FR'。 。 。 .
negotiateLanguage(acceptLanguage, languages)
最佳支持语言 Accept-Language 页眉值,或 undefined。见
语言检测。 。 。 .
negotiateLanguage('fr-CA,fr;q=0.9,en;q=0.8', ['en', 'fr']); // 'fr'
isI18nPageRoute(value), (中文(简体) ), isI18nLayoutRoute(value)
为行走路线定义而设的护卫类型.
I18nRoutingError
包抛出的错误 。 内容 code 这是:
| 代码 | 掷出时 |
|---|---|
INVALID_ROUTES | createI18nRouting() 发现定义不一致. |
ROUTE_NOT_FOUND | 没有页面路由匹配文件或路径名传递到一个 get* 方法. |
LANGUAGE_NOT_DETERMINED | useI18nPath() 或者说 I18nLink 得到没有 language 而当前 URL 没有. |
类型
| 类型 | 说明 |
|---|---|
I18nRoute<L>, (中文(简体) ), I18nPageRoute<L>, (中文(简体) ), I18nLayoutRoute<L> | 路由定义类型, 通用于语言联盟 L。 。 。 . |
I18nPaths<L> | { [language]: path }。 。 。 . |
I18nRouteFileOf<T> | 页面路由文件在 typeof routes。 。 。 . |
I18nRouteIdOf<T> | 页面路由代码的结合. |
I18nLanguageOf<T> | 宣布的语言联盟 每个 页面路由. |
I18nPageRouteOf<T> | 页面路由定义的结合. |
I18nPageRouteForFile<T, F> | 文件的页面路由 F (返回类型 getRouteByFile()) (中文(简体) ). |
I18nRouting<T>, (中文(简体) ), I18nRouteMatch<T> | 路径实例和结果 findRouteByPath()。 。 。 . |
I18nPathOptions, (中文(简体) ), I18nRedirectOptions, (中文(简体) ), I18nTranslatePathOptions, (中文(简体) ), I18nLanguageOptions | 联合国 I18nRouting 方法。 。 . |
I18nRoutingErrorCode | 错误代码的结合. |
类型帮助者也接受广泛的类型,例如 I18nRoute<'en' | 'fr'>[]。对于这些,文件和身份证决心 string
和声明的联盟的语言.
@dts-stn/react-router-i18n-routes/react
每个钩子和元件都把路由实例当作它的第一论据(呼克)或 routing 道具(组件). 全体人员
他们从React路由器读取当前位置,所以必须在路由器内渲染出.
useLanguage(routing)
回返 { currentLanguage, alternateLanguage, alternateLanguages } 用于当前位置. alternateLanguage 设定
只有另外一种语言,即双语语言.
useCurrentLanguage(routing)
当前位置的语言,或 undefined (单位:千美元)routing.getLanguage(pathname)) (中文(简体) ).
useI18nRouteMatch(routing)
{ route, language, params } 用于当前位置,或 undefined (单位:千美元)routing.findRouteByPath(pathname)) (中文(简体) ).
useI18nPath(routing, file, options?)
routing.getPath(),与 options.language 默认为当前语言。 掷出 LANGUAGE_NOT_DETERMINED 若为
两者皆无。 用它来 navigate(), (中文(简体) ), <Form action>,其他需要路径而不是链接的任何东西.
useLanguageSwitchPath(routing, language?)
当前页 language (默认:替代语言),保留其参数,搜索和散列. 回返
undefined 在i18n路线之外.
useIsCurrentRoute(routing, file, options?)
当前位置是否 file任何语言的页面路线。 与 options.params,匹配的路径的参数
也必须平等。 用它来 aria-current="page"。 。 。 .
useIsNavigatingTo(routing, file, options?)
是否导航到 file页面路由正在装入 . options.language 默认为当前语言,并且
options.params 还比较了匹配路线的参数. 用于待定指标。 需要数据路由器
(框架模式, createBrowserRouter(),或 createMemoryRouter()) (中文(简体) ).
对于两者 useIsCurrentRoute() 财务报告和审定财务报表 useIsNavigatingTo()数字 :
file也许undefined(结果就是false)),所以你可以从其组件中无条件称呼它们 链接并不总是 i18n 路由.options.params仅比较匹配的路由本身的参数, 这样您就可以通过完整useParams()对象.
<I18nLink>
反应路由器 <Link> 谁 to 道具替换为 routing, (中文(简体) ), file选择 language, (中文(简体) ), params, (中文(简体) ),
search,以及 hash。语言默认为当前语言。 每一个 <Link> 道具,包括 ref, (中文(简体) ),
prefetch,以及 viewTransition页:1.
<I18nLanguageSwitchLink>
未设置的回放路由器 <Link> 改为 useLanguageSwitchPath(),与 lang
财务报告和审定财务报表 hrefLang 设置到目标语言。 内容 to 道具替换为 routing 和可选 language。 所有其他
道具,包括 ref 财务报告和审定财务报表 reloadDocument页:1 它在i18n路线之外没有任何东西.
@dts-stn/react-router-i18n-routes/route-config
| 导出 | 说明 |
|---|---|
toRouteConfigEntries(routes) | 将路由定义转换为 React Router 路由配置条目, 递归式 . |
toPageRouteConfigEntries(pageRoute) | 将一个页面路由转换为每个语言的一个条目,以便在树外注册. |
此切入点仅取决于 @react-router/dev/routes,因此从 app/routes.ts。 。 。 .
行为细节
路径匹配
findRouteByPath(), (中文(简体) ), translatePath(), (中文(简体) ), getLanguage(),而钩子与路由器的路径名匹配
matchRoutes() 跨越每一页路线和语言。 结果:
- 静态片段胜过动态片段, 动态片段胜过平板, 不管宣布的顺序,
- 随从的鞭打,是被忽视的,
- %- 编码路径名被解码,以及
- 路径名称与应用程序相对
basename。 。 。 .
路径生成
getPath() 使用响应路由器 generatePath()因此:
- 缺少所需的抛出物,
- 接后鞭打被去除(
/en/→/en, (中文(简体) ), - 结果总是从
/,即使没有一条道路被宣布(en/about→/en/about链接 从未相对于当前 URL 解析,以及 - 静态片段、搜索和散列按申报方式返回(
/fr/à-propos) (中文(简体) ).
redirect() %- encode 非 ASCII 字符 Location 页眉 (e)/fr/%C3%A0-propos)并留有
逃逸率不变.
语言检测
getLanguage(resource) 按顺序尝试 :
- 匹配页面路由的语言,
- 第一个路径段,如果它是一个支持的语言(
/fr/anything→fr),以及 - 仅使用
{ acceptLanguage: true }(单位:千美元)Request,则Accept-Language标题, 使用negotiateLanguage()。 。 。 .
negotiateLanguage() 尝试下行质量顺序的偏好, 链接保持标题顺序 。 确切地说,
大小写不敏感匹配获胜。 要不然就比较初级亚种,所以 fr-CA 匹配 fr 财务报告和审定财务报表 fr 匹配 fr-CA。 。 。 .
条目 q=0,则 * 通配符,以及错误的条目被忽略.
饼干,会话,用户偏好被故意留给应用程序处理. 先读一下,然后回到
getLanguage()。 。 。 .
财务问题
它能翻译我的内容吗?
没有。 它只处理URL. 与任何翻译库(i18next, react-intl,Lingui,.)对等,并传入语言
从 useCurrentLanguage() 或者说 getLanguage()。 。 。 .
一个页面能否只用一种语言存在?
不是作为i18n路由:每个页面路由必须声明每种语言,这保证了语言切换器总是
有目标 使用 React 路由器自行注册的单语页 route() 内 app/routes.ts。 。 。 .
我可以用一个 /:lang/... 样式而不是翻译路径 ?
对 道路是简单的线条,所以 { en: '/en/about', fr: '/fr/about' } 和被翻译的子弹一样有效.
它是否在声明式或数据模式下工作(无 app/routes.ts{\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}你觉得呢?
核心入站和回放钩与任何路由器配合. /route-config 专门针对框架模式,以及
useIsNavigatingTo() 需要数据路由器。 在其他模式中,从 i18nRoutes 与
getI18nRouteId()。 。 。 .
为什么路由识别像 DASH-0001-EN? 吗?
反应路由器每条路由需要独有的ID,每一页成为每条语言的一个路由. 使用您喜欢的任何 ID 方案
大写后缀为后缀.
捐款
欢迎提出问题和拉动请求。 见 捐款额 用于开发设置和发布过程.