描述在 grunt 中实现模板国际化注入

解读

“模板国际化注入”指的是:在构建阶段,把多语言资源(通常是 JSON 或 YAML 文件)注入到 HTML/JS 模板中,让页面在打包后就能直接带上对应语种的文案,而无需前端再发 Ajax 请求语言包。
国内项目普遍要求“构建即产出多语言包”,因此面试官想确认你是否:

  1. 熟悉 Grunt 的“多任务 + 文件对象格式”配置套路;
  2. 自定义 Grunt 任务组合社区插件完成“读语言包 → 按语种循环 → 渲染模板 → 输出到 dist/{lang}/**”的完整链路;
  3. 知道如何缓存语言包、避免重复 IO以及与后续压缩、CDN 上传任务保持顺序

知识点

  1. grunt.file.readJSON / grunt.file.readYAML:同步读取语言包,避免回调地狱。
  2. grunt.template.process:使用 Lodash 模板语法,把 <%= key %> 替换成对应文案。
  3. grunt.registerMultiTask:创建“i18n”多任务,一次注册即可循环所有语种。
  4. this.files.forEach + grunt.file.write:手动控制目标目录结构,保证 dist/zh、dist/en 等并行产出。
  5. grunt.task.run([‘clean’, ‘i18n’, ‘usemin’, ‘cdn’]):利用任务队列保证国际化注入在压缩、加 hash 之前完成。
  6. locale 回退策略:若当前语种缺失某 key,需回退到 zh-CN 或 en-US,防止页面出现空白。
  7. 性能细节
    • grunt.file.preserveBOM: false 避免 UTF-8 BOM 导致模板解析失败;
    • 大项目语言包 2000+ key 时,用 grunt.log.writeln 打印进度,防止 CI 日志超时。

答案

  1. 目录约定
    locales/
    ├── zh-CN.json
    ├── en-US.json
    src/
    └── index.html (含 <%= i18n.welcome %> 占位符)

  2. 安装依赖
    npm i -D grunt grunt-contrib-clean grunt-contrib-copy

  3. 注册自定义多任务(Gruntfile.js 节选)
    module.exports = function(grunt) {
    grunt.initConfig({
    clean: { i18n: ['dist'] },
    i18nInject: {
    options: {
    localeRoot: 'locales',
    fallback: 'zh-CN'
    },
    files: [{
    expand: true,
    cwd: 'src',
    src: '**/*.html',
    dest: 'dist'
    }]
    }
    });

    grunt.registerMultiTask('i18nInject', function() {
    const path = require('path');
    const fallback = this.options().fallback;
    const localeRoot = this.options().localeRoot;
    const locales = grunt.file.expand(path.join(localeRoot, '*.json'))
    .map(f => ({
    code: path.basename(f, '.json'),
    dict: grunt.file.readJSON(f)
    }));

    this.files.forEach(fGroup => {  
      const tpl = grunt.file.read(fGroup.src[0]);  
      locales.forEach(({code, dict}) => {  
        const outFile = path.join(fGroup.dest, code, fGroup.src[0]);  
        const missing = {};  
        const i18n = new Proxy(dict, {  
          get(t, key) {  
            if (key in t) return t[key];  
            missing[key] = true;  
            return grunt.file.readJSON(path.join(localeRoot, fallback))[key] || key;  
          }  
        });  
        const html = grunt.template.process(tpl, { data: { i18n } });  
        grunt.file.write(outFile, html);  
        if (Object.keys(missing).length) {  
          grunt.log.warn(`[${code}] 缺失 key:${Object.keys(missing).join(', ')}`);  
        }  
      });  
    });  
    

    });

    grunt.registerTask('i18n', ['clean', 'i18nInject']);
    };

  4. 运行
    npx grunt i18n
    产出
    dist/zh-CN/index.html
    dist/en-US/index.html

  5. 与后续流程衔接
    grunt.registerTask('build', ['i18n', 'useminPrepare', 'concat', 'uglify', 'filerev', 'usemin']);
    保证注入 → 合并 → 压缩 → 加 hash 顺序正确,避免 hash 计算时文案还未写入。

拓展思考

  1. 动态语种:若运营随时新增语种,可让 i18nInject 任务在运行时扫描 locales/,无需改 Gruntfile;结合 grunt-contrib-watch,新增 JSON 后自动重跑 i18n。
  2. 与 React/Vue 同构:模板注入只解决首屏,客户端仍需加载语言包做二次替换;可在构建阶段同时产出 locale-manifest.json,告诉运行时当前语种已注入哪些 key,避免重复下载。
  3. 与 CI 量化:在 GitLab-CI 中把 grunt i18n --verbose 输出重定向到文件,用正则提取“缺失 key”数量,当缺失率 > 1% 时让流水线失败,强制翻译达标才能上线。