使用 grunt-i18n-static 生成中英双语静态页

解读

在国内前端面试中,**“多语言静态站点生成”常被用来考察候选人对构建工具链的掌控深度。Grunt 虽非最新,但存量项目庞大,“能否在老项目里低成本接入国际化”**是面试官的核心关切。题目表面问“怎么用 grunt-i18n-static”,实则暗含三条考核线:

  1. 对 Grunt 插件机制与配置的熟练度;
  2. 对国际化资源(语言包、模板、路径策略)的架构设计;
  3. 对国内部署场景(CDN 缓存、SEO、路由规范)的落地经验。
    答得太浅(仅贴配置)会被追问性能与缓存;答得太偏(扯到 webpack/vite)会被判跑题。必须紧扣 Grunt 生态,给出可维护、可上线、可回滚的完整方案。

知识点

  1. grunt-i18n-static 本质:基于 grunt-contrib-copy + 模板字符串替换,一次性产出多语言纯静态文件,非运行时切换,适合 SEO 与 CDN。
  2. 语言包格式:默认支持 .json.yml键值必须扁平化(如 nav.home: 首页),避免嵌套过深导致模板语法冗长。
  3. 模板语法:使用 {%= __key %} 占位符,与 Lodash template 完全兼容,因此可复用 grunt.template.process 的变量注入能力。
  4. 目录规范:国内主流约定 dist/zh-cn/dist/en/ 双目录并行,避免在文件名中加语言码index.en.html 会被百度降权)。
  5. 缓存策略:静态资源文件名带 <%= hash %>但 HTML 入口文件禁止缓存(Nginx location ~* \.(html)$ { add_header Cache-Control no-store; })。
  6. 回滚方案:Grunt 阶段生成 dist/.manifest-i18n.json,记录 语言→文件→hash 映射,上线脚本对比旧 manifest,差异文件做增量上传,降低 CDN 刷新成本。
  7. 与 grunt-contrib-watch 联动:监听 locales/*.jsonsrc/templates/**/*触发 livereload 时只重编当前语言,避免全量构建拖慢开发体验。
  8. 合规细节:中文内容必须经过 grunt-text-replace 做敏感词过滤,生成后走一遍 grunt-htmllint 保证无外链死链,否则无法通过国内内容安全扫描。

答案

  1. 安装依赖
npm i -D grunt-i18n-static grunt-contrib-clean grunt-contrib-copy grunt-filerev grunt-usemin
  1. 语言包准备
    locales/zh-cn.json
{ "title": "欢迎来到官网", "btn": "立即体验" }

locales/en.json

{ "title": "Welcome to Official", "btn": "Try Now" }
  1. Gruntfile.js 核心配置
module.exports = function(grunt) {
  grunt.initConfig({
    clean: { dist: 'dist' },

    // 1. 按语言维度循环构建
    i18n_static: {
      options: {
        locales: ['zh-cn', 'en'],
        localeDir: 'locales',
        templateDir: 'src/templates',
        // 关键:输出路径带语言目录
        dest: function(locale) { return 'dist/' + locale; },
        // 替换引擎用 Lodash
        process: function(content, locale) {
          var dict = grunt.file.readJSON('locales/' + locale + '.json');
          return grunt.template.process(content, { data: dict });
        }
      },
      pages: {
        expand: true,
        cwd: 'src/templates',
        src: '**/*.html',
        dest: '<%= i18n_static.options.dest %>'
      }
    },

    // 2. 静态资源加 hash
    filerev: {
      options: { algorithm: 'md5', length: 8 },
      dist: { src: ['dist/**/*.{js,css,png,jpg}'] }
    },

    // 3. 替换 HTML 中的资源引用
    usemin: {
      html: 'dist/**/*.html',
      options: {
        assetsDirs: ['dist/zh-cn', 'dist/en']
      }
    },

    // 4. 生成上线清单
    manifest: {
      generate: {
        options: {
          basePath: 'dist',
          process: function(path) {
            return path.replace(/^dist\//, '');
          }
        },
        src: ['dist/**/*'],
        dest: 'dist/.manifest-i18n.json'
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-clean');
  grunt.loadNpmTasks('grunt-i18n-static');
  grunt.loadNpmTasks('grunt-filerev');
  grunt.loadNpmTasks('grunt-usemin');
  grunt.registerTask('build', ['clean', 'i18n_static', 'filerev', 'usemin', 'manifest']);
};
  1. 模板示例
    src/templates/index.html
<!doctype html>
<html lang="{%= lang %}">
<head>
  <meta charset="utf-8">
  <title>{%= title %}</title>
  <!-- build:css css/index.min.css -->
  <link rel="stylesheet" href="css/index.css">
  <!-- endbuild -->
</head>
<body>
  <h1>{%= title %}</h1>
  <button>{%= btn %}</button>
</body>
</html>
  1. 上线脚本(CI 片段)
# 仅上传变更文件
aws s3 sync dist/ s3://my-cdn/ --exclude "*" --include "$(node scripts/diff-manifest.js)"
# 刷新 CDN 目录
aliyun cdn RefreshObjectCaches --ObjectPath "http://cdn.xxx.com/zh-cn/" --ObjectType "Directory"
aliyun cdn RefreshObjectCaches --ObjectPath "http://cdn.xxx.com/en/" --ObjectType "Directory"

拓展思考

  1. 增量构建提速:当语言包多达 10+ 时,全量构建耗时 30s+,可改写 grunt-i18n-staticmultiTask利用 grunt-newer 只重编变动的语言目录,把耗时压到 5s 内。
  2. SSR 降级方案:若未来迁移到 Next.js,需保留原 Grunt 生成的静态页做长尾 SEO 兜底,通过 Nginx map $http_user_agent $is_bot 把爬虫流量打到静态目录,实现 SSR 与静态页并存,避免流量切换导致的排名震荡。
  3. 敏感词热更新:运营随时可能调整敏感词库,把 grunt-text-replace 的词典抽成独立 npm 私包,CI 阶段拉取最新版,无需改业务代码即可重新部署,满足国内监管“即发即审”要求。