Nuxt.js国际化和本地化

章节目标

通过本章节的学习,你将能够:

  • 理解国际化和本地化的基本概念
  • 掌握Nuxt.js中的多语言支持实现
  • 实现日期和时间的本地化格式化
  • 实现数字和货币的本地化格式化
  • 了解国际化最佳实践

1. 国际化和本地化的基本概念

1.1 什么是国际化和本地化?

  • 国际化(Internationalization,i18n):指设计和开发应用时,使其能够适应不同语言和地区的需求,而不需要对代码进行重大修改。
  • 本地化(Localization,l10n):指将国际化的应用适配到特定语言和地区的过程,包括翻译文本、调整日期格式、货币符号等。

1.2 国际化和本地化的重要性

  • 全球市场:帮助应用进入全球市场,覆盖更多用户
  • 用户体验:为用户提供母语界面,提升用户体验
  • 品牌形象:显示对不同文化的尊重,提升品牌形象
  • 法律合规:某些国家/地区要求提供本地化服务

2. Nuxt.js中的多语言支持

2.1 使用@nuxtjs/i18n模块

@nuxtjs/i18n是Nuxt.js官方推荐的国际化模块,提供了全面的国际化功能。

2.1.1 安装和配置

安装模块:

npm install @nuxtjs/i18n

配置nuxt.config.js:

export default {
  modules: [
    '@nuxtjs/i18n'
  ],
  i18n: {
    locales: [
      {
        code: 'zh',
        iso: 'zh-CN',
        name: '中文',
        file: 'zh.js'
      },
      {
        code: 'en',
        iso: 'en-US',
        name: 'English',
        file: 'en.js'
      }
    ],
    lazy: true,
    langDir: 'lang/',
    defaultLocale: 'zh',
    vueI18n: {
      fallbackLocale: 'zh'
    },
    detectBrowserLanguage: {
      useCookie: true,
      cookieKey: 'i18n_redirected',
      alwaysRedirect: false,
      fallbackLocale: 'zh'
    }
  }
}

2.1.2 创建语言文件

在项目根目录创建lang文件夹,并添加语言文件:

lang/zh.js:

export default {
  welcome: '欢迎使用Nuxt.js',
  about: '关于我们',
  contact: '联系我们',
  home: '首页',
  hello: '你好,{name}',
  count: '共 {count} 条记录',
  date: {
    format: 'YYYY年MM月DD日',
    today: '今天',
    yesterday: '昨天'
  }
}

lang/en.js:

export default {
  welcome: 'Welcome to Nuxt.js',
  about: 'About Us',
  contact: 'Contact Us',
  home: 'Home',
  hello: 'Hello, {name}',
  count: '{count} records in total',
  date: {
    format: 'MM/DD/YYYY',
    today: 'Today',
    yesterday: 'Yesterday'
  }
}

2.2 在组件中使用

2.2.1 基本用法

<template>
  <div>
    <h1>{{ $t('welcome') }}</h1>
    <p>{{ $t('hello', { name: 'World' }) }}</p>
    <p>{{ $t('count', { count: 100 }) }}</p>
    
    <div class="language-switcher">
      <button 
        v-for="locale in $i18n.locales" 
        :key="locale.code"
        @click="switchLanguage(locale.code)"
        :class="{ active: $i18n.locale === locale.code }"
      >
        {{ locale.name }}
      </button>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    switchLanguage(lang) {
      this.$i18n.locale = lang;
    }
  }
}
</script>

2.2.2 路由国际化

@nuxtjs/i18n模块支持路由国际化,可通过以下配置实现:

export default {
  i18n: {
    // 其他配置...
    parsePages: true,
    pages: {
      about: {
        zh: '/about',
        en: '/about-us'
      },
      contact: {
        zh: '/contact',
        en: '/contact-us'
      }
    }
  }
}

3. 日期和时间的本地化格式化

3.1 使用date-fns或dayjs

3.1.1 安装依赖

# 使用date-fns
npm install date-fns date-fns-tz date-fns/locale

# 或使用dayjs
npm install dayjs dayjs-timezone

3.1.2 创建日期格式化工具

// plugins/date-formatter.js
import Vue from 'vue';
import { format, formatDistanceToNow } from 'date-fns';
import { zhCN, enUS } from 'date-fns/locale';

export default (context, inject) => {
  const locales = {
    zh: zhCN,
    en: enUS
  };
  
  const formatDate = (date, formatStr, lang = context.app.i18n.locale) => {
    const locale = locales[lang] || zhCN;
    return format(date, formatStr, { locale });
  };
  
  const formatRelativeTime = (date, lang = context.app.i18n.locale) => {
    const locale = locales[lang] || zhCN;
    return formatDistanceToNow(date, { locale, addSuffix: true });
  };
  
  inject('formatDate', formatDate);
  inject('formatRelativeTime', formatRelativeTime);
  
  Vue.prototype.$formatDate = formatDate;
  Vue.prototype.$formatRelativeTime = formatRelativeTime;
};

3.1.3 在组件中使用

<template>
  <div>
    <p>{{ $formatDate(new Date(), 'yyyy年MM月dd日 EEEE') }}</p>
    <p>{{ $formatRelativeTime(new Date(Date.now() - 3600000)) }}</p>
  </div>
</template>

4. 数字和货币的本地化格式化

4.1 使用Intl.NumberFormat

Intl.NumberFormat是JavaScript内置的国际化API,可用于格式化数字和货币。

4.1.1 创建数字格式化工具

// plugins/number-formatter.js
import Vue from 'vue';

export default (context, inject) => {
  const formatNumber = (number, options = {}, lang = context.app.i18n.locale) => {
    return new Intl.NumberFormat(lang, options).format(number);
  };
  
  const formatCurrency = (amount, currency = 'CNY', lang = context.app.i18n.locale) => {
    return new Intl.NumberFormat(lang, {
      style: 'currency',
      currency,
      minimumFractionDigits: 2
    }).format(amount);
  };
  
  inject('formatNumber', formatNumber);
  inject('formatCurrency', formatCurrency);
  
  Vue.prototype.$formatNumber = formatNumber;
  Vue.prototype.$formatCurrency = formatCurrency;
};

4.1.2 在组件中使用

<template>
  <div>
    <p>{{ $formatNumber(123456.78, { maximumFractionDigits: 2 }) }}</p>
    <p>{{ $formatCurrency(123456.78, 'CNY') }}</p>
    <p>{{ $formatCurrency(123456.78, 'USD', 'en') }}</p>
  </div>
</template>

5. 国际化最佳实践

5.1 文本提取和管理

5.1.1 集中管理翻译文本

  • 将所有翻译文本集中到语言文件中,避免硬编码
  • 按功能模块组织语言文件,提高可维护性
  • 使用有意义的键名,便于理解和管理

5.1.2 自动化文本提取

使用i18n-extract工具自动提取代码中的翻译键:

npm install --save-dev i18n-extract

创建提取脚本:

// scripts/extract-i18n.js
const { extractFromFiles } = require('i18n-extract');
const fs = require('fs');
const path = require('path');

const files = [
  './pages/**/*.vue',
  './components/**/*.vue',
  './layouts/**/*.vue'
];

const extractedKeys = extractFromFiles(files, {
  marker: '$t',
  keySeparator: '.',
});

// 生成语言文件模板
const template = {};
extractedKeys.forEach(key => {
  let current = template;
  const parts = key.split('.');
  
  for (let i = 0; i < parts.length; i++) {
    const part = parts[i];
    if (!current[part]) {
      current[part] = i === parts.length - 1 ? '' : {};
    }
    current = current[part];
  }
});

// 写入文件
fs.writeFileSync(
  path.join(__dirname, '../lang/template.json'),
  JSON.stringify(template, null, 2)
);

console.log('Translation keys extracted successfully!');

5.2 处理复数形式

不同语言的复数规则可能不同,@nuxtjs/i18n支持复数形式:

// lang/zh.js
export default {
  apple: '苹果',
  apple_plural: '苹果'
}

// lang/en.js
export default {
  apple: 'apple',
  apple_plural: 'apples'
}

使用:

<template>
  <p>{{ $tc('apple', count) }}</p>
</template>

5.3 处理性别形式

某些语言需要根据性别调整文本:

// lang/en.js
export default {
  welcome: {
    male: 'Welcome, Mr. {name}',
    female: 'Welcome, Ms. {name}',
    other: 'Welcome, {name}'
  }
}

使用:

<template>
  <p>{{ $t(`welcome.${gender}`, { name: userName }) }}</p>
</template>

5.4 图片和资源的国际化

对于需要国际化的图片和资源,可以按语言组织:

static/
  images/
    zh/
      banner.png
    en/
      banner.png

使用:

<template>
  <img :src="`/images/${$i18n.locale}/banner.png`" alt="Banner">
</template>

6. 实用案例分析

6.1 案例:多语言网站

6.1.1 项目结构

nuxt-i18n-example/
├── lang/
│   ├── zh.js
│   └── en.js
├── pages/
│   ├── index.vue
│   ├── about.vue
│   └── contact.vue
├── components/
│   └── LanguageSwitcher.vue
├── nuxt.config.js
└── package.json

6.1.2 语言切换组件

<!-- components/LanguageSwitcher.vue -->
<template>
  <div class="language-switcher">
    <button 
      v-for="locale in locales" 
      :key="locale.code"
      @click="switchLanguage(locale.code)"
      :class="{ active: currentLocale === locale.code }"
      :aria-label="`Switch to ${locale.name}`"
    >
      {{ locale.name }}
    </button>
  </div>
</template>

<script>
export default {
  computed: {
    locales() {
      return this.$i18n.locales;
    },
    currentLocale() {
      return this.$i18n.locale;
    }
  },
  methods: {
    switchLanguage(lang) {
      this.$i18n.locale = lang;
    }
  }
}
</script>

6.1.3 页面组件

<!-- pages/index.vue -->
<template>
  <div class="home">
    <LanguageSwitcher />
    
    <h1>{{ $t('welcome') }}</h1>
    <p>{{ $t('home.description') }}</p>
    
    <div class="stats">
      <div class="stat-item">
        <h3>{{ $t('stats.users') }}</h3>
        <p>{{ $formatNumber(123456) }}</p>
      </div>
      <div class="stat-item">
        <h3>{{ $t('stats.revenue') }}</h3>
        <p>{{ $formatCurrency(9876543.21) }}</p>
      </div>
    </div>
    
    <div class="latest-news">
      <h2>{{ $t('news.latest') }}</h2>
      <div v-for="news in newsList" :key="news.id" class="news-item">
        <h3>{{ news.title }}</h3>
        <p>{{ $formatDate(news.date, 'yyyy-MM-dd') }}</p>
        <p>{{ news.excerpt }}</p>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      newsList: [
        {
          id: 1,
          title: this.$t('news.item1.title'),
          excerpt: this.$t('news.item1.excerpt'),
          date: new Date()
        },
        {
          id: 2,
          title: this.$t('news.item2.title'),
          excerpt: this.$t('news.item2.excerpt'),
          date: new Date(Date.now() - 86400000)
        }
      ]
    };
  }
};
</script>

6.2 案例:本地化表单验证

<template>
  <form @submit.prevent="submitForm" class="contact-form">
    <h2>{{ $t('contact.form.title') }}</h2>
    
    <div class="form-group">
      <label :for="'name-' + $i18n.locale">{{ $t('contact.form.name') }}</label>
      <input 
        :id="'name-' + $i18n.locale"
        v-model="form.name"
        :placeholder="$t('contact.form.namePlaceholder')"
        @blur="validateField('name')"
      />
      <span v-if="errors.name" class="error">{{ errors.name }}</span>
    </div>
    
    <div class="form-group">
      <label :for="'email-' + $i18n.locale">{{ $t('contact.form.email') }}</label>
      <input 
        :id="'email-' + $i18n.locale"
        type="email"
        v-model="form.email"
        :placeholder="$t('contact.form.emailPlaceholder')"
        @blur="validateField('email')"
      />
      <span v-if="errors.email" class="error">{{ errors.email }}</span>
    </div>
    
    <div class="form-group">
      <label :for="'message-' + $i18n.locale">{{ $t('contact.form.message') }}</label>
      <textarea 
        :id="'message-' + $i18n.locale"
        v-model="form.message"
        :placeholder="$t('contact.form.messagePlaceholder')"
        rows="4"
        @blur="validateField('message')"
      ></textarea>
      <span v-if="errors.message" class="error">{{ errors.message }}</span>
    </div>
    
    <button type="submit" :disabled="isSubmitting">{{ $t('contact.form.submit') }}</button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      form: {
        name: '',
        email: '',
        message: ''
      },
      errors: {},
      isSubmitting: false
    };
  },
  methods: {
    validateField(field) {
      this.errors[field] = '';
      
      if (!this.form[field]) {
        this.errors[field] = this.$t(`contact.form.errors.${field}.required`);
        return;
      }
      
      if (field === 'email') {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailRegex.test(this.form[email])) {
          this.errors[field] = this.$t('contact.form.errors.email.invalid');
        }
      }
      
      if (field === 'message' && this.form.message.length < 10) {
        this.errors[field] = this.$t('contact.form.errors.message.minLength');
      }
    },
    submitForm() {
      // 验证所有字段
      Object.keys(this.form).forEach(field => {
        this.validateField(field);
      });
      
      // 检查是否有错误
      if (Object.values(this.errors).some(error => error)) {
        return;
      }
      
      this.isSubmitting = true;
      
      // 模拟表单提交
      setTimeout(() => {
        alert(this.$t('contact.form.success'));
        this.form = {
          name: '',
          email: '',
          message: ''
        };
        this.isSubmitting = false;
      }, 1000);
    }
  }
};
</script>

7. 总结回顾

通过本章节的学习,你已经了解了:

  • 国际化和本地化的基本概念及重要性
  • 如何使用@nuxtjs/i18n模块实现多语言支持
  • 如何实现日期和时间的本地化格式化
  • 如何实现数字和货币的本地化格式化
  • 国际化最佳实践,包括文本提取、复数形式处理、性别形式处理等

国际化和本地化是构建全球应用的重要组成部分。通过本章节介绍的技术和方法,你可以创建能够适应不同语言和地区需求的Nuxt.js应用,为全球用户提供更好的用户体验。

8. 扩展阅读

9. 课后练习

  1. 创建一个支持中英文切换的Nuxt.js项目
  2. 实现日期和时间的本地化格式化
  3. 实现数字和货币的本地化格式化
  4. 处理复数形式和性别形式
  5. 为项目添加语言切换功能并测试
« 上一篇 Nuxt.js可访问性设计 下一篇 » Nuxt.js多环境配置