前端多语言别再硬编码!3步搞定i18n让老外也夸你代码香

前端多语言别再硬编码!3步搞定i18n让老外也夸你代码香

前端多语言别再硬编码!3步搞定i18n让老外也夸你代码香

前端多语言别再硬编码!3步搞定i18n让老外也夸你代码香


开篇先吐槽:还在用if-else判断语言?Out啦

说真的,每次看到项目里那种if (lang === 'zh') { return '你好' } else { return 'Hello' }的代码,我都想穿越回去给当时的自己两巴掌。这玩意儿就跟在代码里写死密码一样,当时觉得"哎呀就两种语言嘛,简单快捷",等产品经理突然在群里甩一句"老板说要支持泰语和越南语"的时候,你就知道什么叫技术债利滚利了。

我记得最惨的一次是18年做跨境电商项目,刚开始就中英双语,我图省事直接硬编码,变量名起得那叫一个随心所欲:text1text2btnText… 结果三个月后业务爆炸式增长,要加西班牙语、法语、德语。我那天晚上对着满屏的乱码和错位布局,差点把键盘吃了。最绝的是德语,那个长度你懂的,“设置"在德语里叫"Einstellungen”,按钮直接撑爆,UI妹子看我的眼神就像在看一个杀人凶手。

而且你们发现没有,现在的项目要是没i18n(国际化),就像吃泡面没调料包——能吃,但索然无味,甚至有点恶心。特别是做SaaS产品的,上来第一句话就是"支不支持多语言",你要说没有,人家转头就走,连价都不问。

最恐怖的场景是什么?是周五下班前五分钟,产品经理屁颠屁颠跑过来说:“刚接到个大客户,阿拉伯那边的,钱给够了,下周上线。” 阿拉伯语!RTL(从右到左)布局!你看着满屏的margin-leftfloat: right,心里只有一个念头:这班是非上不可吗?

所以啊,今天咱们就聊聊怎么把i18n这玩意儿整明白,让你下次遇到这种需求时能优雅地泡杯咖啡,淡淡地说:“哦,改个配置的事儿,十分钟。”


到底啥是i18n,别被缩写吓住

先给小白们科普一下,i18n 这个看起来像是密码学的缩写,其实就是 Internationalization(国际化)的偷懒写法——I和n中间有18个字母。同理还有 L10n(Localization,本地化),中间10个字母。

这俩词儿经常被人混着用,但其实有微妙差别。国际化(i18n)是"让软件有能力支持多种语言和地区",是技术架构层面的;本地化(L10n)是"真正去翻译内容、调整格式、适配文化习惯",是业务内容层面的。简单说,i18n是搭舞台,L10n是上台唱戏。

现在的前端框架里,这玩意儿已经长得非常成熟了。Vue有vue-i18n,React有react-i18next,Angular自己内置了$locale。它们的核心思路都差不多:把文字抽离成资源文件,运行时根据当前语言动态替换。听起来简单,但魔鬼在细节里,后面咱们慢慢扒。


选对工具真的能少加班,主流方案大乱斗

市面上方案多得让人眼花,我挑几个主流的给你唠唠,省得你选型时抓瞎。

i18next:老牌劲旅,生态丰富到怀疑人生

这货可以说是i18n界的 jQuery,老牌、稳定、生态丰富到让你怀疑人生。不光支持前端,Node.js、React Native、甚至Flutter都能用。插件系统更是离谱,你要啥功能基本都有现成的。

// i18next 基础配置示例import i18next from'i18next';// 初始化配置 i18next.init({lng:'zh',// 当前语言fallbackLng:'en',// 兜底语言,万一翻译缺失就显示这个resources:{zh:{translation:{welcome:'欢迎回来,{{name}}!',items:'你有 {{count}} 条新消息'}},en:{translation:{welcome:'Welcome back, {{name}}!',items:'You have {{count}} new messages'}}},interpolation:{escapeValue:false,// React项目里要关掉,防止XSS但让React处理转义},// 复数处理配置,后面会细讲pluralSeparator:'_',nsSeparator:':'});// 使用方式 i18next.t('welcome',{name:'张三'});// "欢迎回来,张三!" i18next.t('items',{count:5});// "你有 5 条新消息"

看到那个{{name}}没?这就是插值,比字符串拼接优雅一万倍。而且i18next自带复数处理,英文里count为1和不为1时自动切换单复数,不用你写if-else。

vue-i18n:Vue亲儿子,丝滑得像德芙

如果你是Vue党,这个基本是标配。Composition API支持、TypeScript支持、甚至支持<i18n>单文件组件块,直接在.vue文件里写翻译。

<!-- Vue 3 + vue-i18n v9 完整示例 --> <template> <div> <!-- 基础使用 --> <h1>{{ $t('user.greeting', { name: user.name }) }}</h1> <!-- 带复数的 --> <p>{{ $tc('message.unread', unreadCount, { count: unreadCount }) }}</p> <!-- 日期格式化 --> <time>{{ $d(new Date(), 'short') }}</time> <!-- 数字格式化(价格) --> <span>{{ $n(price, 'currency') }}</span> <!-- 切换语言按钮 --> <button @click="toggleLocale"> {{ $t('action.switchLang') }} </button> </div> </template> <script setup> import { useI18n } from 'vue-i18n'; import { ref, watch } from 'vue'; // 组合式API用法 const { t, locale, setLocaleMessage, mergeLocaleMessage } = useI18n({ inheritLocale: true, // 继承全局配置 useScope: 'local' // 启用局部翻译,可以在<i18n>块定义 }); const user = ref({ name: '李四' }); const unreadCount = ref(5); const price = ref(2999.99); const toggleLocale = () => { // 切换语言,组件会自动重渲染 locale.value = locale.value === 'zh' ? 'en' : 'zh'; // 可以在这里做持久化,存localStorage或发请求改Cookie localStorage.setItem('user-lang', locale.value); }; // 动态加载语言包(懒加载) const loadMessages = async (lang) => { const messages = await import(`./locales/${lang}.json`); setLocaleMessage(lang, messages.default); locale.value = lang; }; // 监听语言变化,上报埋点或重新获取数据 watch(locale, (newVal) => { console.log(`语言切换至: ${newVal}`); // 这里可以触发事件总线或重新请求用户数据 }); </script> <i18n> { "zh": { "localKey": "这是组件级别的翻译,优先级高于全局" }, "en": { "localKey": "This is component-level translation" } } </i18n> <style scoped> .user-profile { padding: 20px; } .price { color: #e74c3c; font-weight: bold; } </style> 

但是! 升级到v9版本时那些坑,我踩得膝盖都青了。最大的 breaking change 是$tc(复数方法)被合并到t里了,旧项目迁移时满屏报错。还有getChoiceIndex这种复数规则自定义方式也变了,升级前一定要看迁移文档,别像我头铁直接改版本号。

react-i18next:React界的扛把子

React玩家基本都用这个,Hooks用起来确实香。useTranslation一调,翻译函数到手。

// React 18 + react-i18next 完整实战示例 import React, { Suspense, useCallback } from 'react'; import { useTranslation, Trans, initReactI18next } from 'react-i18next'; import i18n from 'i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import HttpBackend from 'i18next-http-backend'; // 初始化配置,包含所有高级特性 i18n .use(HttpBackend) // 从服务器加载翻译文件 .use(LanguageDetector) // 自动检测用户语言 .use(initReactI18next) .init({ fallbackLng: 'en', debug: process.env.NODE_ENV === 'development', // 命名空间配置,大型项目必备 ns: ['common', 'home', 'user', 'checkout'], defaultNS: 'common', // 后端加载配置 backend: { loadPath: '/locales/{{lng}}/{{ns}}.json', // 可以加查询参数防缓存 queryStringParams: { v: '1.0.0' } }, // 检测器配置 detection: { order: ['querystring', 'cookie', 'localStorage', 'navigator'], caches: ['cookie', 'localStorage'], lookupQuerystring: 'lng', lookupCookie: 'i18next', lookupLocalStorage: 'i18nextLng' }, // 插值配置 interpolation: { escapeValue: false, // React已经处理了XSS }, // React特定配置 react: { useSuspense: true, // 启用Suspense模式 bindI18n: 'languageChanged loaded', bindI18nStore: 'added removed', transEmptyNodeValue: '', // 空节点默认值 transSupportBasicHtmlNodes: true, // 支持基础HTML标签 transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'] } }); // 高阶组件:为类组件提供翻译能力(虽然推荐用Hooks,但老项目可能还有类组件) export function withTranslation(Component) { return function TranslatedComponent(props) { const { t, i18n } = useTranslation(); return <Component {...props} t={t} i18n={i18n} />; }; } // 自定义Hook:封装业务逻辑 export function useLocalizedValidation(schema) { const { t } = useTranslation('validation'); return useCallback((values) => { const errors = {}; if (!values.email) { errors.email = t('email.required'); } else if (!/^\S+@\S+\.\S+$/.test(values.email)) { errors.email = t('email.invalid'); } // 更多验证规则... return errors; }, [t]); } // 实际组件示例 function UserDashboard() { const { t, i18n } = useTranslation(['user', 'common']); const [user, setUser] = React.useState(null); // 带加载状态的翻译(处理动态命名空间) const { t: checkoutT, ready: checkoutReady } = useTranslation('checkout', { useSuspense: false // 手动处理加载状态 }); const changeLanguage = (lng) => { i18n.changeLanguage(lng); // 更新HTML lang属性,对SEO和屏幕阅读器很重要 document.documentElement.lang = lng; // RTL语言特殊处理 document.documentElement.dir = ['ar', 'he'].includes(lng) ? 'rtl' : 'ltr'; }; if (!checkoutReady) return <div>Loading translations...</div>; return ( <div className="dashboard"> <h1>{t('user:welcomeTitle', { name: user?.name || t('common:guest') })}</h1> {/* Trans组件处理带HTML的翻译 */} <p> <Trans i18nKey="user:agreementText" t={t}> 点击注册即表示您同意 <a href="/terms" onClick={(e) => { e.preventDefault(); /* 打开弹窗 */ }}> 服务条款 </a> 和 <a href="/privacy">隐私政策</a> </Trans> </p> {/* 复数处理 */} <div className="stats"> {t('user:notificationCount', { count: user?.unread || 0 })} </div> {/* 价格格式化 */} <div className="pricing"> {t('checkout:totalAmount', { amount: checkoutT('format.currency', { val: 199.99 }) })} </div> <div className="lang-switcher"> {['zh', 'en', 'ja', 'ar'].map((lng) => ( <button key={lng} onClick={() => changeLanguage(lng)} className={i18n.language === lng ? 'active' : ''} > {t(`common:languages.${lng}`)} </button> ))} </div> </div> ); } // 懒加载语言包示例(路由级别) const AdminPanel = React.lazy(() => import('./AdminPanel')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <UserDashboard /> </Suspense> ); } export default App; 

SSR水合报错怎么破? 这是Next.js玩家的痛。服务端渲染时语言检测和客户端不一致,导致HTML不匹配。解决方案是强制统一语言来源,比如都从Cookie读,或者SSR时禁用自动检测,直接用URL参数定死语言。

formatjs:Google出品,必属精品?

Google搞的,主打轻量和标准化。如果你追求极致性能,或者做移动端H5怕包体积太大,可以考虑。但它生态没i18next丰富,很多功能要自己造轮子。

到底怎么选?

别听专家瞎吹,看三点:

  1. 技术栈:Vue就用vue-i18n,React就用react-i18next,别瞎折腾
  2. 项目规模:小项目随便哪个都行,大项目选生态好的(i18next系)
  3. 团队能力:如果团队TS玩得6,选类型支持好的;如果都是老前端,选文档全的

撸起袖子干:从零搭建多语言架构的骚操作

选型完了,开始动手。这部分是干货中的干货,建议收藏。

文件目录怎么摆?强迫症患者的终极抉择

我见过两种主流方案,各有优劣:

方案A:按语言分

locales/ zh/ common.json home.json user.json en/ common.json home.json user.json 

方案B:按模块分

locales/ common/ zh.json en.json home/ zh.json en.json 

我推荐方案A,因为翻译人员通常按语言工作,给他们一个文件夹就完事。而且代码里动态加载时路径规则更统一:/locales/${lang}/${namespace}.json

JSON资源文件编写规范:key的命名艺术

别再写btn.submittitle.home这种烂大街的了,毫无语义,后期维护想死。推荐用层级命名空间

{"user":{"profile":{"title":"个人资料","editButton":"编辑资料","saveSuccess":"保存成功!","validation":{"nicknameRequired":"昵称不能为空","nicknameTooLong":"昵称不能超过20个字符"}},"security":{"passwordChange":{"title":"修改密码","oldPasswordPlaceholder":"请输入当前密码","newPasswordStrength":"密码强度:{{level}}"}}},"common":{"actions":{"confirm":"确认","cancel":"取消","backToHome":"返回首页"},"status":{"loading":"加载中...","error":"出错了,请重试","empty":"暂无数据"}}}

看到没?按业务模块 > 页面 > 功能 > 具体元素的层级来,找起来快,改起来不容易漏。

动态加载语言包:别让首屏慢得像蜗牛

千万别把所有语言打包进JS!用户只看一种语言,你塞十几种进去,首屏直接爆炸。

// Vue 3 动态加载示例import{ createI18n }from'vue-i18n';const i18n =createI18n({legacy:false,locale:'zh',// 默认先显示中文fallbackLocale:'en',messages:{}// 先空着,动态加载});// 加载语言包的函数asyncfunctionloadLocaleMessages(locale){// 已经加载过就直接返回if(i18n.global.availableLocales.includes(locale))return;try{// 动态import,webpack/vite会自动代码分割const messages =awaitimport(/* webpackChunkName: "locale-[request]" */`./locales/${locale}.json`); i18n.global.setLocaleMessage(locale, messages.default); i18n.global.locale.value = locale;// 设置HTML lang属性,SEO需要 document.querySelector('html').setAttribute('lang', locale);}catch(error){ console.error(`Failed to load locale ${locale}:`, error);// 失败时回退到默认语言 i18n.global.locale.value ='zh';}}// 初始化时根据用户偏好加载const savedLang = localStorage.getItem('user-lang')|| navigator.language.split('-')[0];loadLocaleMessages(savedLang);

检测用户语言的几种姿势

URL参数?lang=en,最SEO友好,搜索引擎能分别收录不同语言版本
Cookiei18next=zh,用户选择后记住偏好,下次访问直接生效
浏览器HeaderAccept-Language: zh-CN,zh;q=0.9,第一次访问时猜用户语言

推荐组合策略:优先URL参数(方便分享和SEO),其次Cookie(记住用户选择),最后浏览器Header(首次访问)。

// i18next检测器配置示例detection:{order:['querystring','cookie','localStorage','sessionStorage','navigator','htmlTag'],lookupQuerystring:'lng',lookupCookie:'i18next',lookupLocalStorage:'i18nextLng',lookupSessionStorage:'i18nextLng',// 缓存用户选择caches:['localStorage','cookie'],cookieMinutes:10080,// 一周cookieDomain:'myDomain.com',// 处理特殊逻辑convertDetectedLanguage:(lng)=>{// 把zh-CN、zh-TW、zh-HK统一映射为zh,除非你有特殊需求const lngMap ={'zh-CN':'zh','zh-TW':'zh','zh-HK':'zh','en-US':'en','en-GB':'en'};return lngMap[lng]|| lng;}}

切换语言时的"瞬移"问题

直接改语言会导致页面闪烁,用户体验极差。解决方案:

// 优雅切换:先加载新语言包,再切换,同时加过渡动画asyncfunctiongracefulLanguageSwitch(targetLang){const app = document.getElementById('app');// 1. 显示加载遮罩 app.style.opacity ='0.5'; app.style.pointerEvents ='none';try{// 2. 预加载新语言包awaitloadLocaleMessages(targetLang);// 3. 切换语言(Vue/React会自动重渲染) i18n.global.locale.value = targetLang; localStorage.setItem('user-lang', targetLang);// 4. 发送埋点trackEvent('language_change',{from: currentLang,to: targetLang });}catch(error){ toast.error('语言切换失败,请重试');}finally{// 5. 恢复交互setTimeout(()=>{ app.style.opacity ='1'; app.style.pointerEvents ='auto';},300);// 给渲染留点时间}}

那些让人头秃的深水区:复数、性别和日期格式化

以为翻译就是字符串替换?Too young!自然语言的复杂度能把你CPU干烧。

复数地狱:俄语和阿拉伯语教你做人

英语复数简单,就单数(one)和复数(other)。但俄语有三种:one(1)、few(2-4)、many(5+)。阿拉伯语有六种!zero、one、two、few、many、other。

// i18next复数配置示例(俄语){"ru":{"translation":{"key_one":"{{count}} книга","key_few":"{{count}} книги","key_many":"{{count}} книг","key_other":"{{count}} книг"}}}// 使用 i18next.t('key',{count:1});// "1 книга" i18next.t('key',{count:3});// "3 книги" i18next.t('key',{count:5});// "5 книг"

i18next内置了CLDR(Unicode Common Locale Data Repository)规则,自动根据语言选择复数形式。但你得提供完整的翻译key,别偷懒只写两个。

性别相关的文案处理

法语、德语、西班牙语里,形容词要跟着名词性别变。比如"欢迎来到我们的平台",如果用户资料里选了女性,法语要说"Bienvenue sur notre plateforme"(Bienvenue是女性形式),男性则是"Bienvenu"。

// 性别化处理方案{"fr":{"welcome":{"male":"Bienvenu, {{name}} !","female":"Bienvenue, {{name}} !","other":"Bienvenue, {{name}} !"}}}// 使用t('welcome.'+ user.gender,{name: user.name });

时间和数字的本地化陷阱

日期陷阱:美国的12/01/2024是12月1日,英国人的第一反应是1月12日。千万别自己拼接字符串!

// 错误示范(别这么干!)const dateStr =`${month}/${day}/${year}`;// 灾难之源// 正确姿势:用Intl API或库内置的格式化// i18next示例 i18next.t('key',{date:newDate(),formatParams:{date:{weekday:'long',year:'numeric',month:'long',day:'numeric'}}});// 或者直接用Intl(原生API,无需库)const formatter =newIntl.DateTimeFormat('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}); formatter.format(newDate());// "2024/01/15 14:30"

货币格式化:符号位置、小数位数、千分位分隔符,每个国家都不一样。

// 货币格式化示例const price =1234567.89;// 美国:$1,234,567.89newIntl.NumberFormat('en-US',{style:'currency',currency:'USD'}).format(price);// 德国:1.234.567,89 €(符号在后,逗号当小数点)newIntl.NumberFormat('de-DE',{style:'currency',currency:'EUR'}).format(price);// 日本:¥1,234,568(没有小数位)newIntl.NumberFormat('ja-JP',{style:'currency',currency:'JPY'}).format(price);// 阿拉伯:١٬٢٣٤٬٥٦٧٫٨٩ د.إ(用阿拉伯数字)newIntl.NumberFormat('ar-AE',{style:'currency',currency:'AED'}).format(price);

RTL(从右向左)布局:当代码突然"镜像"了

支持阿拉伯语、希伯来语时,整个世界都要反过来。不是简单的文字右对齐,是整个布局逻辑镜像

dir="rtl"属性的魔力

HTML5有dir属性,设为rtl后,浏览器会自动:

  • 文字从右向左排
  • margin-leftmargin-right自动对调(其实不是,这个要手动处理)
  • Flexbox和Grid的startend方向反转
<!-- 根元素设置dir --><htmllang="ar"dir="rtl"><body><!-- 内容会自动RTL,但CSS要配合 --></body></html>

CSS怎么写才不炸?

不要用物理方向(left/right),用逻辑方向(start/end)

/* 错误:硬编码左右 */.sidebar{float: left;margin-right: 20px;border-right: 2px solid #ccc;}/* 正确:使用逻辑属性 */.sidebar{float: inline-start;/* 根据dir自动选择左或右 */margin-inline-end: 20px;/* 逻辑上的"结束边距" */border-inline-end: 2px solid #ccc;}/* Flexbox和Grid天然支持逻辑方向 */.navbar{display: flex;justify-content: space-between;/* 这个不用改 */padding-inline: 20px;/* 水平方向内边距,自动适配RTL */}

PostCSS插件自动转换
如果你已有大量历史代码,手动改要改到明年。用postcss-rtlcss插件,自动把left转成rightmargin-left转成margin-right(在RTL模式下)。

// postcss.config.js module.exports ={plugins:[require('postcss-rtlcss')({// 基于CSS逻辑属性处理,生成RTL版本的CSSmode:'combined',// 在一个文件里同时支持LTR和RTLltrPrefix:'[dir="ltr"]',rtlPrefix:'[dir="rtl"]',// 忽略某些选择器(比如图标字体)ignorePrefixedRules:['.fa','.icon']})]};

图标和箭头的方向尴尬

返回箭头在RTL里应该指向右边吗?看情况

  • 如果是"返回上一页"的功能箭头,应该指向右(因为RTL的阅读流是从右到左,"回去"就是往右)
  • 如果是"下一步"的箭头,应该指向左(继续往阅读流方向走)
/* 自动翻转图标 */.icon-arrow{/* LTR默认朝右 */transform:rotate(0deg);transition: transform 0.3s;}[dir="rtl"] .icon-arrow{/* RTL时水平翻转 */transform:scaleX(-1);}/* 但有些图标不应该翻转,比如播放按钮(三角形) */.icon-play{/* 标记为不翻转 *//* 在postcss配置里忽略,或者单独写覆盖 */}

混合文本(BiDi)处理

当中文、英文和阿拉伯文混在一起时,浏览器可能搞不清楚文字方向,导致光标乱跳、选区错乱。用<bdi>标签或dir="auto"

<!-- 用户生成的内容,不知道是什么语言 --><divclass="comment"><bdi>用户昵称可能是阿拉伯语:محمد</bdi><span>说:</span><bdidir="auto">这里可能是混合内容:Hello مرحبا 你好</bdi></div>

踩坑实录:线上爆雷后的紧急抢救指南

都是血泪史,希望你用不上,但必须得知道。

语言包加载失败,页面全是key值

最社死的现场:用户打开页面,满屏home.titleuser.welcome,跟密码本似的。

兜底策略

// i18next配置兜底 i18next.init({fallbackLng:'en',// 缺失key时的处理parseMissingKeyHandler:(key)=>{// 上报到监控系统reportError(`Missing translation key: ${key} in ${i18next.language}`);// 返回友好提示,而不是显示keyreturn`[${key}]`;},// 或者返回默认语言的值missingKeyHandler:(lng, ns, key, fallbackValue)=>{// 尝试从默认语言找const defaultValue = i18next.getResource('en', ns, key);if(defaultValue)return defaultValue;return fallbackValue || key;}});

内存泄漏警告

动态注册的语言包没销毁,单页应用跑久了,内存里堆积了几十种语言包。

// Vue卸载时清理(虽然i18next有引用计数,但显式清理更保险)onUnmounted(()=>{// 移除当前组件添加的翻译(如果是动态添加的)if(loadedLocale && loadedLocale !=='zh'){ i18n.global.removeLocaleMessage(loadedLocale);}});

SEO优化:hreflang标签

告诉搜索引擎不同语言的版本,避免重复内容惩罚:

<!-- 在<head>里 --><linkrel="alternate"hreflang="zh"href="https://example.com/zh/home"/><linkrel="alternate"hreflang="en"href="https://example.com/en/home"/><linkrel="alternate"hreflang="x-default"href="https://example.com/"/>

缓存策略:强制刷新语言包

语言包更新了,用户浏览器还在用旧版本。解决方案:

// 1. 文件名加hash(推荐)// webpack/vite配置import zh from'./locales/zh.json?version=2';// 每次更新改版本号// 2. 或者加载时加查询参数backend:{loadPath:'/locales/{{lng}}.json?v={{version}}',queryStringParams:{version:'1.2.3'}// CI/CD时自动注入版本号}// 3. 提供手动刷新按钮(用户级)constrefreshTranslations=async()=>{// 清除i18next缓存 i18next.services.backendConnector.backend.clear();// 如果后端支持// 重新加载当前语言await i18next.reloadResources(i18next.language);// 触发重渲染 i18next.emit('languageChanged', i18next.language);};

提升幸福感的开发小技巧与自动化流程

自动扫描硬编码字符串

别让后端硬塞翻译文案,用脚本自动提取:

// extract-i18n-keys.jsconst fs =require('fs');const path =require('path');const glob =require('glob');// 匹配中文字符的正则(根据需求调整)const chineseRegex =/[\u4e00-\u9fa5]+/g;// 匹配模板字符串中的中文const templateRegex =/`([^`]*[\u4e00-\u9fa5][^`]*)`/g;const results =newMap();functionextractFromFile(filePath){const content = fs.readFileSync(filePath,'utf-8');const lines = content.split('\n'); lines.forEach((line, index)=>{// 跳过注释if(line.trim().startsWith('//')|| line.trim().startsWith('*'))return;const matches = line.match(chineseRegex)||[]; matches.forEach(match=>{const key = match;// 这里需要智能生成key,比如基于内容hash或路径if(!results.has(key)){ results.set(key,{key:`auto.${generateKey(match)}`,zh: match,files:[]});} results.get(key).files.push(`${filePath}:${index +1}`);});});}functiongenerateKey(str){// 简单的key生成策略:取前5个字的拼音或hashreturn str.slice(0,5).replace(/\s/g,'_');}// 扫描src目录 glob.sync('src/**/*.{js,vue,tsx}').forEach(extractFromFile);// 输出到JSON,供翻译人员使用const output ={}; results.forEach((value, key)=>{ output[value.key]={zh: value.zh,context:`出现在: ${value.files.join(', ')}`};}); fs.writeFileSync('extracted-keys.json',JSON.stringify(output,null,2)); console.log(`提取了 ${results.size} 个待翻译条目`);

对接翻译平台API

代码提交后自动触发翻译,第二天早上直接验收成果。主流平台(Lokalise、Crowdin、Transifex)都有API:

// 伪代码:CI/CD流程中自动同步// .github/workflows/sync-i18n.ymlconst axios =require('axios');asyncfunctionsyncTranslations(){// 1. 上传新key到翻译平台const newKeys =JSON.parse(fs.readFileSync('extracted-keys.json'));await axios.post('https://api.lokalise.com/api2/projects/{project_id}/keys',{keys: Object.entries(newKeys).map(([key, value])=>({key_name: key,platforms:['web'],translations:[{language_iso:'zh',translation: value.zh }]}))},{headers:{'X-Api-Token': process.env.LOKALISE_TOKEN}});// 2. 下载已完成的翻译const response =await axios.post(`https://api.lokalise.com/api2/projects/{project_id}/files/download`,{format:'json',original_filenames:true,directory_prefix:''});// 3. 解压并更新到项目awaitdownloadAndExtract(response.data.bundle_url,'./locales/');}// GitHub Actions配置示例(yaml)/* name: Sync Translations on: push: branches: [main] jobs: sync: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - run: node scripts/sync-i18n.js env: LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} */

VS Code插件推荐

i18n Ally:实时预览翻译效果,键值跳转,缺失key高亮。再也不用反复刷新页面看效果。

配置.vscode/settings.json

{"i18n-ally.localesPaths":["src/locales"],"i18n-ally.keystyle":"nested","i18n-ally.displayLanguage":"zh","i18n-ally.enabledFrameworks":["vue","react"]}

TypeScript类型安全

给翻译Key加上智能提示,手滑写错Key编译直接报错:

// i18n.d.tsimport'react-i18next';import zh from'./locales/zh.json';// 根据中文资源文件生成类型typeResources=typeof zh;declaremodule'react-i18next'{interfaceCustomTypeOptions{ resources: Resources;// 开启严格模式,不允许使用未定义的key strictKeyChecks:true;}}// 使用时有完整提示const{ t }=useTranslation();t('user.profile.title');// ✅ 有提示和类型检查t('user.profile.titel');// ❌ 编译报错:Key不存在

灰度发布多语言功能

先让内部员工或小部分用户试试水:

// 基于用户ID的灰度functionshouldEnableI18n(userId){// 内部员工全部开放if(isEmployee(userId))returntrue;// 10%的用户开放returnhashUserId(userId)%100<10;}// 或基于地区灰度functionshouldEnableI18nByRegion(ip){const region =getRegionFromIP(ip);// 先在香港和新加坡试点return['HK','SG'].includes(region);}

最后唠两句:为了不让翻译小姐姐顺着网线过来打你

保持Key的语义化:别用text1label2这种,到时候改个文案要改几十个文件,翻译人员也看不懂上下文。用user.profile.saveButton这种自描述的。

给翻译人员留点"活路":上下文备注一定要写清楚。比如"confirm": "确认",要备注这是"删除确认"还是"订单确认",不然翻译出来可能完全不对。

{"confirm":{"value":"确认","context":"用于删除操作前的二次确认弹窗,用户点击后数据将被永久删除"}}

记住,多语言不仅仅是文字替换,更是文化和习惯的尊重。颜色、图标、甚至图片都可能需要本地化。比如白色在西方代表纯洁,在东方某些场合代表丧事;竖起大拇指在有些国家是冒犯。别让老外觉得你的产品"怪怪的",不然会被笑话的,而且是真的会丢订单。

好了,差不多就这些。i18n这玩意儿,前期架构设计好,后期能省你80%的力气。别等到要支持第8种语言时才想起来重构,那时候你就只能像我一样,对着满屏的if-else和乱码,一边改一边骂娘了。

代码写完了,去泡杯咖啡吧,顺便想想怎么跟产品经理解释为什么阿拉伯语的上线时间要比预期多两周——因为RTL真的能把人逼疯。☕️

在这里插入图片描述

Read more

介绍一下 机器人坐标转换的 RT 矩阵

好的,我们来详细介绍一下机器人学和计算机视觉中至关重要的 RT矩阵,也常被称为刚体变换矩阵 或 位姿矩阵。 1. 什么是 RT 矩阵? RT矩阵 的核心作用是描述一个刚体(比如机器人的手臂、末端执行器、自动驾驶车辆上的传感器)在三维空间中的位置和姿态,即 位姿。 * R 代表 旋转:一个 3x3 的矩阵,描述了物体是如何相对于参考坐标系旋转的。 * T 代表 平移:一个 3x1 的向量,描述了物体相对于参考坐标系的位移。 为了将旋转和平移统一在一个矩阵运算中(便于连续变换和计算),我们使用一个 4x4 的齐次坐标变换矩阵,这就是我们通常所说的 RT 矩阵。 它的标准形式如下: text复制下载 [ r11 r12 r13 tx ] T = [ r21 r22 r23

By Ne0inhk
Rokid 手势识别技术深度解析:解锁 AR 无接触交互的核心秘密

Rokid 手势识别技术深度解析:解锁 AR 无接触交互的核心秘密

引言 在聊手势识别前,咱们先搞清楚:Rokid是谁?它为啥能把AR手势做得这么自然? Rokid是国内AR(增强现实)领域的“老兵”了,从2014年成立就盯着一个目标——让AR走进日常。你可能见过它的产品:能戴在脸上的“AR眼镜”Max Pro、能揣在兜里的“AR主机”Station 2、适合专业场景的“Station Pro”,这些设备不是用来“炫技”的,而是想让咱们摆脱手机、手柄的束缚,直接用手“摸”虚拟东西。 而手势识别,就是Rokid给AR设备装的“最自然的遥控器”——比如调大虚拟屏幕像捏橡皮一样捏合手指,翻页像翻书一样挥手。但不同设备、不同开发需求,需要搭配不同版本的SDK(软件开发工具包),这就像“不同型号的手机要装对应版本的APP”。 一、基础认知:先选对版本,避免开发走弯路 Rokid手势识别技术随SDK版本迭代持续优化,不同版本适配的Unity(开发工具)

By Ne0inhk
VSCode Github Copilot使用OpenAI兼容的自定义模型方法

VSCode Github Copilot使用OpenAI兼容的自定义模型方法

背景 VSCode 1.105.0发布了,但是用户最期待的Copilot功能却没更新!!! (Github Copilot Chat 中使用OpenAI兼容的自定义模型。) 🔥官方也关闭了Issue,并且做了回复,并表示未来也不会更新这个功能: “实际上,这个功能在可预见的未来只面向内部人员开放,作为一种“高级”实验功能。是否实现特定模型提供者的功能,我们交由扩展作者自行决定。仅限内部人员使用可以让我们快速推进,并提供一种可能并非始终百分之百完善,但能够持续改进并快速修复 bug 的体验。如果这个功能对你很重要,我建议切换到内部版本 insider。” 🤗 官方解决方案:安装VSCode扩展支持 你们完全不用担心只需要在 VS Code 中安装扩展:OAI Compatible Provider for Copilot 通过任何兼容 OpenAI 的提供商驱动的 GitHub Copilot Chat,使用前沿开源大模型,如 Kimi K2、DeepSeek

By Ne0inhk

Mac Mini M4 跑 AI 模型全攻略:从 Ollama 到 Stable Diffusion 的保姆级配置指南

Mac Mini M4 本地AI模型实战:从零构建你的个人智能工作站 最近身边不少朋友都在讨论,能不能用一台小巧的Mac Mini M4,搭建一个属于自己的AI开发环境。毕竟,不是每个人都有预算去租用云端的高性能GPU,也不是所有项目都适合把数据传到云端处理。我折腾了大概两周,从Ollama到Stable Diffusion,把整个流程走了一遍,发现M4芯片的潜力远超预期。这篇文章,就是把我踩过的坑、验证过的有效配置,以及一些提升效率的小技巧,毫无保留地分享给你。无论你是想本地运行大语言模型进行对话和创作,还是想离线生成高质量的AI图像,这篇指南都能帮你把Mac Mini M4变成一个得力的AI伙伴。 1. 环境准备与基础配置 在开始安装任何AI工具之前,确保你的系统环境是干净且高效的,这能避免后续无数莫名其妙的依赖冲突。Mac Mini M4出厂预装的是较新的macOS版本,但这还不够。 首先,打开“系统设置” -> “通用” -> “软件更新”,确保你的macOS已经更新到可用的最新版本。苹果对Metal图形API和神经网络引擎的优化通常会随着系统更新而提升,这对于后续运

By Ne0inhk