PSI进销存软件

PSI - 专业进销存管理软件

PSI进销存软件多语言国际化实现

国际化概述

进销存软件面向不同地区和语言的用户,需要实现完善的多语言支持。本文介绍 PSI 进销存软件中多语言国际化的完整实现方案,包括界面文本翻译、货币格式、日期格式、数值格式等本地化适配。

国际化架构设计

模块 功能 实现方式
界面翻译 菜单、按钮、提示信息 JSON 语言包
数字格式 小数点、千分位、精度 Intl.NumberFormat
货币格式 货币符号、位置、小数位 Intl.NumberFormat
日期格式 年月日顺序、时分秒 Intl.DateTimeFormat
排序规则 字母排序、比较规则 Intl.Collator

语言包管理

完整的国际化语言包实现:

// 多语言服务
class I18nService {
  constructor() {
    this.currentLocale = 'zh-CN';
    this.fallbackLocale = 'zh-CN';
    this.translations = {};
    this.loadedLocales = new Set();
  }

  // 初始化加载语言包
  async init(locales = ['zh-CN', 'en-US', 'ja-JP']) {
    for (const locale of locales) {
      await this.loadLocale(locale);
    }

    // 检测浏览器语言
    const browserLocale = this.detectBrowserLocale();
    if (this.translations[browserLocale]) {
      this.currentLocale = browserLocale;
    }
  }

  // 加载语言包
  async loadLocale(locale) {
    try {
      const translations = await this.loadTranslations(locale);
      this.translations[locale] = translations;
      this.loadedLocales.add(locale);
    } catch (error) {
      console.error(`Failed to load locale ${locale}:`, error);
    }
  }

  // 获取翻译文本
  t(key, params = {}, locale = null) {
    const currentLocale = locale || this.currentLocale;

    // 尝试当前语言
    let text = this.getTranslation(currentLocale, key);

    // 回退到默认语言
    if (text === key && currentLocale !== this.fallbackLocale) {
      text = this.getTranslation(this.fallbackLocale, key);
    }

    // 参数替换
    if (Object.keys(params).length > 0) {
      text = this.replaceParams(text, params);
    }

    return text;
  }

  // 获取翻译(支持嵌套 key)
  getTranslation(locale, key) {
    const translations = this.translations[locale];
    if (!translations) return key;

    const keys = key.split('.');
    let value = translations;

    for (const k of keys) {
      if (value && typeof value === 'object' && k in value) {
        value = value[k];
      } else {
        return key;
      }
    }

    return typeof value === 'string' ? value : key;
  }

  // 参数替换
  replaceParams(text, params) {
    return text.replace(/\{\{(\w+)\}\}/g, (match, key) => {
      return params[key] !== undefined ? params[key] : match;
    });
  }

  // 检测浏览器语言
  detectBrowserLocale() {
    const browserLang = navigator.language || navigator.userLanguage;
    const supportedLocales = Array.from(this.loadedLocales);

    // 精确匹配
    if (supportedLocales.includes(browserLang)) {
      return browserLang;
    }

    // 语言匹配(如 zh-TW -> zh-CN)
    const baseLang = browserLang.split('-')[0];
    const matched = supportedLocales.find(l => l.startsWith(baseLang));

    return matched || this.fallbackLocale;
  }

  // 切换语言
  setLocale(locale) {
    if (!this.translations[locale]) {
      console.warn(`Locale ${locale} not loaded`);
      return false;
    }
    this.currentLocale = locale;
    localStorage.setItem('psi-locale', locale);
    return true;
  }
}

// 语言包结构示例
const zhCN = {
  // 菜单
  menu: {
    dashboard: '仪表盘',
    purchase: '采购管理',
    sales: '销售管理',
    inventory: '库存管理',
    finance: '财务管理',
    system: '系统设置'
  },

  // 按钮
  button: {
    save: '保存',
    cancel: '取消',
    confirm: '确认',
    delete: '删除',
    edit: '编辑',
    add: '新增',
    search: '查询',
    export: '导出',
    import: '导入',
    submit: '提交',
    reset: '重置'
  },

  // 表单
  form: {
    required: '此字段为必填项',
    invalidEmail: '请输入有效的邮箱地址',
    minLength: '最少输入 {{min}} 个字符',
    maxLength: '最多输入 {{max}} 个字符',
    range: '数值必须在 {{min}} 到 {{max}} 之间'
  },

  // 业务
  business: {
    order: {
      create: '创建订单',
      modify: '修改订单',
      cancel: '取消订单',
      status: {
        pending: '待处理',
        processing: '处理中',
        completed: '已完成',
        cancelled: '已取消'
      }
    },
    inventory: {
      inStock: '库存',
      availableStock: '可用库存',
      reservedStock: '预留库存',
      warning: '库存预警',
      shortage: '库存不足'
    }
  },

  // 消息
  message: {
    saveSuccess: '保存成功',
    saveFailed: '保存失败',
    deleteSuccess: '删除成功',
    deleteConfirm: '确定要删除吗?',
    operationSuccess: '操作成功',
    operationFailed: '操作失败'
  }
};

const enUS = {
  menu: {
    dashboard: 'Dashboard',
    purchase: 'Purchase',
    sales: 'Sales',
    inventory: 'Inventory',
    finance: 'Finance',
    system: 'Settings'
  },
  button: {
    save: 'Save',
    cancel: 'Cancel',
    confirm: 'Confirm',
    delete: 'Delete',
    edit: 'Edit',
    add: 'Add',
    search: 'Search',
    export: 'Export',
    import: 'Import',
    submit: 'Submit',
    reset: 'Reset'
  },
  form: {
    required: 'This field is required',
    invalidEmail: 'Please enter a valid email address',
    minLength: 'Minimum {{min}} characters required',
    maxLength: 'Maximum {{max}} characters allowed',
    range: 'Value must be between {{min}} and {{max}}'
  },
  business: {
    order: {
      create: 'Create Order',
      modify: 'Modify Order',
      cancel: 'Cancel Order',
      status: {
        pending: 'Pending',
        processing: 'Processing',
        completed: 'Completed',
        cancelled: 'Cancelled'
      }
    },
    inventory: {
      inStock: 'In Stock',
      availableStock: 'Available Stock',
      reservedStock: 'Reserved Stock',
      warning: 'Stock Warning',
      shortage: 'Out of Stock'
    }
  },
  message: {
    saveSuccess: 'Saved successfully',
    saveFailed: 'Save failed',
    deleteSuccess: 'Deleted successfully',
    deleteConfirm: 'Are you sure you want to delete?',
    operationSuccess: 'Operation successful',
    operationFailed: 'Operation failed'
  }
};

本地化格式化

货币、日期、数值等本地化格式处理:

// 本地化格式化服务
class LocaleFormatter {
  constructor(locale = 'zh-CN') {
    this.locale = locale;
    this.formatters = {};
  }

  // 货币格式化
  formatCurrency(amount, currency = 'CNY') {
    const formatter = new Intl.NumberFormat(this.locale, {
      style: 'currency',
      currency: currency,
      minimumFractionDigits: this.getCurrencyDecimal(currency),
      maximumFractionDigits: this.getCurrencyDecimal(currency)
    });

    return formatter.format(amount);
  }

  // 获取货币小数位数
  getCurrencyDecimal(currency) {
    const decimalMap = {
      'CNY': 2,  // 人民币:2位
      'USD': 2,  // 美元:2位
      'EUR': 2,  // 欧元:2位
      'JPY': 0,  // 日元:0位
      'KRW': 0,  // 韩元:0位
      'GBP': 2   // 英镑:2位
    };
    return decimalMap[currency] || 2;
  }

  // 数字格式化
  formatNumber(value, options = {}) {
    const {
      minimumFractionDigits = 2,
      maximumFractionDigits = 2,
      useGrouping = true
    } = options;

    const formatter = new Intl.NumberFormat(this.locale, {
      minimumFractionDigits,
      maximumFractionDigits,
      useGrouping
    });

    return formatter.format(value);
  }

  // 百分比格式化
  formatPercent(value, decimal = 2) {
    const formatter = new Intl.NumberFormat(this.locale, {
      style: 'percent',
      minimumFractionDigits: decimal,
      maximumFractionDigits: decimal
    });

    return formatter.format(value);
  }

  // 日期格式化
  formatDate(date, format = 'short') {
    const dateObj = new Date(date);

    const formatOptions = {
      short: { year: 'numeric', month: '2-digit', day: '2-digit' },
      medium: { year: 'numeric', month: 'short', day: 'numeric' },
      long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' },
      full: {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        weekday: 'long',
        hour: 'numeric',
        minute: 'numeric',
        second: 'numeric'
      }
    };

    const formatter = new Intl.DateTimeFormat(this.locale, formatOptions[format] || formatOptions.short);
    return formatter.format(dateObj);
  }

  // 时间格式化
  formatTime(date, options = {}) {
    const { hour12 = false, ...rest } = options;
    const dateObj = new Date(date);

    const formatter = new Intl.DateTimeFormat(this.locale, {
      hour: 'numeric',
      minute: 'numeric',
      second: 'numeric',
      hour12,
      ...rest
    });

    return formatter.format(dateObj);
  }

  // 相对时间格式化
  formatRelativeTime(date) {
    const now = new Date();
    const target = new Date(date);
    const diff = now - target;

    const rtf = new Intl.RelativeTimeFormat(this.locale, { numeric: 'auto' });

    const seconds = Math.floor(diff / 1000);
    const minutes = Math.floor(seconds / 60);
    const hours = Math.floor(minutes / 60);
    const days = Math.floor(hours / 24);

    if (days > 0) return rtf.format(-days, 'day');
    if (hours > 0) return rtf.format(-hours, 'hour');
    if (minutes > 0) return rtf.format(-minutes, 'minute');
    return rtf.format(-seconds, 'second');
  }

  // 字符串排序(考虑 locale)
  sort(strings) {
    const collator = new Intl.Collator(this.locale, {
      sensitivity: 'base',
      ignorePunctuation: true
    });

    return strings.sort(collator.compare);
  }

  // 列表格式化(最后加连接词)
  formatList(items, style = 'conjunction') {
    const formatter = new Intl.ListFormat(this.locale, { style, type: style });
    return formatter.format(items);
  }
}

// 使用示例
const formatter = new LocaleFormatter('zh-CN');

// 货币
formatter.formatCurrency(1234.56);        // ¥1,234.56
formatter.formatCurrency(1234.56, 'USD'); // $1,234.56
formatter.formatCurrency(1234.56, 'JPY'); // ¥1,235

// 数字
formatter.formatNumber(1234567.89);       // 1,234,567.89

// 日期
formatter.formatDate('2025-06-16');       // 2025/06/16
formatter.formatDate('2025-06-16', 'long'); // 2025年6月16日 星期一

// 列表
formatter.formatList(['苹果', '香蕉', '橙子']); // 苹果、香蕉和橙子

多货币支持

进销存系统需要支持多币种:

// 多货币服务
class MultiCurrencyService {
  constructor(exchangeRateApi) {
    this.exchangeRateApi = exchangeRateApi;
    this.baseCurrency = 'CNY';
    this.rates = {};
    this.lastUpdate = null;
  }

  // 更新汇率
  async updateRates() {
    try {
      const rates = await this.exchangeRateApi.getRates(this.baseCurrency);
      this.rates = rates;
      this.lastUpdate = new Date();

      // 缓存到本地存储
      localStorage.setItem('exchange-rates', JSON.stringify({
        base: this.baseCurrency,
        rates: this.rates,
        updated: this.lastUpdate.toISOString()
      }));

      return this.rates;
    } catch (error) {
      // 使用缓存的汇率
      const cached = this.loadCachedRates();
      if (cached) {
        this.rates = cached.rates;
        return this.rates;
      }
      throw error;
    }
  }

  // 加载缓存的汇率
  loadCachedRates() {
    const cached = localStorage.getItem('exchange-rates');
    if (!cached) return null;

    const data = JSON.parse(cached);
    const updated = new Date(data.updated);
    const now = new Date();

    // 超过24小时不使用缓存
    if ((now - updated) / (1000 * 60 * 60) > 24) {
      return null;
    }

    return data;
  }

  // 货币转换
  convert(amount, fromCurrency, toCurrency) {
    if (fromCurrency === toCurrency) return amount;

    // 转换为基准货币
    let inBase = amount;
    if (fromCurrency !== this.baseCurrency) {
      inBase = amount / (this.rates[fromCurrency] || 1);
    }

    // 从基准货币转换到目标货币
    if (toCurrency === this.baseCurrency) {
      return inBase;
    }

    return inBase * (this.rates[toCurrency] || 1);
  }

  // 格式化金额(带货币符号)
  formatAmount(amount, currency, locale) {
    const formatter = new LocaleFormatter(locale);
    return formatter.formatCurrency(amount, currency);
  }

  // 获取支持的货币列表
  getSupportedCurrencies() {
    return [
      { code: 'CNY', name: '人民币', symbol: '¥', locale: 'zh-CN' },
      { code: 'USD', name: '美元', symbol: '$', locale: 'en-US' },
      { code: 'EUR', name: '欧元', symbol: '€', locale: 'de-DE' },
      { code: 'GBP', name: '英镑', symbol: '£', locale: 'en-GB' },
      { code: 'JPY', name: '日元', symbol: '¥', locale: 'ja-JP' },
      { code: 'HKD', name: '港币', symbol: 'HK$', locale: 'zh-HK' },
      { code: 'TWD', name: '新台币', symbol: 'NT$', locale: 'zh-TW' }
    ];
  }
}

总结

多语言国际化是进销存软件走向国际市场的基础,核心价值包括:

通过完善的国际化实现,PSI 进销存软件可以服务于更广阔的国际市场。

← 下一篇:PSI进销存会员进阶功能设计与实现