1862 lines
61 KiB
Plaintext
1862 lines
61 KiB
Plaintext
import { BasePluralRule, Tmui4xI18nTml, DateTimeFormatReal, I18nOptions, I18nOptionsReally, NumberFormat, NumberFormatOpts, StringOrNull, NumberOrNull, StringOrNumber, StringOrNumberOrNull, DateOrNumberOrString, UTSJSONObjectOrNull, UTSJSONObjectOrArray, AnyOrNull, GetAnyType, I18nOptionsOrNull, NumberFormatOrNull, DateTimeFormatOrNull, GetAnyTypeOrNull } from "../interface";
|
||
|
||
/**
|
||
* uniapp-x多语言插件
|
||
* @author tmui4x
|
||
* @copyright https://tmui.design
|
||
* @date 2025/7/8
|
||
*/
|
||
|
||
// 日期时间名称格式类型
|
||
type DateTimeNames = {
|
||
long : string[],
|
||
short : string[],
|
||
narrow : string[]
|
||
}
|
||
type BestTimeTYpe = { unit : string, value : number }
|
||
type timeUnitsTYpe = {unit:string,ms:number,threshold:number}
|
||
export function mergeI18nOpts(args : I18nOptions|null = null) : I18nOptionsReally {
|
||
let localLang = uni.getStorageSync("language")
|
||
// 提供默认配置
|
||
const currentLan = localLang!=null&&localLang!=''&& typeof localLang == 'string'?(localLang! as string):"zh-Hans"
|
||
const defaultOptions : I18nOptionsReally = {
|
||
locale: currentLan,
|
||
fallbackLocale: "en",
|
||
messages: {},
|
||
datetimeFormats: new Map(),
|
||
numberFormats: new Map(),
|
||
modifiers: new Map(),
|
||
pluralRules: new Map(),
|
||
missing: null,
|
||
missingWarn: true,
|
||
fallbackWarn: true,
|
||
fallbackRoot: true,
|
||
fallbackFormat: false,
|
||
unresolving: true,
|
||
postTranslation: null,
|
||
warnHtmlMessage: true,
|
||
escapeParameter: true,
|
||
inheritLocale: true,
|
||
warnHandler: (v, e) => {
|
||
console.warn(`[tmui4x]:${v}`, e == null ? '' : e!);
|
||
},
|
||
pluralRule: (choice : number, choicesLength : number, orgRule ?: BasePluralRule) : number => {
|
||
// 默认英语复数规则实现
|
||
// 参考 Vue I18n 标准实现:https://vue-i18n.intlify.dev/guide/essentials/pluralization
|
||
// 如果提供了原始规则函数,优先使用
|
||
if (orgRule != null) {
|
||
return orgRule(choice, choicesLength);
|
||
}
|
||
|
||
// 确保索引在有效范围内
|
||
if (choicesLength <= 1) {
|
||
return 0;
|
||
}
|
||
return choice;
|
||
// 标准英语复数规则
|
||
// 当只有2个选项时: 0=复数形式, 1=单数形式
|
||
// 当有3个或更多选项时: 0=零, 1=单数, 2=复数
|
||
// if (choicesLength == 2) {
|
||
// // 二元模式: [复数, 单数]
|
||
// return choice == 1 ? 1 : 0;
|
||
// } else {
|
||
// // 多元模式: [零, 单数, 复数, ...]
|
||
// if (choice == 0) {
|
||
// return 0; // 零
|
||
// } else if (choice == 1) {
|
||
// return 1; // 单数
|
||
// } else {
|
||
// return Math.min(2, choicesLength - 1); // 复数或最后一个选项
|
||
// }
|
||
// }
|
||
},
|
||
globalInjection: true,
|
||
allowComposition: true,
|
||
legacy: true
|
||
};
|
||
|
||
// 如果没有传入参数,返回默认配置
|
||
if (args == null) {
|
||
return defaultOptions;
|
||
}
|
||
|
||
// 合并用户配置和默认配置
|
||
return {
|
||
locale: args.locale ?? defaultOptions.locale,
|
||
fallbackLocale: args.fallbackLocale ?? defaultOptions.fallbackLocale,
|
||
messages: args.messages ?? defaultOptions.messages,
|
||
datetimeFormats: args.datetimeFormats ?? defaultOptions.datetimeFormats,
|
||
numberFormats: args.numberFormats ?? defaultOptions.numberFormats,
|
||
modifiers: args.modifiers ?? defaultOptions.modifiers,
|
||
pluralRules: args.pluralRules ?? defaultOptions.pluralRules,
|
||
missing: args.missing ?? defaultOptions.missing,
|
||
missingWarn: args.missingWarn ?? defaultOptions.missingWarn,
|
||
fallbackWarn: args.fallbackWarn ?? defaultOptions.fallbackWarn,
|
||
fallbackRoot: args.fallbackRoot ?? defaultOptions.fallbackRoot,
|
||
fallbackFormat: args.fallbackFormat ?? defaultOptions.fallbackFormat,
|
||
unresolving: args.unresolving ?? defaultOptions.unresolving,
|
||
postTranslation: args.postTranslation ?? defaultOptions.postTranslation,
|
||
warnHtmlMessage: args.warnHtmlMessage ?? defaultOptions.warnHtmlMessage,
|
||
escapeParameter: args.escapeParameter ?? defaultOptions.escapeParameter,
|
||
inheritLocale: args.inheritLocale ?? defaultOptions.inheritLocale,
|
||
warnHandler: args.warnHandler ?? defaultOptions.warnHandler,
|
||
pluralRule: args.pluralRule ?? defaultOptions.pluralRule,
|
||
globalInjection: args.globalInjection ?? defaultOptions.globalInjection,
|
||
allowComposition: args.allowComposition ?? defaultOptions.allowComposition,
|
||
legacy: args.legacy ?? defaultOptions.legacy
|
||
} as I18nOptionsReally;
|
||
|
||
|
||
}
|
||
export class Tmui4xI18n implements Tmui4xI18nTml {
|
||
ops : I18nOptionsReally
|
||
constructor(args : I18nOptions|null) {
|
||
this.ops = mergeI18nOpts(args)
|
||
}
|
||
// 语言
|
||
setLocale(local : string) {
|
||
uni.setStorageSync('language',local)
|
||
this.ops.locale = local
|
||
}
|
||
//设置语言
|
||
getLocale() : string {
|
||
return this.ops.locale
|
||
}
|
||
getFallbackLocale() : string {
|
||
return this.ops.fallbackLocale
|
||
}
|
||
|
||
setFallbackLocale(local : string) {
|
||
this.ops.fallbackLocale = local
|
||
}
|
||
setOptions(args : I18nOptions|null){
|
||
this.ops = mergeI18nOpts(args)
|
||
}
|
||
/**
|
||
* 翻译方法 - 参考 Vue I18n 实现(UTS类型安全优化版本)
|
||
* @param key 翻译键
|
||
* @param args 可选参数:数字(用于复数)、对象(用于插值)、字符串(locale)等同原vueI18n使用方式
|
||
* @param opts 可选的第二个参数当args为复数数字,key中函数 | 时,此opts起效且必须为utsjsonobject格式等同原vueI18n使用方式
|
||
* @returns 翻译后的字符串
|
||
*/
|
||
t(key : string, ...argsopts : Array<any>) : string {
|
||
|
||
let repeatCount : NumberOrNull = null;
|
||
let values : UTSJSONObjectOrArray = {};
|
||
let locale : string = this.ops.locale;
|
||
if (argsopts.length > 0) {
|
||
// 严格的类型检查和参数解析
|
||
let args : AnyOrNull = argsopts[0]
|
||
let opts : AnyOrNull = null;
|
||
if (argsopts.length > 1) {
|
||
opts = argsopts[1]
|
||
}
|
||
|
||
if (args != null && opts == null) {
|
||
if (typeof args == 'number') {
|
||
// args 是数字类型
|
||
const numArgs : number = args;
|
||
values = { count: numArgs, n: numArgs };
|
||
repeatCount = numArgs
|
||
} else if (typeof args == 'string') {
|
||
// args 是字符串类型(locale)
|
||
const strArgs : string = args;
|
||
locale = strArgs;
|
||
|
||
} else if (args instanceof UTSJSONObject) {
|
||
const objArgs : UTSJSONObject = args;
|
||
values = objArgs;
|
||
|
||
} else if (Array.isArray(args)) {
|
||
const arrArgs : Array<any> = args;
|
||
values = arrArgs;
|
||
}
|
||
} else if (typeof args == 'number' && opts instanceof UTSJSONObject) {
|
||
const objOpts : UTSJSONObject = opts;
|
||
values = objOpts;
|
||
const numArgs : number = args;
|
||
repeatCount = numArgs
|
||
}
|
||
}
|
||
|
||
|
||
// 类型安全的消息获取
|
||
let message : StringOrNull = this.getMessage(key, locale);
|
||
|
||
// 如果没有找到消息,尝试回退语言
|
||
if (message == null && locale != this.ops.fallbackLocale) {
|
||
message = this.getMessage(key, this.ops.fallbackLocale);
|
||
if (this.ops.fallbackWarn && message != null) {
|
||
this.ops.warnHandler(`Fall back to translate the key '${key}' with '${this.ops.fallbackLocale}' locale.`, null);
|
||
}
|
||
}
|
||
|
||
// 如果仍然没有找到消息
|
||
if (message == null) {
|
||
this.ops.warnHandler(`Not found '${key}' key in '${locale}' locale messages.`, null);
|
||
return key; // 返回键名作为默认值
|
||
}
|
||
|
||
// 类型安全的复数处理
|
||
if (repeatCount != null && typeof message == 'string' && message.includes('|')) {
|
||
const safeCount : number = repeatCount;
|
||
message = this.handlePlural(message, safeCount, locale);
|
||
}
|
||
|
||
// 类型安全的插值处理
|
||
if (typeof message == 'string') {
|
||
message = this.interpolate(message, values);
|
||
}
|
||
|
||
// 类型安全的后处理
|
||
if (this.ops.postTranslation != null && typeof message == 'string') {
|
||
const processedMessage : string = this.ops.postTranslation(message, key);
|
||
message = processedMessage;
|
||
}
|
||
|
||
// 确保返回字符串类型
|
||
return typeof message == 'string' ? message : key;
|
||
}
|
||
|
||
/**
|
||
* 数字格式化方法 - 参考 Vue I18n 实现
|
||
* @param val 要格式化的数字
|
||
* @param name 格式化名称模板,如果不存在以opts为准,如果opts也没有则取默认值。
|
||
* @param opts 格式化选项
|
||
* @returns 格式化后的字符串
|
||
*/
|
||
n(val : number, formatName ?: string, opts ?: NumberFormat) : string {
|
||
let currentLocale = (opts?.local ?? this.ops.locale)!;
|
||
let formatOptions : NumberFormatOpts = {
|
||
style: 'decimal',
|
||
currency: 'CNY',
|
||
local: currentLocale,
|
||
currencyDisplay: 'symbol',
|
||
useGrouping: false,
|
||
minimumIntegerDigits: 1,
|
||
minimumFractionDigits: 0,
|
||
maximumFractionDigits: 3,
|
||
minimumSignificantDigits: null,
|
||
maximumSignificantDigits: null,
|
||
};
|
||
if (opts != null) {
|
||
formatOptions = {
|
||
style: opts?.style ?? formatOptions.style,
|
||
currency: opts?.currency ?? formatOptions.currency,
|
||
local: opts?.local ?? formatOptions.local,
|
||
currencyDisplay: opts?.currencyDisplay ?? formatOptions.currencyDisplay,
|
||
useGrouping: opts?.useGrouping ?? formatOptions.useGrouping,
|
||
minimumIntegerDigits: opts?.minimumIntegerDigits ?? formatOptions.minimumIntegerDigits,
|
||
minimumFractionDigits: opts?.minimumFractionDigits ?? formatOptions.minimumFractionDigits,
|
||
maximumFractionDigits: opts?.maximumFractionDigits ?? formatOptions.maximumFractionDigits,
|
||
minimumSignificantDigits: opts?.minimumSignificantDigits ?? formatOptions.minimumSignificantDigits,
|
||
maximumSignificantDigits: opts?.maximumSignificantDigits ?? formatOptions.maximumSignificantDigits
|
||
};
|
||
}
|
||
|
||
// 处理参数
|
||
if (typeof formatName == 'string') {
|
||
// 如果是字符串,从预定义格式中获取
|
||
const formatNameReal = formatName!;
|
||
if (this.ops.numberFormats.has(currentLocale)) {
|
||
const localeFormats = this.ops.numberFormats.get(currentLocale)!;
|
||
if (localeFormats.has(formatNameReal)) {
|
||
const formatOptions_templ = localeFormats.get(formatNameReal)!;
|
||
formatOptions = {
|
||
style: formatOptions_templ?.style ?? formatOptions.style,
|
||
currency: formatOptions_templ?.currency ?? formatOptions.currency,
|
||
local: formatOptions_templ?.local ?? formatOptions.local,
|
||
currencyDisplay: formatOptions_templ?.currencyDisplay ?? formatOptions.currencyDisplay,
|
||
useGrouping: formatOptions_templ?.useGrouping ?? formatOptions.useGrouping,
|
||
minimumIntegerDigits: formatOptions_templ?.minimumIntegerDigits ?? formatOptions.minimumIntegerDigits,
|
||
minimumFractionDigits: formatOptions_templ?.minimumFractionDigits ?? formatOptions.minimumFractionDigits,
|
||
maximumFractionDigits: formatOptions_templ?.maximumFractionDigits ?? formatOptions.maximumFractionDigits,
|
||
minimumSignificantDigits: formatOptions_templ?.minimumSignificantDigits ?? formatOptions.minimumSignificantDigits,
|
||
maximumSignificantDigits: formatOptions_templ?.maximumSignificantDigits ?? formatOptions.maximumSignificantDigits
|
||
};
|
||
} else {
|
||
this.ops.warnHandler(`Number format '${formatNameReal}' not found for locale '${currentLocale}'`, null);
|
||
}
|
||
} else {
|
||
this.ops.warnHandler(`Number formats not found for locale '${currentLocale}'`, null);
|
||
}
|
||
}
|
||
|
||
currentLocale = formatOptions.local
|
||
// 执行数字格式化
|
||
return this.formatNumber(val, formatOptions, currentLocale);
|
||
}
|
||
|
||
/**
|
||
* 日期时间格式化方法 - 参考 Vue I18n 实现
|
||
* @param val 要格式化的日期时间值(Date对象、时间戳数字或日期字符串)
|
||
* @param formatName 格式化名称模板,如果不存在以opts为准,如果opts也没有则取默认值
|
||
* @param opts 格式化选项
|
||
* @returns 格式化后的字符串
|
||
*/
|
||
d(val : DateOrNumberOrString, formatName ?: StringOrNull, opts ?: DateTimeFormatOrNull) : string {
|
||
let currentLocale = (opts?.local ?? this.ops.locale)!;
|
||
let formatOptions : DateTimeFormatReal = {
|
||
localeMatcher: 'best fit',
|
||
local: currentLocale,
|
||
calendar: 'gregory',
|
||
numberingSystem: 'latn',
|
||
dateSeparator: "",
|
||
timeZone: 'UTC',
|
||
hour12: false,
|
||
hourCycle: 'h23',
|
||
formatMatcher: 'best fit',
|
||
weekday: '',
|
||
era: '',
|
||
year: 'numeric',
|
||
month: 'numeric',
|
||
day: 'numeric',
|
||
dayPeriod: '',
|
||
hour: '',
|
||
minute: '',
|
||
second: '',
|
||
fractionalSecondDigits: 0,
|
||
timeZoneName: '',
|
||
dateStyle: '',
|
||
timeStyle: ''
|
||
};
|
||
|
||
if (opts != null) {
|
||
formatOptions = {
|
||
localeMatcher: opts?.localeMatcher ?? formatOptions.localeMatcher,
|
||
local: opts?.local ?? formatOptions.local,
|
||
dateSeparator: opts?.dateSeparator ?? formatOptions.dateSeparator,
|
||
calendar: opts?.calendar ?? formatOptions.calendar,
|
||
numberingSystem: opts?.numberingSystem ?? formatOptions.numberingSystem,
|
||
timeZone: opts?.timeZone ?? formatOptions.timeZone,
|
||
hour12: opts?.hour12 ?? formatOptions.hour12,
|
||
hourCycle: opts?.hourCycle ?? formatOptions.hourCycle,
|
||
formatMatcher: opts?.formatMatcher ?? formatOptions.formatMatcher,
|
||
weekday: opts?.weekday ?? formatOptions.weekday,
|
||
era: opts?.era ?? formatOptions.era,
|
||
year: opts?.year ?? formatOptions.year,
|
||
month: opts?.month ?? formatOptions.month,
|
||
day: opts?.day ?? formatOptions.day,
|
||
dayPeriod: opts?.dayPeriod ?? formatOptions.dayPeriod,
|
||
hour: opts?.hour ?? formatOptions.hour,
|
||
minute: opts?.minute ?? formatOptions.minute,
|
||
second: opts?.second ?? formatOptions.second,
|
||
fractionalSecondDigits: opts?.fractionalSecondDigits ?? formatOptions.fractionalSecondDigits,
|
||
timeZoneName: opts?.timeZoneName ?? formatOptions.timeZoneName,
|
||
dateStyle: opts?.dateStyle ?? formatOptions.dateStyle,
|
||
timeStyle: opts?.timeStyle ?? formatOptions.timeStyle
|
||
};
|
||
}
|
||
|
||
// 处理参数
|
||
if (typeof formatName == 'string') {
|
||
// 如果是字符串,从预定义格式中获取
|
||
const formatNameReal = formatName!;
|
||
if (this.ops.datetimeFormats.has(currentLocale)) {
|
||
const localeFormats = this.ops.datetimeFormats.get(currentLocale)!;
|
||
if (localeFormats.has(formatNameReal)) {
|
||
const formatOptions_templ = localeFormats.get(formatNameReal)!;
|
||
formatOptions = {
|
||
localeMatcher: formatOptions_templ?.localeMatcher ?? formatOptions.localeMatcher,
|
||
local: formatOptions_templ?.local ?? formatOptions.local,
|
||
calendar: formatOptions_templ?.calendar ?? formatOptions.calendar,
|
||
numberingSystem: formatOptions_templ?.numberingSystem ?? formatOptions.numberingSystem,
|
||
timeZone: formatOptions_templ?.timeZone ?? formatOptions.timeZone,
|
||
hour12: formatOptions_templ?.hour12 ?? formatOptions.hour12,
|
||
hourCycle: formatOptions_templ?.hourCycle ?? formatOptions.hourCycle,
|
||
formatMatcher: formatOptions_templ?.formatMatcher ?? formatOptions.formatMatcher,
|
||
dateSeparator: formatOptions_templ?.dateSeparator ?? formatOptions.dateSeparator,
|
||
weekday: formatOptions_templ?.weekday ?? formatOptions.weekday,
|
||
era: formatOptions_templ?.era ?? formatOptions.era,
|
||
year: formatOptions_templ?.year ?? formatOptions.year,
|
||
month: formatOptions_templ?.month ?? formatOptions.month,
|
||
day: formatOptions_templ?.day ?? formatOptions.day,
|
||
dayPeriod: formatOptions_templ?.dayPeriod ?? formatOptions.dayPeriod,
|
||
hour: formatOptions_templ?.hour ?? formatOptions.hour,
|
||
minute: formatOptions_templ?.minute ?? formatOptions.minute,
|
||
second: formatOptions_templ?.second ?? formatOptions.second,
|
||
fractionalSecondDigits: formatOptions_templ?.fractionalSecondDigits ?? formatOptions.fractionalSecondDigits,
|
||
timeZoneName: formatOptions_templ?.timeZoneName ?? formatOptions.timeZoneName,
|
||
dateStyle: formatOptions_templ?.dateStyle ?? formatOptions.dateStyle,
|
||
timeStyle: formatOptions_templ?.timeStyle ?? formatOptions.timeStyle
|
||
};
|
||
} else {
|
||
this.ops.warnHandler(`DateTime format '${formatNameReal}' not found for locale '${currentLocale}'`, null);
|
||
}
|
||
} else {
|
||
this.ops.warnHandler(`DateTime formats not found for locale '${currentLocale}'`, null);
|
||
}
|
||
}
|
||
currentLocale = formatOptions.local
|
||
|
||
// 执行日期时间格式化
|
||
return this.formatDateTime(val, formatOptions, currentLocale);
|
||
}
|
||
/**
|
||
* 动态添加语言的新字段
|
||
* @param {string} local 语言
|
||
* @param {UTSJSONObject} newMessage 键值及字段。
|
||
*/
|
||
mergeLocaleMessage(local : string, newMessage : UTSJSONObject) {
|
||
let nowmessage = this.ops.messages.getJSON(local);
|
||
if (nowmessage != null) {
|
||
nowmessage = { ...nowmessage, ...newMessage }
|
||
this.ops.messages.set(local, nowmessage)
|
||
} else {
|
||
this.ops.messages.set(local, newMessage)
|
||
}
|
||
}
|
||
/**
|
||
* 检测翻译键是否存在 - 参考 Vue I18n 的 te 方法实现
|
||
* @param {string} key 翻译键,支持嵌套路径如 'user.name'
|
||
* @param {string} locale 可选的语言代码,不提供则使用当前语言
|
||
* @returns {boolean} 存在返回true,不存在返回false
|
||
*/
|
||
te(key : string, locale ?: string | null) : boolean {
|
||
// 确定要检查的语言
|
||
const targetLocale = (locale != null ? locale : this.ops.locale);
|
||
|
||
// 检查语言包是否存在
|
||
const messages = this.ops.messages;
|
||
if (messages[targetLocale] == null) {
|
||
return false;
|
||
}
|
||
|
||
// 获取指定语言的消息
|
||
const localeMessages = messages[targetLocale] as UTSJSONObjectOrNull;
|
||
if (localeMessages == null) {
|
||
return false;
|
||
}
|
||
|
||
// 检查翻译键是否存在
|
||
const message = this.getNestedValue(localeMessages, key);
|
||
return message != null;
|
||
}
|
||
/**
|
||
* 相对时间
|
||
* @param {string,number,Date} timeValue 时间差(毫秒)
|
||
* @param {string} unit 首选单位,它会自动进阶更高的维度,比如提供秒,超过60秒以分为单位进阶。
|
||
* @param {string} locale 可选语言,不提供以默认创建的设置为准。
|
||
* @returns {string} 格式化后的时间字符串
|
||
*/
|
||
rt(timeValues ?: number | Date | string | null, units ?: string | null, locale ?: string | null) : string {
|
||
const targetLocale = (locale != null ? locale : this.ops.locale)
|
||
const timeValue = (timeValues != null ? new Date() : timeValues!)
|
||
const unit = (units != null ? 'minute' : units!)
|
||
|
||
|
||
try {
|
||
// 获取目标时间的时间戳
|
||
let targetTimestamp = 0;
|
||
if (typeof timeValue == 'number') {
|
||
targetTimestamp = timeValue;
|
||
} else if (timeValue instanceof Date) {
|
||
targetTimestamp = timeValue.getTime();
|
||
} else if (typeof timeValue == 'string') {
|
||
targetTimestamp = new Date(timeValue.replace(/-/g,'/')).getTime();
|
||
}
|
||
|
||
// 获取当前时间戳
|
||
const currentTimestamp = new Date().getTime();
|
||
|
||
// 计算时间差(毫秒)
|
||
const timeDiff = targetTimestamp - currentTimestamp;
|
||
|
||
// 验证单位参数
|
||
const validUnits = ['year', 'years', 'quarter', 'quarters', 'month', 'months', 'week', 'weeks', 'day', 'days', 'hour', 'hours', 'minute', 'minutes', 'second', 'seconds'];
|
||
let normalizedUnit = unit.toLowerCase();
|
||
|
||
// 标准化单位名称(移除复数形式)
|
||
if (normalizedUnit.endsWith('s') && normalizedUnit != 'seconds') {
|
||
normalizedUnit = normalizedUnit.slice(0, -1);
|
||
}
|
||
|
||
// 处理特殊单位名称
|
||
switch (normalizedUnit) {
|
||
case 'yr':
|
||
case 'y':
|
||
normalizedUnit = 'year';
|
||
break;
|
||
case 'mo':
|
||
case 'm':
|
||
normalizedUnit = 'month';
|
||
break;
|
||
case 'w':
|
||
normalizedUnit = 'week';
|
||
break;
|
||
case 'd':
|
||
normalizedUnit = 'day';
|
||
break;
|
||
case 'h':
|
||
normalizedUnit = 'hour';
|
||
break;
|
||
case 'min':
|
||
normalizedUnit = 'minute';
|
||
break;
|
||
case 's':
|
||
case 'sec':
|
||
normalizedUnit = 'second';
|
||
break;
|
||
case 'q':
|
||
normalizedUnit = 'quarter';
|
||
break;
|
||
}
|
||
|
||
// 验证单位是否有效
|
||
if (!validUnits.includes(normalizedUnit) && !validUnits.includes(normalizedUnit + 's')) {
|
||
this.ops.warnHandler(`Invalid unit '${unit}' for relative time formatting. Valid units are: ${validUnits.join(', ')}`, null);
|
||
return `${timeValue} ${unit}`; // 返回原始格式作为回退
|
||
}
|
||
|
||
// 智能选择最合适的时间单位和数值
|
||
const bestTime = this.getBestTimeUnit(timeDiff, normalizedUnit);
|
||
|
||
// 使用自定义实现,确保跨平台兼容性
|
||
return this.formatRelativeTimeFallback(bestTime.value, bestTime.unit, targetLocale);
|
||
|
||
} catch (e) {
|
||
this.ops.warnHandler(`Relative time formatting failed for value ${timeValue} ${unit}`, e as Error);
|
||
return `${timeValue} ${unit}`; // 返回原始格式作为回退
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取可用语言列表
|
||
* @returns {string[]}
|
||
*/
|
||
availableLocales() : string[] {
|
||
return UTSJSONObject.keys(this.ops.messages)
|
||
}
|
||
/**
|
||
* 智能选择最合适的时间单位和数值
|
||
* @param timeDiff 时间差(毫秒)
|
||
* @param preferredUnit 首选单位
|
||
* @returns 包含最佳单位和数值的对象
|
||
*/
|
||
private getBestTimeUnit(timeDiff : number, preferredUnit : string) : BestTimeTYpe {
|
||
const absTimeDiff = Math.abs(timeDiff);
|
||
|
||
// 定义时间单位的毫秒值和优先级
|
||
const timeUnits:timeUnitsTYpe[] = [
|
||
{ unit: 'second', ms: 1000, threshold: 60 },
|
||
{ unit: 'minute', ms: 1000 * 60, threshold: 60 },
|
||
{ unit: 'hour', ms: 1000 * 60 * 60, threshold: 24 },
|
||
{ unit: 'day', ms: 1000 * 60 * 60 * 24, threshold: 7 },
|
||
{ unit: 'week', ms: 1000 * 60 * 60 * 24 * 7, threshold: 4.33 },
|
||
{ unit: 'month', ms: 1000 * 60 * 60 * 24 * 30.44, threshold: 3 },
|
||
{ unit: 'quarter', ms: 1000 * 60 * 60 * 24 * 30.44 * 3, threshold: 4 },
|
||
{ unit: 'year', ms: 1000 * 60 * 60 * 24 * 365.25, threshold: Infinity }
|
||
];
|
||
|
||
// 找到首选单位的索引
|
||
let startIndex = timeUnits.findIndex(tu => tu.unit === preferredUnit);
|
||
if (startIndex === -1) startIndex = 0; // 如果找不到,从秒开始
|
||
|
||
// 从首选单位开始,向上查找最合适的单位
|
||
for (let i = startIndex; i < timeUnits.length; i++) {
|
||
const currentUnit = timeUnits[i];
|
||
const value = Math.round(timeDiff / currentUnit.ms);
|
||
const absValue = Math.abs(value);
|
||
|
||
// 如果当前单位的值在合理范围内,或者已经是最大单位
|
||
if (absValue < currentUnit.threshold || i === timeUnits.length - 1) {
|
||
return { unit: currentUnit.unit, value: value } as BestTimeTYpe;
|
||
}
|
||
}
|
||
|
||
// 回退到首选单位
|
||
const fallbackUnit = timeUnits[startIndex];
|
||
return {
|
||
unit: fallbackUnit.unit,
|
||
value: Math.round(timeDiff / fallbackUnit.ms)
|
||
} as BestTimeTYpe;
|
||
}
|
||
|
||
/**
|
||
* 相对时间格式化的回退实现
|
||
* @param value 数值
|
||
* @param unit 单位
|
||
* @param locale 语言代码
|
||
* @returns 格式化后的相对时间字符串
|
||
*/
|
||
private formatRelativeTimeFallback(value : number, unit : string, locale : string) : string {
|
||
const absValue = Math.abs(value);
|
||
const isPast = value < 0;
|
||
const isFuture = value > 0;
|
||
const isNow = value == 0;
|
||
|
||
// 获取语言代码
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
|
||
// 处理特殊情况(今天、昨天、明天等)
|
||
if (unit == 'day') {
|
||
if (isNow) {
|
||
return langCode == 'zh' ? '今天' : langCode == 'ja' ? '今日' : langCode == 'ko' ? '오늘' : 'today';
|
||
} else if (value == -1) {
|
||
return langCode == 'zh' ? '昨天' : langCode == 'ja' ? '昨日' : langCode == 'ko' ? '어제' : 'yesterday';
|
||
} else if (value == 1) {
|
||
return langCode == 'zh' ? '明天' : langCode == 'ja' ? '明日' : langCode == 'ko' ? '내일' : 'tomorrow';
|
||
}
|
||
}
|
||
|
||
// 获取单位的本地化名称
|
||
const unitName = this.getLocalizedUnitName(unit, absValue, langCode);
|
||
|
||
// 构建相对时间字符串
|
||
if (langCode == 'zh') {
|
||
if (isPast) {
|
||
return `${absValue}${unitName}前`;
|
||
} else if (isFuture) {
|
||
return `${absValue}${unitName}后`;
|
||
} else {
|
||
return `现在`;
|
||
}
|
||
} else if (langCode == 'ja') {
|
||
if (isPast) {
|
||
return `${absValue}${unitName}前`;
|
||
} else if (isFuture) {
|
||
return `${absValue}${unitName}後`;
|
||
} else {
|
||
return `今`;
|
||
}
|
||
} else if (langCode == 'ko') {
|
||
if (isPast) {
|
||
return `${absValue}${unitName} 전`;
|
||
} else if (isFuture) {
|
||
return `${absValue}${unitName} 후`;
|
||
} else {
|
||
return `지금`;
|
||
}
|
||
} else {
|
||
// 英语和其他语言
|
||
if (isPast) {
|
||
return `${absValue} ${unitName} ago`;
|
||
} else if (isFuture) {
|
||
return `in ${absValue} ${unitName}`;
|
||
} else {
|
||
return `now`;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取本地化的单位名称
|
||
* @param unit 单位
|
||
* @param value 数值(用于确定单复数)
|
||
* @param langCode 语言代码
|
||
* @returns 本地化的单位名称
|
||
*/
|
||
private getLocalizedUnitName(unit : string, value : number, langCode : string) : string {
|
||
const isPlural = value != 1;
|
||
|
||
switch (langCode) {
|
||
case 'zh':
|
||
switch (unit) {
|
||
case 'year': return '年';
|
||
case 'quarter': return '季度';
|
||
case 'month': return '个月';
|
||
case 'week': return '周';
|
||
case 'day': return '天';
|
||
case 'hour': return '小时';
|
||
case 'minute': return '分钟';
|
||
case 'second': return '秒';
|
||
default: return unit;
|
||
}
|
||
case 'ja':
|
||
switch (unit) {
|
||
case 'year': return '年';
|
||
case 'quarter': return '四半期';
|
||
case 'month': return 'ヶ月';
|
||
case 'week': return '週間';
|
||
case 'day': return '日';
|
||
case 'hour': return '時間';
|
||
case 'minute': return '分';
|
||
case 'second': return '秒';
|
||
default: return unit;
|
||
}
|
||
case 'ko':
|
||
switch (unit) {
|
||
case 'year': return '년';
|
||
case 'quarter': return '분기';
|
||
case 'month': return '개월';
|
||
case 'week': return '주';
|
||
case 'day': return '일';
|
||
case 'hour': return '시간';
|
||
case 'minute': return '분';
|
||
case 'second': return '초';
|
||
default: return unit;
|
||
}
|
||
default:
|
||
// 英语和其他语言
|
||
switch (unit) {
|
||
case 'year': return isPlural ? 'years' : 'year';
|
||
case 'quarter': return isPlural ? 'quarters' : 'quarter';
|
||
case 'month': return isPlural ? 'months' : 'month';
|
||
case 'week': return isPlural ? 'weeks' : 'week';
|
||
case 'day': return isPlural ? 'days' : 'day';
|
||
case 'hour': return isPlural ? 'hours' : 'hour';
|
||
case 'minute': return isPlural ? 'minutes' : 'minute';
|
||
case 'second': return isPlural ? 'seconds' : 'second';
|
||
default: return unit;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 内部日期时间格式化实现
|
||
* @param value 日期时间值
|
||
* @param options 格式化选项
|
||
* @param locale 语言代码
|
||
* @returns 格式化后的字符串
|
||
*/
|
||
private formatDateTime(value : DateOrNumberOrString, options : DateTimeFormatReal, locale : string) : string {
|
||
try {
|
||
// 转换输入值为Date对象
|
||
let dateObj : Date = new Date();
|
||
if (value instanceof Date) {
|
||
dateObj = value;
|
||
} else if (typeof value == 'number') {
|
||
dateObj = new Date(value);
|
||
} else if (typeof value == 'string') {
|
||
dateObj = new Date(value.replace(/-/g,'/'));
|
||
}
|
||
|
||
// 检查日期是否有效
|
||
if (isNaN(dateObj.getTime())) {
|
||
this.ops.warnHandler(`Invalid date: ${value}`, null);
|
||
return value.toString();
|
||
}
|
||
|
||
// 处理预定义样式
|
||
if (options.dateStyle != '' || options.timeStyle != '') {
|
||
return this.formatWithPredefinedStyle(dateObj, options, locale);
|
||
}
|
||
|
||
// 自定义格式化
|
||
return this.formatWithCustomOptions(dateObj, options, locale);
|
||
|
||
} catch (e) {
|
||
this.ops.warnHandler(`DateTime formatting failed for value ${value}`, e as Error);
|
||
return value.toString();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 使用预定义样式格式化日期时间
|
||
* @param date Date对象
|
||
* @param options 格式化选项
|
||
* @param locale 语言代码
|
||
* @returns 格式化后的字符串
|
||
*/
|
||
private formatWithPredefinedStyle(date : Date, options : DateTimeFormatReal, locale : string) : string {
|
||
let result = '';
|
||
|
||
// 处理日期样式
|
||
if (options.dateStyle != '') {
|
||
switch (options.dateStyle) {
|
||
case 'full':
|
||
result += this.getFullDateFormat(date, locale);
|
||
break;
|
||
case 'long':
|
||
result += this.getLongDateFormat(date, locale);
|
||
break;
|
||
case 'medium':
|
||
result += this.getMediumDateFormat(date, locale);
|
||
break;
|
||
case 'short':
|
||
result += this.getShortDateFormat(date, locale);
|
||
break;
|
||
default:
|
||
result += this.getMediumDateFormat(date, locale);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 处理时间样式
|
||
if (options.timeStyle != '') {
|
||
if (result != '') result += ' ';
|
||
switch (options.timeStyle) {
|
||
case 'full':
|
||
result += this.getFullTimeFormat(date, options, locale);
|
||
break;
|
||
case 'long':
|
||
result += this.getLongTimeFormat(date, options, locale);
|
||
break;
|
||
case 'medium':
|
||
result += this.getMediumTimeFormat(date, options, locale);
|
||
break;
|
||
case 'short':
|
||
result += this.getShortTimeFormat(date, options, locale);
|
||
break;
|
||
default:
|
||
result += this.getMediumTimeFormat(date, options, locale);
|
||
break;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 使用自定义选项格式化日期时间
|
||
* @param date Date对象
|
||
* @param options 格式化选项
|
||
* @param locale 语言代码
|
||
* @returns 格式化后的字符串
|
||
*/
|
||
private formatWithCustomOptions(date : Date, options : DateTimeFormatReal, locale : string) : string {
|
||
const dateParts : string[] = [];
|
||
const timeParts : string[] = [];
|
||
|
||
// 根据语言环境确定日期组件的排列顺序
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
const isUSFormat = langCode == 'en' && (locale.toLowerCase().includes('us') || locale.toLowerCase() == 'en');
|
||
|
||
// 处理非日期组件(星期和纪元)
|
||
if (options.weekday != '') {
|
||
dateParts.push(this.formatWeekday(date, options.weekday, locale));
|
||
}
|
||
|
||
if (options.era != '') {
|
||
dateParts.push(this.formatEra(date, options.era, locale));
|
||
}
|
||
|
||
// 根据语言环境按正确顺序添加日期组件
|
||
if (isUSFormat) {
|
||
// 美式英语:月/日/年
|
||
if (options.month != '') {
|
||
dateParts.push(this.formatMonth(date, options.month, locale, options.numberingSystem, options.dateSeparator));
|
||
}
|
||
if (options.day != '') {
|
||
dateParts.push(this.formatDay(date, options.day, locale, options.numberingSystem, options.dateSeparator));
|
||
}
|
||
if (options.year != '') {
|
||
dateParts.push(this.formatYear(date, options.year, locale, options.numberingSystem, options.dateSeparator));
|
||
}
|
||
} else {
|
||
// 其他语言(包括中文、英式英语等):年/月/日 或 日/月/年
|
||
if (langCode == 'zh' || langCode == 'ja' || langCode == 'ko') {
|
||
// 东亚语言:年/月/日
|
||
if (options.year != '') {
|
||
dateParts.push(this.formatYear(date, options.year, locale, options.numberingSystem, options.dateSeparator));
|
||
}
|
||
if (options.month != '') {
|
||
dateParts.push(this.formatMonth(date, options.month, locale, options.numberingSystem, options.dateSeparator));
|
||
}
|
||
if (options.day != '') {
|
||
dateParts.push(this.formatDay(date, options.day, locale, options.numberingSystem, options.dateSeparator));
|
||
}
|
||
} else {
|
||
// 欧洲语言(英式英语、德语、法语等):日/月/年
|
||
if (options.day != '') {
|
||
dateParts.push(this.formatDay(date, options.day, locale, options.numberingSystem, options.dateSeparator));
|
||
}
|
||
if (options.month != '') {
|
||
dateParts.push(this.formatMonth(date, options.month, locale, options.numberingSystem, options.dateSeparator));
|
||
}
|
||
if (options.year != '') {
|
||
dateParts.push(this.formatYear(date, options.year, locale, options.numberingSystem, options.dateSeparator));
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理时间组件
|
||
if (options.hour != '') {
|
||
timeParts.push(this.formatHour(date, options.hour, options.hour12, options.hourCycle, options.numberingSystem));
|
||
}
|
||
|
||
if (options.minute != '') {
|
||
timeParts.push(this.formatMinute(date, options.minute, options.numberingSystem));
|
||
}
|
||
|
||
if (options.second != '') {
|
||
timeParts.push(this.formatSecond(date, options.second, options.fractionalSecondDigits, options.numberingSystem));
|
||
}
|
||
|
||
// 组合时间部分(用冒号连接)
|
||
let timeStr = '';
|
||
if (timeParts.length > 0) {
|
||
timeStr = timeParts.join(':');
|
||
|
||
// 添加 AM/PM
|
||
if (options.dayPeriod != '' && options.hour12) {
|
||
timeStr += ` ${this.formatDayPeriod(date, options.dayPeriod, locale)}`;
|
||
}
|
||
|
||
// 添加时区
|
||
if (options.timeZoneName != '') {
|
||
timeStr += ` ${this.formatTimeZone(date, options.timeZoneName, options.timeZone)}`;
|
||
}
|
||
}
|
||
|
||
// 组合日期和时间
|
||
const result : string[] = [];
|
||
if (dateParts.length > 0) {
|
||
result.push(this.combineDateTimeParts(dateParts, locale, options.dateSeparator));
|
||
}
|
||
if (timeStr != '') {
|
||
result.push(timeStr);
|
||
}
|
||
|
||
return result.join(' ');
|
||
}
|
||
|
||
/**
|
||
* 内部数字格式化实现
|
||
* @param value 数字值
|
||
* @param options 格式化选项
|
||
* @param locale 语言代码
|
||
* @returns 格式化后的字符串
|
||
*/
|
||
private formatNumber(value : number, options : NumberFormatOpts, locale : string) : string {
|
||
try {
|
||
// 基础格式化逻辑
|
||
let result = value.toString();
|
||
|
||
// 处理有效数字位数(优先级高于小数位数)
|
||
if (options.minimumSignificantDigits != null || options.maximumSignificantDigits != null) {
|
||
result = this.formatWithSignificantDigits(value, options);
|
||
} else {
|
||
// 处理小数位数
|
||
if (options.minimumFractionDigits != null || options.maximumFractionDigits != null) {
|
||
const minDigits = options.minimumFractionDigits == null ? 0 : options.minimumFractionDigits!;
|
||
const maxDigits = options.maximumFractionDigits == null ? 3 : options.maximumFractionDigits!;
|
||
result = value.toFixed(Math.max(minDigits, Math.min(maxDigits, 10)));
|
||
|
||
// 移除尾随零(如果maxDigits允许)
|
||
if (maxDigits > minDigits) {
|
||
result = parseFloat(result).toString();
|
||
// 确保最小小数位数
|
||
if (minDigits > 0) {
|
||
const parts = result.split('.');
|
||
if (parts.length == 1) {
|
||
result += '.' + '0'.repeat(minDigits);
|
||
} else {
|
||
const decimalPart = parts[1];
|
||
if (decimalPart.length < minDigits) {
|
||
result += '0'.repeat(minDigits - decimalPart.length);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理最小整数位数
|
||
if (options.minimumIntegerDigits != null && options.minimumIntegerDigits > 1) {
|
||
result = this.formatWithMinimumIntegerDigits(result, options.minimumIntegerDigits);
|
||
}
|
||
}
|
||
|
||
// 处理千分位分隔符
|
||
if (options.useGrouping == true) {
|
||
result = this.addThousandsSeparator(result, locale);
|
||
}
|
||
|
||
// 处理样式
|
||
if (options.style != null) {
|
||
switch (options.style) {
|
||
case 'currency':
|
||
result = this.formatCurrency(result, options, locale);
|
||
break;
|
||
case 'percent':
|
||
result = this.formatPercent(result, value);
|
||
break;
|
||
case 'decimal':
|
||
default:
|
||
// 已经是十进制格式
|
||
break;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
} catch (e) {
|
||
this.ops.warnHandler(`Number formatting failed for value ${value}`, e as Error);
|
||
return value.toString();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 使用有效数字位数格式化数字
|
||
* @param value 数字值
|
||
* @param options 格式化选项
|
||
* @returns 格式化后的字符串
|
||
*/
|
||
private formatWithSignificantDigits(value : number, options : NumberFormatOpts) : string {
|
||
const minSigDigits = options.minimumSignificantDigits ?? 1;
|
||
const maxSigDigits = options.maximumSignificantDigits ?? 21;
|
||
|
||
// 确保有效数字位数在合理范围内
|
||
const actualMinSigDigits = Math.max(1, Math.min(minSigDigits, 21));
|
||
const actualMaxSigDigits = Math.max(actualMinSigDigits, Math.min(maxSigDigits, 21));
|
||
|
||
// 使用toPrecision方法格式化有效数字
|
||
let result = value.toPrecision(actualMaxSigDigits);
|
||
|
||
// 移除科学计数法表示(如果不是很大或很小的数)
|
||
if (!result.includes('e') && !result.includes('E')) {
|
||
// 移除尾随零,但保持最小有效数字位数
|
||
const num = parseFloat(result);
|
||
result = num.toPrecision(actualMinSigDigits);
|
||
|
||
// 如果结果不包含科学计数法,转换为普通数字格式
|
||
if (!result.includes('e') && !result.includes('E')) {
|
||
result = parseFloat(result).toString();
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 使用最小整数位数格式化数字
|
||
* @param numStr 数字字符串
|
||
* @param minIntegerDigits 最小整数位数
|
||
* @returns 格式化后的字符串
|
||
*/
|
||
private formatWithMinimumIntegerDigits(numStr : string, minIntegerDigits : number) : string {
|
||
const parts = numStr.split('.');
|
||
const integerPart = parts[0];
|
||
const decimalPart = parts.length > 1 ? parts[1] : '';
|
||
|
||
// 在整数部分前面补零
|
||
const paddedInteger = integerPart.padStart(minIntegerDigits, '0');
|
||
|
||
return decimalPart == '' ? `${paddedInteger}.${decimalPart}` : paddedInteger;
|
||
}
|
||
|
||
// ==================== 日期时间格式化辅助方法 ====================
|
||
|
||
/**
|
||
* 获取完整日期格式
|
||
*/
|
||
private getFullDateFormat(date : Date, locale : string) : string {
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
const isUSFormat = langCode == 'en' && (locale.toLowerCase().includes('us') || locale.toLowerCase() == 'en');
|
||
|
||
if (langCode == 'zh') {
|
||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日 ${this.formatWeekday(date, 'long', locale)}`;
|
||
} else if (langCode == 'ja') {
|
||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日 ${this.formatWeekday(date, 'long', locale)}`;
|
||
} else if (langCode == 'ko') {
|
||
return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일 ${this.formatWeekday(date, 'long', locale)}`;
|
||
} else if (isUSFormat) {
|
||
// 美式英语:dddd, MMMM d, yyyy
|
||
return `${this.formatWeekday(date, 'long', locale)}, ${this.formatMonth(date, 'long', locale)} ${date.getDate()}, ${date.getFullYear()}`;
|
||
} else {
|
||
// 欧洲语言:dddd, d MMMM yyyy
|
||
return `${this.formatWeekday(date, 'long', locale)}, ${date.getDate()} ${this.formatMonth(date, 'long', locale)} ${date.getFullYear()}`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取长日期格式
|
||
*/
|
||
private getLongDateFormat(date : Date, locale : string) : string {
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
const isUSFormat = langCode == 'en' && (locale.toLowerCase().includes('us') || locale.toLowerCase() == 'en');
|
||
|
||
if (langCode == 'zh') {
|
||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
|
||
} else if (langCode == 'ja') {
|
||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
|
||
} else if (langCode == 'ko') {
|
||
return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`;
|
||
} else if (isUSFormat) {
|
||
// 美式英语:MMMM d, yyyy
|
||
return `${this.formatMonth(date, 'long', locale)} ${date.getDate()}, ${date.getFullYear()}`;
|
||
} else {
|
||
// 欧洲语言:d MMMM yyyy
|
||
return `${date.getDate()} ${this.formatMonth(date, 'long', locale)} ${date.getFullYear()}`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取中等日期格式
|
||
*/
|
||
private getMediumDateFormat(date : Date, locale : string) : string {
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
const isUSFormat = langCode == 'en' && (locale.toLowerCase().includes('us') || locale.toLowerCase() == 'en');
|
||
|
||
if (langCode == 'zh' || langCode == 'ja' || langCode == 'ko') {
|
||
// 东亚语言:YYYY-MM-dd
|
||
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
|
||
} else if (isUSFormat) {
|
||
// 美式英语:MMM d, yyyy
|
||
return `${this.formatMonth(date, 'short', locale)} ${date.getDate()}, ${date.getFullYear()}`;
|
||
} else {
|
||
// 欧洲语言:d MMM yyyy
|
||
return `${date.getDate()} ${this.formatMonth(date, 'short', locale)} ${date.getFullYear()}`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取短日期格式
|
||
*/
|
||
private getShortDateFormat(date : Date, locale : string) : string {
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
const isUSFormat = langCode == 'en' && (locale.toLowerCase().includes('us') || locale.toLowerCase() == 'en');
|
||
const year = date.getFullYear().toString().slice(-2);
|
||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||
const day = date.getDate().toString().padStart(2, '0');
|
||
|
||
if (isUSFormat) {
|
||
// 美式英语:MM/dd/yy
|
||
return `${month}/${day}/${year}`;
|
||
} else if (langCode == 'zh' || langCode == 'ja' || langCode == 'ko') {
|
||
// 东亚语言:yy/MM/dd
|
||
return `${year}/${month}/${day}`;
|
||
} else {
|
||
// 欧洲语言(英式英语、德语、法语等):dd/MM/yy
|
||
return `${day}/${month}/${year}`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取完整时间格式
|
||
*/
|
||
private getFullTimeFormat(date : Date, options : DateTimeFormatReal, locale : string) : string {
|
||
const timeStr = this.getMediumTimeFormat(date, options, locale);
|
||
const timeZone = this.formatTimeZone(date, 'long', options.timeZone);
|
||
return `${timeStr} ${timeZone}`;
|
||
}
|
||
|
||
/**
|
||
* 获取长时间格式
|
||
*/
|
||
private getLongTimeFormat(date : Date, options : DateTimeFormatReal, locale : string) : string {
|
||
const timeStr = this.getMediumTimeFormat(date, options, locale);
|
||
const timeZone = this.formatTimeZone(date, 'short', options.timeZone);
|
||
return `${timeStr} ${timeZone}`;
|
||
}
|
||
|
||
/**
|
||
* 获取中等时间格式
|
||
*/
|
||
private getMediumTimeFormat(date : Date, options : DateTimeFormatReal, locale : string) : string {
|
||
const hour = this.formatHour(date, 'numeric', options.hour12, options.hourCycle);
|
||
const minute = this.formatMinute(date, '2-digit');
|
||
const second = this.formatSecond(date, '2-digit', 0);
|
||
const dayPeriod = options.hour12 ? ` ${this.formatDayPeriod(date, 'short', locale)}` : '';
|
||
return `${hour}:${minute}:${second}${dayPeriod}`;
|
||
}
|
||
|
||
/**
|
||
* 获取短时间格式
|
||
*/
|
||
private getShortTimeFormat(date : Date, options : DateTimeFormatReal, locale : string) : string {
|
||
const hour = this.formatHour(date, 'numeric', options.hour12, options.hourCycle);
|
||
const minute = this.formatMinute(date, '2-digit');
|
||
const dayPeriod = options.hour12 ? ` ${this.formatDayPeriod(date, 'short', locale)}` : '';
|
||
return `${hour}:${minute}${dayPeriod}`;
|
||
}
|
||
|
||
/**
|
||
* 格式化星期
|
||
*/
|
||
private formatWeekday(date : Date, style : string, locale : string) : string {
|
||
const weekdays = this.getWeekdayNames(locale);
|
||
const dayIndex = date.getDay();
|
||
|
||
switch (style) {
|
||
case 'long':
|
||
return weekdays.long[dayIndex];
|
||
case 'short':
|
||
return weekdays.short[dayIndex];
|
||
case 'narrow':
|
||
return weekdays.narrow[dayIndex];
|
||
default:
|
||
return weekdays.long[dayIndex];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 格式化纪元
|
||
*/
|
||
private formatEra(date : Date, style : string, locale : string) : string {
|
||
const year = date.getFullYear();
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
|
||
if (langCode == 'zh') {
|
||
return year > 0 ? '公元' : '公元前';
|
||
} else {
|
||
switch (style) {
|
||
case 'long':
|
||
return year > 0 ? 'Anno Domini' : 'Before Christ';
|
||
case 'short':
|
||
case 'narrow':
|
||
default:
|
||
return year > 0 ? 'AD' : 'BC';
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 数字系统转换
|
||
* @param number 数字字符串
|
||
* @param numberingSystem 数字系统
|
||
* @returns 转换后的数字字符串
|
||
*/
|
||
private convertNumberingSystem(number : string, numberingSystem : string) : string {
|
||
if (numberingSystem == 'latn' || numberingSystem == '') {
|
||
return number; // 拉丁数字(默认)
|
||
}
|
||
|
||
// 数字系统映射表
|
||
const numberingSystems = new Map<string, string[]>();
|
||
numberingSystems.set('arab', ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩']); // 阿拉伯-印度数字
|
||
numberingSystems.set('arabext', ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']); // 扩展阿拉伯-印度数字
|
||
numberingSystems.set('deva', ['०', '१', '२', '३', '४', '५', '६', '७', '८', '९']); // 天城文数字
|
||
numberingSystems.set('fullwide', ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']); // 全角数字
|
||
numberingSystems.set('hanidec', ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九']); // 汉字数字
|
||
numberingSystems.set('thai', ['๐', '๑', '๒', '๓', '๔', '๕', '๖', '๗', '๘', '๙']); // 泰文数字
|
||
|
||
const digits = numberingSystems.get(numberingSystem);
|
||
if (digits == null) {
|
||
return number; // 不支持的数字系统,返回原数字
|
||
}
|
||
|
||
// 转换每个数字字符
|
||
let result = '';
|
||
for (let i = 0; i < number.length; i++) {
|
||
const char = number.charAt(i);
|
||
if (char >= '0' && char <= '9') {
|
||
const digitIndex = parseInt(char);
|
||
result += digits[digitIndex];
|
||
} else {
|
||
result += char; // 非数字字符保持不变
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 格式化年份
|
||
*/
|
||
private formatYear(date : Date, style : string, locale : string = '', numberingSystem : string = 'latn', dateSeparator : string = '') : string {
|
||
const year = date.getFullYear();
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
|
||
let yearStr = '';
|
||
switch (style) {
|
||
case 'numeric':
|
||
yearStr = this.convertNumberingSystem(year.toString(), numberingSystem);
|
||
// 如果提供了分隔符,返回纯数字;否则中文环境下添加"年"字
|
||
return (dateSeparator != '' || langCode != 'zh') ? yearStr : `${yearStr}年`;
|
||
case '2-digit':
|
||
const shortYear = year.toString().slice(-2).padStart(2, '0');
|
||
yearStr = this.convertNumberingSystem(shortYear, numberingSystem);
|
||
return (dateSeparator != '' || langCode != 'zh') ? yearStr : `${yearStr}年`;
|
||
default:
|
||
yearStr = this.convertNumberingSystem(year.toString(), numberingSystem);
|
||
return (dateSeparator != '' || langCode != 'zh') ? yearStr : `${yearStr}年`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 格式化月份
|
||
*/
|
||
private formatMonth(date : Date, style : string, locale : string, numberingSystem : string = 'latn', dateSeparator : string = '') : string {
|
||
const month = date.getMonth();
|
||
const monthNames = this.getMonthNames(locale);
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
|
||
switch (style) {
|
||
case 'numeric':
|
||
// 如果提供了分隔符,返回纯数字;否则中文环境下添加"月"字
|
||
const monthStr = this.convertNumberingSystem((month + 1).toString(), numberingSystem);
|
||
return (dateSeparator != '' || langCode != 'zh') ? monthStr : `${monthStr}月`;
|
||
case '2-digit':
|
||
const paddedMonth = (month + 1).toString().padStart(2, '0');
|
||
const paddedMonthStr = this.convertNumberingSystem(paddedMonth, numberingSystem);
|
||
return (dateSeparator != '' || langCode != 'zh') ? paddedMonthStr : `${paddedMonthStr}月`;
|
||
case 'long':
|
||
return monthNames.long[month];
|
||
case 'short':
|
||
return monthNames.short[month];
|
||
case 'narrow':
|
||
return monthNames.narrow[month];
|
||
default:
|
||
const defaultMonthStr = this.convertNumberingSystem((month + 1).toString(), numberingSystem);
|
||
return (dateSeparator != '' || langCode != 'zh') ? defaultMonthStr : `${defaultMonthStr}月`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 格式化日期
|
||
*/
|
||
private formatDay(date : Date, style : string, locale : string = '', numberingSystem : string = 'latn', dateSeparator : string = '') : string {
|
||
const day = date.getDate();
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
|
||
switch (style) {
|
||
case 'numeric':
|
||
// 如果提供了分隔符,返回纯数字;否则中文环境下添加"日"字
|
||
const dayStr = this.convertNumberingSystem(day.toString(), numberingSystem);
|
||
return (dateSeparator != '' || langCode != 'zh') ? dayStr : `${dayStr}日`;
|
||
case '2-digit':
|
||
const paddedDay = day.toString().padStart(2, '0');
|
||
const paddedDayStr = this.convertNumberingSystem(paddedDay, numberingSystem);
|
||
return (dateSeparator != '' || langCode != 'zh') ? paddedDayStr : `${paddedDayStr}日`;
|
||
default:
|
||
const defaultDayStr = this.convertNumberingSystem(day.toString(), numberingSystem);
|
||
return (dateSeparator != '' || langCode != 'zh') ? defaultDayStr : `${defaultDayStr}日`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 格式化小时
|
||
*/
|
||
private formatHour(date : Date, style : string, hour12 : boolean, hourCycle : string, numberingSystem : string = 'latn') : string {
|
||
let hour = date.getHours();
|
||
|
||
if (hour12) {
|
||
if (hourCycle == 'h11') {
|
||
hour = hour % 12;
|
||
} else { // h12
|
||
hour = hour % 12;
|
||
if (hour == 0) hour = 12;
|
||
}
|
||
} else {
|
||
if (hourCycle == 'h11') {
|
||
hour = hour % 12;
|
||
} else if (hourCycle == 'h24') {
|
||
if (hour == 0) hour = 24;
|
||
}
|
||
// h23 保持原样
|
||
}
|
||
|
||
switch (style) {
|
||
case 'numeric':
|
||
return this.convertNumberingSystem(hour.toString(), numberingSystem);
|
||
case '2-digit':
|
||
return this.convertNumberingSystem(hour.toString().padStart(2, '0'), numberingSystem);
|
||
default:
|
||
return this.convertNumberingSystem(hour.toString(), numberingSystem);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 格式化分钟
|
||
*/
|
||
private formatMinute(date : Date, style : string, numberingSystem : string = 'latn') : string {
|
||
const minute = date.getMinutes();
|
||
switch (style) {
|
||
case 'numeric':
|
||
return this.convertNumberingSystem(minute.toString(), numberingSystem);
|
||
case '2-digit':
|
||
return this.convertNumberingSystem(minute.toString().padStart(2, '0'), numberingSystem);
|
||
default:
|
||
return this.convertNumberingSystem(minute.toString(), numberingSystem);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 格式化秒
|
||
*/
|
||
private formatSecond(date : Date, style : string, fractionalDigits : number, numberingSystem : string = 'latn') : string {
|
||
const second = date.getSeconds();
|
||
const millisecond = date.getMilliseconds();
|
||
|
||
let result = '';
|
||
switch (style) {
|
||
case 'numeric':
|
||
result = this.convertNumberingSystem(second.toString(), numberingSystem);
|
||
break;
|
||
case '2-digit':
|
||
result = this.convertNumberingSystem(second.toString().padStart(2, '0'), numberingSystem);
|
||
break;
|
||
default:
|
||
result = this.convertNumberingSystem(second.toString(), numberingSystem);
|
||
break;
|
||
}
|
||
|
||
// 添加小数秒
|
||
if (fractionalDigits > 0) {
|
||
const fractional = millisecond.toString().padStart(3, '0').slice(0, fractionalDigits);
|
||
const convertedFractional = this.convertNumberingSystem(fractional, numberingSystem);
|
||
result += `.${convertedFractional}`;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 格式化时段(AM/PM)
|
||
*/
|
||
private formatDayPeriod(date : Date, style : string, locale : string) : string {
|
||
const hour = date.getHours();
|
||
const isAM = hour < 12;
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
|
||
if (langCode == 'zh') {
|
||
switch (style) {
|
||
case 'long':
|
||
return isAM ? '上午' : '下午';
|
||
case 'short':
|
||
case 'narrow':
|
||
default:
|
||
return isAM ? '上午' : '下午';
|
||
}
|
||
} else {
|
||
switch (style) {
|
||
case 'long':
|
||
return isAM ? 'AM' : 'PM';
|
||
case 'short':
|
||
case 'narrow':
|
||
default:
|
||
return isAM ? 'AM' : 'PM';
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 格式化时区
|
||
*/
|
||
private formatTimeZone(date : Date, style : string, timeZone : string) : string {
|
||
// 简化实现,实际应用中可能需要更复杂的时区处理
|
||
switch (style) {
|
||
case 'long':
|
||
return timeZone == 'UTC' ? 'Coordinated Universal Time' : timeZone;
|
||
case 'short':
|
||
default:
|
||
return timeZone == 'UTC' ? 'UTC' : timeZone;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 组合日期时间各部分
|
||
*/
|
||
private combineDateTimeParts(parts : string[], locale : string, dateSeparator : string = '') : string {
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
|
||
// 如果设置了自定义分隔符,使用自定义分隔符连接
|
||
if (dateSeparator != '') {
|
||
return parts.join(dateSeparator);
|
||
}
|
||
|
||
if (langCode == 'zh') {
|
||
// 中文习惯:直接连接(年份和日期已包含单位字符)
|
||
return parts.join('');
|
||
} else {
|
||
// 英文习惯:用空格分隔
|
||
return parts.join(' ');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取星期名称
|
||
*/
|
||
private getWeekdayNames(locale : string) : DateTimeNames {
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
|
||
if (langCode == 'zh') {
|
||
return {
|
||
long: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
|
||
short: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
|
||
narrow: ['日', '一', '二', '三', '四', '五', '六']
|
||
};
|
||
} else {
|
||
return {
|
||
long: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
||
short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||
narrow: ['S', 'M', 'T', 'W', 'T', 'F', 'S']
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取月份名称
|
||
*/
|
||
private getMonthNames(locale : string) : DateTimeNames {
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
|
||
if (langCode == 'zh') {
|
||
return {
|
||
long: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
|
||
short: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
|
||
narrow: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']
|
||
};
|
||
} else {
|
||
return {
|
||
long: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
||
short: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||
narrow: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D']
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 添加千分位分隔符
|
||
* @param numStr 数字字符串
|
||
* @param locale 语言代码
|
||
* @returns 带千分位分隔符的字符串
|
||
*/
|
||
private addThousandsSeparator(numStr : string, locale : string) : string {
|
||
const parts = numStr.split('.');
|
||
const integerPart = parts[0];
|
||
const decimalPart = parts.length > 1 ? parts[1] : '';
|
||
|
||
// 根据语言选择分隔符
|
||
const separator = this.getThousandsSeparator(locale);
|
||
|
||
// 添加千分位分隔符
|
||
const formattedInteger = integerPart.replace(/(\d)(?=(\d{3})+(?!\d))/g, `$1${separator}`);
|
||
|
||
return decimalPart == '' ? `${formattedInteger}.${decimalPart}` : formattedInteger;
|
||
}
|
||
|
||
/**
|
||
* 千分位分隔符映射配置
|
||
*/
|
||
private static readonly THOUSANDS_SEPARATOR_MAP = new Map<string, string>([
|
||
// 使用逗号的语言
|
||
['en', ','], // 英语
|
||
['zh', ','], // 中文
|
||
['zh-Hant', ','], // 中文
|
||
['zh', ','], // 中文
|
||
['ja', ','], // 日语
|
||
['ko', ','], // 韩语
|
||
['pt', ','], // 葡萄牙语
|
||
['th', ','], // 泰语
|
||
['vi', ','], // 越南语
|
||
['id', ','], // 印尼语
|
||
['ms', ','], // 马来语
|
||
['tl', ','], // 菲律宾语
|
||
['he', ','], // 希伯来语
|
||
['ar', ','], // 阿拉伯语
|
||
// 使用空格的语言
|
||
['de', ' '], // 德语
|
||
['fr', ' '], // 法语
|
||
['ru', ' '], // 俄语
|
||
['sv', ' '], // 瑞典语
|
||
['no', ' '], // 挪威语
|
||
['da', ' '], // 丹麦语
|
||
['fi', ' '], // 芬兰语
|
||
['cs', ' '], // 捷克语
|
||
['sk', ' '], // 斯洛伐克语
|
||
['pl', ' '], // 波兰语
|
||
// 使用点号的语言
|
||
['es', '.'], // 西班牙语
|
||
['it', '.'], // 意大利语
|
||
['nl', '.'], // 荷兰语
|
||
['tr', '.'], // 土耳其语
|
||
// 使用撇号的语言
|
||
['ch', "'"], // 瑞士德语
|
||
]);
|
||
|
||
/**
|
||
* 获取千分位分隔符
|
||
* @param locale 语言代码
|
||
* @returns 分隔符字符
|
||
*/
|
||
private getThousandsSeparator(locale : string) : string {
|
||
// 提取语言代码前缀(如 'en-US' -> 'en')
|
||
const langCode = locale.split('-')[0].toLowerCase();
|
||
|
||
// 从映射中获取分隔符
|
||
const separator = Tmui4xI18n.THOUSANDS_SEPARATOR_MAP.get(langCode);
|
||
|
||
// 如果找到对应的分隔符则返回,否则返回默认的逗号
|
||
return separator ?? ',';
|
||
}
|
||
|
||
/**
|
||
* 格式化货币
|
||
* @param numStr 数字字符串
|
||
* @param options 格式化选项
|
||
* @param locale 语言代码
|
||
* @returns 格式化后的货币字符串
|
||
*/
|
||
private formatCurrency(numStr : string, options : NumberFormatOpts, locale : string) : string {
|
||
const currency = options.currency ?? 'USD';
|
||
const currencyDisplay = options.currencyDisplay ?? 'symbol';
|
||
|
||
let currencySymbol = currency;
|
||
if (currencyDisplay == 'symbol') {
|
||
// 全球主要经济体货币符号映射
|
||
switch (currency.toUpperCase()) {
|
||
// 美洲
|
||
case 'USD':
|
||
currencySymbol = '$';
|
||
break;
|
||
case 'CAD':
|
||
currencySymbol = 'C$';
|
||
break;
|
||
case 'BRL':
|
||
currencySymbol = 'R$';
|
||
break;
|
||
case 'MXN':
|
||
currencySymbol = '$';
|
||
break;
|
||
// 欧洲
|
||
case 'EUR':
|
||
currencySymbol = '€';
|
||
break;
|
||
case 'GBP':
|
||
currencySymbol = '£';
|
||
break;
|
||
case 'CHF':
|
||
currencySymbol = 'CHF';
|
||
break;
|
||
case 'RUB':
|
||
currencySymbol = '₽';
|
||
break;
|
||
// 亚太地区
|
||
case 'JPY':
|
||
currencySymbol = '¥';
|
||
break;
|
||
case 'CNY':
|
||
case 'RMB':
|
||
currencySymbol = '¥';
|
||
break;
|
||
case 'KRW':
|
||
currencySymbol = '₩';
|
||
break;
|
||
case 'INR':
|
||
currencySymbol = '₹';
|
||
break;
|
||
case 'AUD':
|
||
currencySymbol = 'A$';
|
||
break;
|
||
case 'SGD':
|
||
currencySymbol = 'S$';
|
||
break;
|
||
case 'HKD':
|
||
currencySymbol = 'HK$';
|
||
break;
|
||
case 'TWD':
|
||
currencySymbol = 'NT$';
|
||
break;
|
||
case 'THB':
|
||
currencySymbol = '฿';
|
||
break;
|
||
case 'MYR':
|
||
currencySymbol = 'RM';
|
||
break;
|
||
case 'VND':
|
||
currencySymbol = '₫';
|
||
break;
|
||
case 'IDR':
|
||
currencySymbol = 'Rp';
|
||
break;
|
||
case 'PHP':
|
||
currencySymbol = '₱';
|
||
break;
|
||
// 中东地区
|
||
case 'SAR':
|
||
currencySymbol = '﷼';
|
||
break;
|
||
case 'AED':
|
||
currencySymbol = 'د.إ';
|
||
break;
|
||
case 'ILS':
|
||
currencySymbol = '₪';
|
||
break;
|
||
case 'TRY':
|
||
currencySymbol = '₺';
|
||
break;
|
||
// 非洲
|
||
case 'ZAR':
|
||
currencySymbol = 'R';
|
||
break;
|
||
default:
|
||
currencySymbol = currency;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 根据货币和地区决定货币符号位置(参考国际标准)
|
||
return this.getCurrencyPosition(currencySymbol, numStr, currency, locale);
|
||
}
|
||
|
||
/**
|
||
* 获取货币符号位置
|
||
* @param symbol 货币符号
|
||
* @param numStr 数字字符串
|
||
* @param currency 货币代码
|
||
* @param locale 语言代码
|
||
* @returns 格式化后的货币字符串
|
||
*/
|
||
private getCurrencyPosition(symbol : string, numStr : string, currency : string, locale : string) : string {
|
||
const currencyCode = currency.toUpperCase();
|
||
|
||
// 符号在前面的货币(无空格)
|
||
const prefixNoSpace = ['USD', 'CAD', 'AUD', 'HKD', 'SGD', 'TWD', 'GBP', 'CNY', 'RMB', 'JPY'];
|
||
// 符号在前面的货币(有空格)
|
||
const prefixWithSpace = ['BRL', 'MXN', 'KRW', 'INR', 'THB', 'VND', 'IDR', 'PHP', 'ILS', 'TRY'];
|
||
// 符号在后面的货币(有空格)
|
||
const suffixWithSpace = ['EUR', 'CHF', 'RUB', 'MYR', 'SAR', 'AED', 'ZAR'];
|
||
|
||
if (prefixNoSpace.includes(currencyCode)) {
|
||
return `${symbol}${numStr}`;
|
||
} else if (prefixWithSpace.includes(currencyCode)) {
|
||
return `${symbol} ${numStr}`;
|
||
} else if (suffixWithSpace.includes(currencyCode)) {
|
||
return `${numStr} ${symbol}`;
|
||
} else {
|
||
// 默认根据语言决定
|
||
if (locale.startsWith('en') || locale.startsWith('zh')) {
|
||
return `${symbol}${numStr}`;
|
||
} else {
|
||
return `${numStr} ${symbol}`;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 格式化百分比
|
||
* @param numStr 数字字符串
|
||
* @param originalValue 原始数值
|
||
* @returns 格式化后的百分比字符串
|
||
*/
|
||
private formatPercent(numStr : string, originalValue : number) : string {
|
||
// 将数值乘以100并添加百分号
|
||
const percentValue = originalValue * 100;
|
||
return `${percentValue}%`;
|
||
}
|
||
/**
|
||
* 获取翻译消息
|
||
* @param key 翻译键
|
||
* @param locale 语言代码
|
||
* @returns 翻译消息或null
|
||
*/
|
||
private getMessage(key : string, locale : string) : StringOrNull {
|
||
const messages = this.ops.messages;
|
||
if (messages[locale] == null) {
|
||
return null;
|
||
}
|
||
|
||
const localeMessages = messages[locale] as UTSJSONObjectOrNull;
|
||
return this.getNestedValue(localeMessages, key);
|
||
}
|
||
|
||
/**
|
||
* 获取嵌套对象的值
|
||
* @param obj 对象
|
||
* @param path 路径,如 'user.name'
|
||
* @returns 值或null
|
||
*/
|
||
private getNestedValue(obj : UTSJSONObjectOrNull, path : string) : StringOrNull {
|
||
const keys = path.split('.');
|
||
|
||
let getAny : GetAnyTypeOrNull = null;
|
||
|
||
getAny = (objdata : AnyOrNull) : AnyOrNull => {
|
||
let realGetAny = getAny!;
|
||
let path : StringOrNull = keys.shift()
|
||
if (objdata == null) return null;
|
||
if (
|
||
typeof objdata == 'string' ||
|
||
typeof objdata == 'number'
|
||
) return objdata
|
||
if (path == null) return null;
|
||
return realGetAny((objdata! as UTSJSONObject).getAny(path));
|
||
}
|
||
|
||
let current : AnyOrNull = getAny(obj);
|
||
|
||
if (typeof current == 'number') return current.toString()
|
||
return typeof current == 'string' ? current : null;
|
||
}
|
||
|
||
/**
|
||
* 处理复数形式
|
||
* @param message 包含复数的消息
|
||
* @param count 数量
|
||
* @param locale 语言代码
|
||
* @returns 选择的复数形式
|
||
*/
|
||
private handlePlural(message : string, count : number, locale : string) : string {
|
||
const choices = message.split('|').map((choice : string) : string => choice.trim());
|
||
const choicesLength = choices.length;
|
||
|
||
// 获取复数规则
|
||
let pluralRule = this.ops.pluralRule;
|
||
if (this.ops.pluralRules.has(locale)) {
|
||
pluralRule = this.ops.pluralRules.get(locale)!;
|
||
}
|
||
|
||
// 应用复数规则
|
||
let index = 0;
|
||
if (pluralRule != null) {
|
||
// 第有理数无法类型嵌套,暂不支持设置为null
|
||
index = pluralRule(count, choicesLength, null);
|
||
}
|
||
|
||
// 确保索引在有效范围内
|
||
const safeIndex = Math.max(0, Math.min(index, choicesLength - 1));
|
||
let result = choices[safeIndex]
|
||
if (result == '' || result == null) {
|
||
result = choices[0]
|
||
}
|
||
if (result == '' || result == null) {
|
||
result = message
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 字符串插值处理
|
||
* @param message 消息模板
|
||
* @param values 插值参数
|
||
* @returns 插值后的字符串
|
||
*/
|
||
private interpolate(message : string, values : UTSJSONObjectOrArray) : string {
|
||
let msg = message;
|
||
if (Array.isArray(values)) {
|
||
// 处理位置插值 {0}, {1}, {2}...
|
||
// #ifndef APP-ANDROID
|
||
msg = message.replace(/\{(\d+)\}/g, (match : string, key : string, index : number) : string => {
|
||
const value = values[parseInt(key)];
|
||
|
||
if (value != null) {
|
||
let result = typeof value == 'number' ? value.toString() : value as string
|
||
return result;
|
||
}
|
||
return match; // 保持原样如果没有找到值
|
||
});
|
||
// #endif
|
||
// #ifdef APP-ANDROID
|
||
msg = message.replace(/\{(\d+)\}/g, (match : string, key : string, offset : number, str : string) : string => {
|
||
const value = values[parseInt(key)];
|
||
if (value != null) {
|
||
let result = typeof value == 'number' ? value.toString() : value as string
|
||
return result;
|
||
}
|
||
return match; // 保持原样如果没有找到值
|
||
});
|
||
// #endif
|
||
} else if (values instanceof UTSJSONObject) {
|
||
if (UTSJSONObject.keys(values).length == 0) {
|
||
return message;
|
||
}
|
||
// 处理命名插值 {name}比如{count},{n}等
|
||
// #ifndef APP-ANDROID
|
||
msg = message.replace(/\{([^}]+)\}/g, (match : string, key : string, index : number) : string => {
|
||
const value = values[key];
|
||
if (value != null) {
|
||
return value as string;
|
||
}
|
||
return match; // 保持原样如果没有找到值
|
||
});
|
||
// #endif
|
||
// #ifdef APP-ANDROID
|
||
msg = message.replace(/\{([^}]+)\}/g, (match : string, key : string, offset : number, str : string) : string => {
|
||
const value = values[key];
|
||
if (value != null) {
|
||
if (typeof value == 'string') return value;
|
||
if (typeof value == 'number') return value.toString();
|
||
|
||
}
|
||
return match; // 保持原样如果没有找到值
|
||
});
|
||
// #endif
|
||
}
|
||
|
||
|
||
|
||
return msg;
|
||
}
|
||
} |