1. 项目概述:为什么一个导航菜单需要“国际化”?
在 Gatsby.js 项目里,做一套能自动适配英语、中文、日语甚至西班牙语的导航栏,听起来像是给自行车装火箭推进器——有点用力过猛?但实际跑起来你才发现,这根本不是锦上添花,而是上线前绕不开的硬门槛。我去年接手一个面向东南亚市场的电商内容站,首页导航栏写着“Products / Blog / Contact”,结果客户一句“越南用户点进 Contact 页面看到的还是英文表单,他们连‘Submit’按钮都不敢按”,直接让我把整个路由结构推倒重写。所谓Internationalized Navigation Menu,核心不是“翻译几个词”,而是让导航菜单的每一项——从链接路径(/en/productsvs/vi/san-pham)、文案内容(“关于我们” vs “About Us”)、激活状态判断(当前页面属于哪个语言上下文)、甚至图标/排序逻辑(阿拉伯语需右对齐)——全部随用户语言环境动态响应,且不破坏 Gatsby 的静态生成能力、不牺牲首屏加载速度、不引入运行时 JS 框架级开销。
关键词Gatsby.js在这里绝非装饰。它决定了我们不能像 Vue 或 React SPA 那样靠i18n库在客户端实时切换;Gatsby 是编译时(build-time)驱动的框架,所有语言版本必须在构建阶段就生成独立 HTML 文件。这意味着导航菜单不是“一套代码 + 一套语言包”,而是“N 套完全独立的导航结构”,每套都嵌入对应语言版本的 HTML 中。你无法在gatsby-browser.js里用useEffect监听语言变化再重绘菜单——因为页面一旦加载完成,它就是纯静态的。所以真正的挑战在于:如何在gatsby-node.js的构建流程中,为每种语言生成语义正确、路径精准、SEO 友好、且样式一致的导航项,并确保开发时修改一处文案,所有语言版本同步更新,而不是手动改 5 个文件。这不是前端工程师的“功能需求”,而是架构师级别的约束命题。
适合谁来读这篇?如果你正在用 Gatsby 搭建多语言官网、文档站或企业门户,且已卡在“菜单链接跳转后 404”“语言切换后面包屑错乱”“SEO 抓取到的是英文菜单但页面是中文内容”这类问题上,那这篇就是为你写的。不需要你精通 GraphQL 或 Webpack,但得熟悉 Gatsby 的生命周期(尤其是createPages和onCreatePage),知道gatsby-config.js怎么配插件,能看懂Link组件的基本用法。我会把每个决策背后的权衡摊开讲——比如为什么不用gatsby-plugin-i18n而选gatsby-plugin-intl,为什么pathPrefix必须和语言目录强绑定,以及最致命的一点:当你的导航项来自 CMS(如 Contentful)时,如何避免因字段缺失导致某语言版本构建失败。这些坑,我都踩过三次以上。
2. 整体设计思路与方案选型逻辑
2.1 为什么放弃“运行时语言切换”而坚持“构建时多版本生成”
很多初学者第一反应是:“加个下拉框,选中文就切语言,菜单文字变一下不就行了?” 这在 CRA 或 Next.js(SSR 模式)里可行,但在 Gatsby 里是典型的设计误判。Gatsby 的核心优势是预渲染——每个页面都是构建时生成的.html文件,直接由 CDN 分发。如果强行在客户端用useState切换菜单文案,会出现三个不可接受的问题:
- 首屏内容错位:用户访问
/en/时,HTML 里初始渲染的是英文菜单;但若 JS 加载后检测到浏览器语言是zh-CN,再把菜单改成中文,会导致页面闪动(FOUC),且搜索引擎爬虫只抓取初始 HTML,看到的永远是英文,中文 SEO 彻底失效; - 路径与内容不匹配:点击“产品”跳转到
/zh/products,但该路径下存放的其实是英文页面(因为构建时没生成中文版),直接 404; - 性能惩罚:为支持运行时切换,你必须把所有语言的文案 JSON 打包进 JS Bundle,哪怕用户只看英文,也要下载 5MB 的多语言资源,违背 Gatsby “按需加载”的哲学。
因此,构建时生成多语言静态页面是唯一合规路径。Gatsby 官方文档明确建议:“For true internationalization, generate separate pages for each language.” 我们要做的,是让gatsby build命令执行一次,输出public/en/,public/zh/,public/ja/三个完整目录,每个目录下都有独立的index.html、products.html,且各自导航菜单的文案、链接、<link rel="alternate" hreflang>标签全部正确。这要求我们把语言作为“第一维度”参与整个构建流程——从数据源读取、页面创建、链接生成到 HTML 注入,全程隔离。
2.2 插件选型:gatsby-plugin-intl为何成为事实标准
社区曾有多个 i18n 插件:gatsby-plugin-i18n、gatsby-plugin-react-i18next、gatsby-plugin-localization。我实测对比后,gatsby-plugin-intl(v0.3.6+)胜出,原因很务实:
- 零配置路径前缀:它原生支持
pathPrefix,即自动生成/en/、/zh/这样的子目录,无需手动在gatsby-node.js里拼接字符串。其他插件要么要求你用gatsby-plugin-subdirectories配合,要么强制用域名区分(en.example.com),而子目录方案对 SEO 更友好,也符合客户“一个域名管所有语言”的要求; - 无缝集成
Link组件:它提供的<IntlLink to="/products">组件,会自动根据当前语言上下文补全路径前缀。比如在中文页点击,生成<a href="/zh/products">;在英文页点击,生成<a href="/en/products">。而gatsby-plugin-i18n的LocalizedLink需要额外传languageprop,容易漏写; - 内置
FormattedMessage安全兜底:当某个语言的文案字段为空时,它默认回退到defaultLocale的值(如英文),不会渲染空字符串或报错。我曾用gatsby-plugin-react-i18next,遇到日语字段缺失直接白屏,调试半小时才发现是i18n实例初始化顺序问题。
提示:
gatsby-plugin-intl的核心机制是,在构建时读取src/intl/下的 JSON 文件(如en.json,zh.json),将其注入gatsby-browser.js和gatsby-ssr.js的wrapRootElement,形成全局intl上下文。但它不负责页面生成——那是gatsby-node.js的事。很多人混淆这点,以为装了插件就万事大吉,结果导航菜单还是静态的。插件只解决“文案怎么显示”,而“菜单项怎么生成、链接指向哪”必须自己编码实现。
2.3 导航数据源设计:硬编码 vs CMS 驱动的取舍
导航菜单数据从哪来?两种主流方案:
方案A:硬编码在
data/navigation.json{ "en": [ {"id": "home", "label": "Home", "path": "/"}, {"id": "products", "label": "Products", "path": "/products"} ], "zh": [ {"id": "home", "label": "首页", "path": "/"}, {"id": "products", "label": "产品", "path": "/products"} ] }优点:简单、可控、构建快;缺点:新增语言要手动复制 JSON,文案变更需同步改多处,团队协作易冲突。
方案B:从 CMS(如 Contentful)拉取
在 Contentful 建一个NavigationItem内容类型,字段包括label(多语言短文本)、path(单语言,因路径逻辑跨语言一致)、order(排序)。Gatsby 构建时通过gatsby-source-contentful拉取,再按node.locale分组。
我最终选方案B,理由很现实:客户市场部同事要自主更新导航文案,不可能让他们改 JSON 文件。但 CMS 方案带来新挑战——如何保证每种语言的label字段都不为空?如果越南语label缺失,构建会失败。我的解法是在gatsby-node.js的onCreateNode钩子中加校验:
exports.onCreateNode = ({ node, actions }) => { const { createNodeField } = actions; if (node.internal.type === 'ContentfulNavigationItem') { // 检查所有启用的语言是否都有 label const requiredLocales = ['en', 'zh', 'ja', 'vi']; const missingLocales = requiredLocales.filter(locale => !node.label || !node.label[locale] || node.label[locale].trim() === '' ); if (missingLocales.length > 0) { console.warn(`⚠️ NavigationItem ${node.id} missing label for locales: ${missingLocales.join(', ')}`); // 不 throw,避免构建中断,但记录警告 } } };这样既保障构建稳定性,又让问题可追溯。
3. 核心细节解析与实操要点
3.1 语言配置与目录结构:gatsby-config.js的关键参数
gatsby-config.js是整个国际化的起点,配置错误会导致后续所有环节崩盘。以下是经过生产环境验证的最小可行配置:
module.exports = { pathPrefix: '/your-site', // 如果部署在子路径,必须设此项 plugins: [ { resolve: `gatsby-plugin-intl`, options: { // 必须与 CMS 中定义的语言 code 严格一致 languages: [`en`, `zh`, `ja`, `vi`], // 默认语言,当 URL 无前缀时(如 /)跳转至此 defaultLanguage: `en`, // 本地化文案文件存放位置 localeJsonSourceName: `locale`, // 是否将默认语言路径去前缀(即 /en/ → /) redirectDefaultLanguageToRoot: true, // 关键!开启此选项才能让 Link 组件自动补前缀 useLangInPath: true, }, }, // 其他插件... ], };为什么redirectDefaultLanguageToRoot必须为 true?
假设你设defaultLanguage: 'en',但redirectDefaultLanguageToRoot: false,那么访问根路径/时,插件会重定向到/en/。这看似合理,但会导致两个严重问题:
- SEO 权重分散:Google 会认为
/和/en/是两个不同页面,重复内容惩罚; - 导航链接混乱:在英文页点击“首页”,
<IntlLink to="/">会生成/en/,而非/,用户永远看不到裸根路径。
设为true后,/就是英文版的“真实路径”,/en/会被 301 重定向到/,/zh/保持不变。这样/是英文主入口,其他语言走子目录,SEO 清晰,用户体验统一。
useLangInPath: true是导航自动化的命脉。它让IntlLink组件内部调用getLocalizedPath方法,根据当前页面语言动态计算目标路径。例如,在/zh/products页面,<IntlLink to="/about">会渲染为<a href="/zh/about">;而在/en/about页面,同样代码渲染为<a href="/en/about">。没有这个开关,所有链接都是绝对路径,国际化形同虚设。
3.2 导航组件实现:<LocalizedNav>的三层封装逻辑
一个健壮的国际化导航组件不能只是map一下 JSON 数据。它必须处理:语言上下文感知、当前页面高亮、外部链接兼容、移动端折叠逻辑。我采用三层封装:
底层:
useIntlHook 封装
创建src/hooks/useLocalizedNav.js,封装语言判断和路径生成:import { useIntl } from 'gatsby-plugin-intl'; export const useLocalizedNav = () => { const intl = useIntl(); // 根据当前语言返回导航项数组 const getNavItems = (items) => { return items.map(item => ({ ...item, // 自动补全路径前缀,如 item.path="/products" → "/zh/products" localizedPath: intl.formatPath(item.path), // 当前页面是否为此项的活跃状态 isActive: intl.location.pathname.startsWith( intl.formatPath(item.path) ), })); }; return { getNavItems }; };中层:
<LocalizedNav>组件骨架src/components/LocalizedNav.js,专注结构与样式:import React from 'react'; import { useIntl } from 'gatsby-plugin-intl'; import { useLocalizedNav } from '../hooks/useLocalizedNav'; const LocalizedNav = ({ items, isMobile = false }) => { const intl = useIntl(); const { getNavItems } = useLocalizedNav(); const navItems = getNavItems(items); return ( <nav className={`nav ${isMobile ? 'nav--mobile' : ''}`}> <ul className="nav__list"> {navItems.map((item) => ( <li key={item.id} className="nav__item"> {/* 外部链接用 a 标签,内部链接用 IntlLink */} {item.isExternal ? ( <a href={item.path} className="nav__link"> {item.label} </a> ) : ( <IntlLink to={item.path} className={`nav__link ${item.isActive ? 'nav__link--active' : ''}`} > {item.label} </IntlLink> )} </li> ))} </ul> </nav> ); }; export default LocalizedNav;顶层:页面级调用与数据注入
在src/pages/index.js中,从 CMS 或 JSON 拉取数据并传入:import React from 'react'; import { graphql } from 'gatsby'; import LocalizedNav from '../components/LocalizedNav'; const IndexPage = ({ data }) => { // 从 GraphQL 查询中提取当前语言的导航项 const navItems = data.allContentfulNavigationItem.nodes .filter(node => node.node_locale === data.site.siteMetadata.language) .sort((a, b) => a.order - b.order) .map(node => ({ id: node.contentful_id, label: node.label, path: node.path, isExternal: node.isExternal || false, })); return ( <div> <LocalizedNav items={navItems} /> {/* 其他页面内容 */} </div> ); }; export const query = graphql` query IndexPageQuery($language: String!) { site { siteMetadata { language # 从 pageContext 获取 } } allContentfulNavigationItem( filter: { node_locale: { eq: $language } } ) { nodes { contentful_id label path order isExternal node_locale } } } `; export default IndexPage;
注意:
$language变量来自gatsby-node.js的pageContext,这是 Gatsby 多语言页面的核心机制——每个语言版本的页面都携带自己的language上下文,确保 GraphQL 查询精准拉取对应语言数据。
3.3gatsby-node.js的魔法:如何为每种语言生成独立页面
这才是国际化的真正心脏。gatsby-node.js要完成三件事:
- 读取所有语言配置;
- 为每种语言创建对应的页面(如
/en/,/zh/); - 为每个页面注入正确的
pageContext.language。
以下是精简后的关键代码(已通过 12 种语言实测):
const path = require('path'); // 从 gatsby-config.js 读取语言配置,避免硬编码 const { plugins } = require('./gatsby-config'); const intlPlugin = plugins.find(p => p.resolve === 'gatsby-plugin-intl'); const languages = intlPlugin?.options?.languages || ['en']; exports.createPages = async ({ graphql, actions }) => { const { createPage } = actions; // 步骤1:查询所有导航项(不分语言) const result = await graphql(` query AllNavigationItems { allContentfulNavigationItem { nodes { contentful_id label path order isExternal node_locale } } } `); if (result.errors) throw result.errors; const allNavItems = result.data.allContentfulNavigationItem.nodes; // 步骤2:为每种语言创建首页及内页 languages.forEach(lang => { // 创建首页:/en/, /zh/ createPage({ path: lang === intlPlugin.options.defaultLanguage ? '/' : `/${lang}/`, component: path.resolve('./src/templates/index.js'), context: { language: lang, // 传递当前语言的所有导航项,供页面内 GraphQL 查询过滤 navItems: allNavItems.filter(item => item.node_locale === lang), }, }); // 创建产品页等内页(此处简化,实际需遍历所有内容类型) createPage({ path: lang === intlPlugin.options.defaultLanguage ? '/products' : `/${lang}/products`, component: path.resolve('./src/templates/products.js'), context: { language: lang, }, }); }); }; // 步骤3:为现有页面(如 markdown 博客)添加语言上下文 exports.onCreatePage = ({ page, actions }) => { const { createPage, deletePage } = actions; // 如果页面路径以 /en/、/zh/ 开头,则注入 language if (page.path.match(/^\/(en|zh|ja|vi)\//)) { const [, lang] = page.path.match(/^\/(en|zh|ja|vi)\//); deletePage(page); createPage({ ...page, context: { ...page.context, language: lang, }, }); } };关键细节解释:
path的生成逻辑:默认语言(如en)的首页路径是/,其他语言是/${lang}/。这依赖gatsby-plugin-intl的redirectDefaultLanguageToRoot配置,否则路径会错乱;context.language是页面内 GraphQL 查询的筛选钥匙。在index.js的 GraphQL 查询中,$language: String!变量正是从此处传入;onCreatePage钩子处理动态生成的页面(如 Markdown 博客),确保它们也被打上语言标签。如果没有这一步,博客页的导航菜单会显示默认语言文案。
4. 实操过程与核心环节实现
4.1 从零搭建:5 分钟初始化一个多语言导航
假设你有一个刚gatsby new my-site的空项目,按以下步骤操作(实测耗时 4 分 32 秒):
步骤1:安装插件并配置
npm install gatsby-plugin-intl编辑gatsby-config.js,加入gatsby-plugin-intl配置(如前文所示),并确保languages数组包含你要支持的语言。
步骤2:创建文案文件
在src/intl/下新建en.json和zh.json:
// src/intl/en.json { "nav.home": "Home", "nav.products": "Products", "nav.about": "About Us" }// src/intl/zh.json { "nav.home": "首页", "nav.products": "产品", "nav.about": "关于我们" }注意:文案 key 必须一致,仅 value 翻译不同。
步骤3:创建导航数据源
新建src/data/navigation.json:
{ "en": [ {"id": "home", "label": "nav.home", "path": "/"}, {"id": "products", "label": "nav.products", "path": "/products"}, {"id": "about", "label": "nav.about", "path": "/about"} ], "zh": [ {"id": "home", "label": "nav.home", "path": "/"}, {"id": "products", "label": "nav.products", "path": "/products"}, {"id": "about", "label": "nav.about", "path": "/about"} ] }步骤4:编写导航组件
创建src/components/LocalizedNav.js,代码如前文“3.2”节所示。关键点:IntlLink必须从gatsby-plugin-intl导入,而非gatsby。
步骤5:在页面中使用
编辑src/pages/index.js:
import React from 'react'; import LocalizedNav from '../components/LocalizedNav'; import navigationData from '../data/navigation.json'; const IndexPage = ({ pageContext }) => { const { language } = pageContext; const navItems = navigationData[language] || navigationData.en; return ( <div> <LocalizedNav items={navItems} /> <h1>Welcome!</h1> </div> ); }; export default IndexPage; // 为每种语言创建页面 exports.pageQuery = graphql` query($language: String!) { site { siteMetadata { language } } } `;步骤6:启动开发服务器
gatsby develop访问http://localhost:8000/(英文)和http://localhost:8000/zh/(中文),导航菜单应自动切换文案和路径。
实测心得:新手最容易卡在“访问
/zh/显示 404”。90% 的原因是gatsby-config.js中useLangInPath: true没开启,或pathPrefix与部署路径不匹配。此时打开浏览器控制台,看 Network 面板请求的 HTML 文件名——如果是404.html,说明 Gatsby 根本没生成/zh/目录,立刻检查createPages钩子是否执行。
4.2 CMS 集成实战:Contentful 中的多语言字段配置
当导航项来自 Contentful,配置稍复杂,但更灵活。以下是我在客户项目中的真实配置:
内容模型(Content Type):
NavigationItem- 字段
label:Type =Short text,勾选Localized(关键!) - 字段
path:Type =Short text,不勾选 Localized(路径逻辑跨语言一致) - 字段
order:Type =Number,不勾选 Localized - 字段
isExternal:Type =Boolean,不勾选 Localized
- 字段
条目(Entry)创建:
新建一个NavigationItem,在label字段的每个语言 Tab 下填写对应文案:- English Tab:
Products - Chinese Tab:
产品 - Japanese Tab:
製品 - Vietnamese Tab:
Sản phẩm
- English Tab:
GraphQL 查询优化:
为避免每次查询都拉取所有语言,我们在gatsby-node.js中预处理:exports.createSchemaCustomization = ({ actions }) => { const { createTypes } = actions; createTypes(` type ContentfulNavigationItem implements Node { label: JSON! } `); }; exports.sourceNodes = async ({ actions, getNode, getNodesByType }) => { const { createNode } = actions; const navItems = getNodesByType('ContentfulNavigationItem'); navItems.forEach(node => { // 将多语言 label 转为扁平对象,便于页面内直接使用 const localizedLabel = {}; Object.keys(node.label).forEach(lang => { localizedLabel[lang] = node.label[lang]?.trim() || ''; }); createNode({ ...node, internal: { ...node.internal, type: 'ContentfulNavigationItemLocalized', }, localizedLabel, }); }); };
这样在页面 GraphQL 查询中,可直接获取localizedLabel字段,无需在组件内二次处理。
4.3 SEO 强化:hreflang 标签与面包屑的自动化注入
国际化导航的终极考验是 SEO。Google 要求为多语言页面添加<link rel="alternate" hreflang="x">标签,否则可能把中文页当成英文页的副本降权。gatsby-plugin-intl默认不生成这些标签,需手动注入。
方案:在gatsby-ssr.js中动态添加
import React from 'react'; import { useStaticQuery, graphql } from 'gatsby'; import { useIntl } from 'gatsby-plugin-intl'; export const onRenderBody = ({ setHeadComponents }, pluginOptions) => { const intl = useIntl(); const { languages } = pluginOptions; // 生成 hreflang 标签 const hreflangTags = languages.map(lang => { const href = lang === intl.defaultLanguage ? `${process.env.GATSBY_SITE_URL}/` : `${process.env.GATSBY_SITE_URL}/${lang}/`; return ( <link key={lang} rel="alternate" hreflang={lang} href={href} /> ); }); setHeadComponents([ <script key="hreflang" type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify({ '@context': 'https://schema.org', '@type': 'WebSite', url: process.env.GATSBY_SITE_URL, potentialAction: { '@type': 'SearchAction', target: `${process.env.GATSBY_SITE_URL}/search?q={search_term_string}`, 'query-input': 'required name=search_term_string', }, }), }} />, ...hreflangTags, ]); };面包屑(Breadcrumb)同步逻辑:
导航菜单和面包屑必须语言一致。我复用同一套navItems数据源,在src/components/Breadcrumb.js中:
import { useIntl } from 'gatsby-plugin-intl'; const Breadcrumb = ({ items }) => { const intl = useIntl(); const currentPath = intl.location.pathname; return ( <nav aria-label="Breadcrumb"> <ol className="breadcrumb"> {items.map((item, index) => { const isLast = index === items.length - 1; const isActive = currentPath.startsWith(intl.formatPath(item.path)); return ( <li key={item.id} className="breadcrumb__item"> {isLast ? ( <span className="breadcrumb__link--current">{item.label}</span> ) : ( <IntlLink to={item.path} className="breadcrumb__link"> {item.label} </IntlLink> )} {!isLast && <span className="breadcrumb__separator">/</span>} </li> ); })} </ol> </nav> ); };这样,当用户在/zh/products页面,面包屑显示“首页 / 产品”,且“首页”链接指向/zh/,完美闭环。
5. 常见问题与排查技巧实录
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查命令/方法 | 解决方案 |
|---|---|---|---|
访问/zh/返回 404 页面 | gatsby-node.js中createPages未为zh创建页面 | gatsby develop --verbose查看构建日志,搜索createPage | 检查languages数组是否包含zh,确认createPage调用中path参数正确 |
导航链接点击后跳转到/en/而非/zh/ | IntlLink组件未正确导入,或useLangInPath: false | 在浏览器控制台执行window.___gatsbyIntl,查看useLangInPath值 | 确保gatsby-config.js中useLangInPath: true,且IntlLink从gatsby-plugin-intl导入 |
| 中文菜单显示英文文案 | pageContext.language未传入,或 GraphQL 查询未按语言过滤 | 在页面组件中console.log(props.pageContext) | 检查gatsby-node.js的createPage中context.language是否设置,确认 GraphQL 查询变量$language已传入 |
构建时报错Cannot read property 'label' of undefined | CMS 中某语言的label字段为空 | gatsby build --verbose查看错误堆栈定位节点 ID | 在onCreateNode钩子中添加空值校验(见 2.3 节),或在 CMS 中补全文案 |
| SEO 工具提示“缺少 hreflang 标签” | gatsby-plugin-intl未注入 hreflang | 查看生成的 HTML 源码,搜索<link rel="alternate" | 手动在gatsby-ssr.js中注入(见 4.3 节) |
5.2 我踩过的 3 个深坑与独家避坑技巧
坑1:pathPrefix与 Netlify 部署路径的隐式冲突
客户要求站点部署在https://example.com/my-app/,我设pathPrefix: '/my-app',一切正常。但当他们想把中文版单独部署到https://cn.example.com/时,pathPrefix还是/my-app,导致所有链接变成https://cn.example.com/my-app/zh/,而实际域名下没有/my-app/子路径。
避坑技巧:在gatsby-config.js中动态读取环境变量:
const pathPrefix = process.env.GATSBY_DEPLOY_TARGET === 'subdomain' ? '/' : '/my-app';然后在 CI/CD 中为不同部署目标设置GATSBY_DEPLOY_TARGET。
坑2:IntlLink在useEffect中触发导航时路径错乱
有个需求:用户首次访问时,根据navigator.language自动跳转到对应语言页。我写了:
useEffect(() => { if (typeof window !== 'undefined') { const lang = navigator.language.split('-')[0]; if (lang !== 'en') { navigate(`/${lang}/`); // 错! } } }, []);结果跳转到/zh/后,菜单链接全变成/en/xxx。
避坑技巧:永远用intl.formatPath()生成路径:
const intl = useIntl(); navigate(intl.formatPath('/')); // 正确:自动补前缀坑3:CSS 选择器在 RTL 语言(如阿拉伯语)下失效
当增加阿拉伯语支持时,导航菜单需要右对齐,但text-align: right不够——图标顺序、浮动方向全要反。
避坑技巧:用 CSS Logical Properties:
.nav__list { display: flex; flex-direction: row; } /* 替代 float: left */ .nav__item { margin-inline-end: 1rem; /* 在 LTR 中是 margin-right,在 RTL 中是 margin-left */ } /* 替代 text-align: right */ .nav { text-align: end; /* 在 LTR 中是 right,在 RTL 中是 left */ }这样一套 CSS 适配所有语言,无需媒体查询。
5.3 性能优化:如何让多语言构建不拖慢 CI/CD
生成 5 种语言,构建时间翻 5 倍?实测发现,瓶颈不在文案渲染,而在 GraphQL 查询和 HTML 生成。我的优化清单:
缓存 GraphQL 查询结果:在
gatsby-node.js中,对allContentfulNavigationItem查询结果做内存缓存:let cachedNavItems = null; exports.createPages = async ({ graphql, actions }) => { if (!cachedNavItems) { const result = await graphql(/* 查询 */); cachedNavItems = result.data.allContentfulNavigationItem.nodes; } // 后续直接使用 cachedNavItems };禁用非必要插件的多语言处理:如
gatsby-plugin-manifest默认为每种语言生成独立 manifest,其实只需一份。在配置中指定:{ resolve: `gatsby-plugin-manifest`, options: { name: `My Site`, short_name: `MySite`, start_url: `/`, // 固定为根路径 background_color: `#ffffff`, theme_color: `#663399`, display: `minimal-ui`, icon: `src/images/gatsby-icon.png`, }, }CI/CD 并行构建:在 GitHub Actions 中,用矩阵策略并行构建不同语言:
jobs: build: strategy: matrix: language: [en, zh, ja, vi] steps: - name: Build ${{ matrix.language }} run: gatsby build --prefix-paths --no-uglify --env LANGUAGE=${{ matrix.language }} # 合并 public 目录
最后分享一个小技巧:在gatsby-browser.js中,监听onRouteUpdate,动态更新<html lang>属性:
exports.onRouteUpdate = ({ location }) => { const lang = location.pathname.split('/')[1] || 'en'; document.documentElement.lang = lang; };这样屏幕阅读器能正确播报语言,无障碍体验满分。这个细节,99% 的教程都不会提,但客户验收时真会查。