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 进销存软件可以服务于更广阔的国际市场。