Nuxt i18n 的 `$tm` 函数：环境差异问题与解决方案

h1Nuxt i18n 的 code$tm 函数：环境差异问题与解决方案（v10 适用）blockquotepstrong适用版本：code@nuxtjs/i18n v10.xbrem如果你使用其他版本，核心思路仍可参考，但具体行为可能略有差异。h2核心问题p在 Nuxt 4 + code@nuxtjs/i18n v10 项目中，使用 code$tm 或 codetm() 获取结构化翻译数据（如数组、对象）时，可能会遇到：ullistrong开发环境 (codenuxt dev)：一切正常，模板渲染正确。listrong生产环境 (codenuxt build 后运行)：报错 codeCannot read properties of undefined (reading 'source')，或页面渲染异常。h2问题根源p问题的根本原因在于 strongi18n 模块在开发环境和生产环境下对语言文件的编译处理方式不同：ullistrong开发环境：为了支持热更新和源码映射，模块会保留语言文件中的原始结构信息，字符串值被包装为 code{ loc: { source: "实际文本" } } 的形式。listrong生产环境：为了减小体积和提升性能，模块会移除这些包装，直接输出纯字符串值。p这种差异导致在模板中访问数据时，如果写死了开发环境下的访问路径（例如 codeitem.name.loc.source），生产环境就会因找不到 codeloc 属性而报错。h2解决方案h3方案一：快速修补（适合临时修复）p在模板中根据环境动态选择访问路径：precodespanspan spanspan spanspan spanspan {{ isDev ? item.name?.loc?.source : item.name }} spanspan spanspan spanspan spanspan spanspan spanspanconstspan isDev span=span importspan.spanmetaspan.spanenvspan.spanDEV spanspanconstspan {span tm span}span =span useI18nspan() spanspan pstrong优点：直接了当，改动最小。brstrong缺点：每个使用 codetm 的地方都要写判断，代码冗余。hrh3方案二：封装统一适配函数（推荐）p创建一个组合式函数，自动处理环境差异，让模板代码保持简洁。precodespanspan// composables/useI18nSafe.ts spanspanimportspan {span useI18nspan }span fromspan 'spanvue-i18nspan' spanspan spanspan/** spanspan * 递归提取开发环境下的实际值（移除 loc.source 包装） spanspan */ spanspanfunctionspan extractValuespan(spanvaluespan:span anyspan):span anyspan { spanspan ifspan (span!spanvaluespan ||span typeofspan valuespan !==span 'spanobjectspan'span) spanreturnspan value spanspan spanspan // 处理被包装的字符串（开发环境特有） spanspan ifspan (spanvaluespan.spanlocspan?.spansourcespan !==span undefinedspan) span{ spanspan returnspan valuespan.spanlocspan.spansource spanspan } spanspan spanspan // 处理数组 spanspan ifspan (spanArrayspan.spanisArrayspan(spanvaluespan)) span{ spanspan returnspan valuespan.spanmapspan(spanextractValuespan) spanspan } spanspan spanspan // 处理对象 spanspan constspan resultspan:span Recordspanspan =span {} spanspan forspan (spanconstspan keyspan inspan valuespan) span{ spanspan resultspan[spankeyspan] span=span extractValuespan(spanvaluespan[spankeyspan]) spanspan } spanspan returnspan result spanspan} spanspan spanspanexportspan functionspan useI18nSafespan()span { spanspan constspan {span tmspan:span originalTmspan,span ...spanrestspan }span =span useI18nspan() spanspan spanspan constspan tmspan =span (spankeyspan:span stringspan)span =>span { spanspan constspan valuespan =span originalTmspan(spankeyspan) spanspan // 仅开发环境需要提取，生产环境直接返回 spanspan ifspan (spanimportspan.spanmetaspan.spanenvspan.spanDEVspan) span{ spanspan returnspan extractValuespan(spanvaluespan) spanspan } spanspan returnspan value spanspan } spanspan spanspan returnspan {span tmspan,span ...spanrestspan } spanspan} p在组件中使用：precodespanspan spanspanconstspan {span tm span}span =span useI18nSafespan() spanspan spanspan spanspan spanspan spanspan span{{ item.name }}span spanspan spanspan pstrong优点：模板代码与生产环境完全一致，无环境感知，维护简单。brstrong缺点：需要额外封装，但一次投入长期受益。hrh2注意事项ollistrong仅字符串字段受影响br 数字、布尔值、数组等类型不会被包装，因此无需特殊处理。listrong递归提取br 方案二中的 codeextractValue 会递归遍历所有层级，可处理深层嵌套对象。listrong性能br 开发环境下会有微小递归开销，不影响生产环境。listrong该问题在 v10 中依然存在br 不要误以为 v10 已修复。只要 i18n 模块为了开发体验保留 AST 信息，这种差异就可能存在。h2总结pcode$tm 环境差异是 code@nuxtjs/i18n 模块为了兼顾开发体验和生产优化而产生的副作用。通过封装一个环境自适应的 codeuseI18nSafe 组合式函数，可以优雅地解决此问题，让代码在不同环境下都能稳定运行。p如果你在使用中遇到其他问题，欢迎留言交流。stylehtml pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}