Files
akmon/uni_modules/ak-i18n/common/composer.uts
2026-01-20 08:04:15 +08:00

642 lines
21 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import BaseFormatter from './format'
import { warn, error, isString, getAllKeys } from './util'
import { Composer, Interpolate, Link, WarnDefault, LinkedModify, PluralizationRule, StringOrNull, NumberOrNull, Availabilities } from './types'
// #ifndef APP
type Interceptor = any
// #endif
export class AvailabilitiesImpl implements Availabilities {
dateTimeFormat : boolean = false
numberFormat : boolean = false
constructor() {
// #ifndef APP
const intlDefined = typeof Intl !== 'undefined'
this.dateTimeFormat = intlDefined && typeof Intl.DateTimeFormat !== 'undefined'
this.numberFormat = intlDefined && typeof Intl.NumberFormat !== 'undefined'
// #endif
}
}
const htmlTagMatcher = /<\/?[\w\s="/.':;#-\/]+>/;
const linkKeyMatcher = /(?:@(?:\.[a-zA-Z0-9_-]+)?:)(?:[\w\-_|./]+|\([\w\-_:|./]+\)|(?:\{[^}]+?\}))/g;
const linkKeyPrefixMatcher = /^@(?:\.([a-zA-Z]+))?:/;
const bracketsMatcher = /[()\{\}\']/g;
const defaultModifiers : Map<string, LinkedModify> = new Map([
['upper', (str : string) : string => str.toLocaleUpperCase()],
['lower', (str : string) : string => str.toLocaleLowerCase()],
['capitalize', (str : string) : string => `${str.charAt(0).toLocaleUpperCase()}${str.substring(1)}`]
])
const DEFAULT_LOCALE = "en-US"
const defaultFormatter = new BaseFormatter()
const availabilities = new AvailabilitiesImpl()
function setTabBarItems(tabbar : string[] | null) {
if (tabbar == null) return
const pages = getCurrentPages()
const page = pages.length > 0 ? pages[pages.length - 1]: null;
// @ts-ignore
// #ifndef APP-ANDROID
const isTabBar = page != null //page.$vm.$basePage.openType == 'switchTab'// page != null && page.$page.meta.isTabBar
// #endif
// #ifdef APP-ANDROID
const isTabBar = page != null
// #endif
if(!isTabBar) return
tabbar.forEach((text, index) => {
uni.setTabBarItem({
text,
index,
// success() {},
fail(err) {
warn(err.errMsg)
}
} as SetTabBarItemOptions)
})
}
function getLocaleMap(locale : string, key : string, options : UTSJSONObject, root : Composer | null = null) : Map<string, UTSJSONObject> {
//'messages'
const __messages = UTSJSONObject.assign({}, options.getJSON(key) ?? {})
// #ifdef APP
let map = new Map<string, UTSJSONObject>()
__messages.toMap().forEach((value, key) => {
if (value instanceof UTSJSONObject) {
map.set(key, value)
}
})
// #endif
// #ifndef APP
let map = __messages.toMap()
// #endif
if (map.size == 0 && root != null) {
// map = root.messages.value
if (!map.has(locale)) {
map.set(locale, {})
}
}
return map
}
function getLocaleTabbarMap(locale : string, key : string, options : UTSJSONObject) : Map<string, string[]> {
const __messages = options.getJSON(key) ?? {}
let map = new Map<string, string[]>()
__messages.toMap().forEach((tabbar, key) => {
if (Array.isArray(tabbar)) {
map.set(key, tabbar as string[]);
if (key == locale) {
setTimeout(()=>{
setTabBarItems(tabbar as string[])
},500)
}
}
})
return map
}
function getModifiers(options : UTSJSONObject) : Map<string, LinkedModify> {
const __modifiers = (options.getJSON('modifiers') ?? {}).toMap();
const _modifiers = new Map<string, LinkedModify>()
__modifiers.forEach((value, key) => {
if (typeof value == 'function') {
try {
_modifiers.set(key, value as LinkedModify)
} catch (e) {
error(35, '自定义修饰器函数必须是类型:(str: string) => string')
}
}
})
return _modifiers
}
function getPluralizationRules(options : UTSJSONObject) : Map<string, PluralizationRule> {
const __pluralizationRules = (options.getJSON('pluralizationRules') ?? {}).toMap()
const _pluralizationRules = new Map<string, PluralizationRule>()
__pluralizationRules.forEach((value, key) => {
if (typeof value == 'function') {
try {
_pluralizationRules.set(key, value as PluralizationRule)
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
error(35, '自定义复数化规则函数必须是类型: ( choice: number, choicesLength: number) => number')
}
}
}
})
return _pluralizationRules
}
function getFormatter(options : UTSJSONObject) : BaseFormatter {
const __formatter = options.get('formatter')
return __formatter != null && __formatter instanceof BaseFormatter ? __formatter : defaultFormatter;
}
let composerID = 0;
/**
* 创建一个Composer实例用于处理国际化信息。
* @param {UTSJSONObject} [options={}] - 配置对象,包含语言环境、格式化器等设置。
* @param {Composer | null} [__root=null] - 根Composer实例用于继承语言环境等信息。
* @returns {Composer} 返回一个新的Composer实例。
*/
export function createComposer(options : UTSJSONObject = {}, __root : Composer | null = null) : Composer {
let _interpolate : Interpolate | null = null;
let _link : Link | null;
let _warnDefault : WarnDefault | null = null;
const _inheritLocale = options.getBoolean('inheritLocale') ?? true;
const _formatter = getFormatter(options);
const _modifiers = getModifiers(options)
const _pluralizationRules = getPluralizationRules(options)
// const flatJson = options.getBoolean('flatJson') ?? false;
const useRoot = __root != null && _inheritLocale
const __locale = ref<string>(
useRoot
? __root!.locale.value
: options.getString('locale') ?? DEFAULT_LOCALE
)
const _fallbackLocale = ref<any | null>(
useRoot
? __root!.fallbackLocale.value
: options.get('fallbackLocale')
)
const _messages = ref<Map<string, UTSJSONObject>>(getLocaleMap(__locale.value, 'messages', options, __root))
const _numberFormats = ref<Map<string, UTSJSONObject>>(getLocaleMap(__locale.value, 'numberFormats', options, __root))
const _datetimeFormats = ref<Map<string, UTSJSONObject>>(getLocaleMap(__locale.value, 'datetimeFormats', options, __root))
const _tabBars = ref<Map<string, string[]>>(getLocaleTabbarMap(__locale.value, 'tabBars', options))
const _locale = computed<string>({
set(val : string) {
__locale.value = val;
// 设置缓存 只有全局才会缓存
if (__root == null) {
uni.setStorageSync('uVueI18nLocale', val)
}
// 设置tabbar
setTabBarItems(_tabBars.value.get(val))
},
get() : string {
return __locale.value
}
} as WritableComputedOptions<string>)
const fallbackLocale = computed<any>({
set(val : any) {
_fallbackLocale.value = val
},
get() : any {
return _fallbackLocale.value ?? false
}
} as WritableComputedOptions<any>)
let availableLocales : string[] = getAllKeys(_messages.value).sort()
/**
* 处理字符串中的链接并返回翻译后的字符串。
* @param {string} str - 要处理的字符串。
* @param {StringOrNull} [locale=null] - 指定语言环境。
* @param {any} values - 用于插值的变量。
* @param {string[]} visitedLinkStack - 已访问过的链接堆栈。
* @param {string} interpolateMode - 插值模式。
* @returns {StringOrNull} 返回处理后的字符串或null。
*/
_link = (str : string, locale : StringOrNull, values : any, visitedLinkStack : string[], interpolateMode : string) : StringOrNull => {
const matches = str.match(linkKeyMatcher)
let ret : string = str
if (matches == null) return str
for (let i = 0; i < matches.length; i++) {
const link = matches[i]
const linkKeyPrefixMatches = link!.match(linkKeyPrefixMatcher)
if (linkKeyPrefixMatches == null) continue;
const [linkPrefix, formatterName] = linkKeyPrefixMatches
// 去掉字符串前面的 @:、@.case: 、括号及大括号
const linkPlaceholder : string = link.replace(linkPrefix!, '').replace(bracketsMatcher, '')
if (visitedLinkStack.includes(linkPlaceholder)) {
warn(`发现循环引用。"${link}"已经在link"已经在${visitedLinkStack.reverse().join(' <- ')}链中访问过`)
return ret
}
if (_interpolate == null || _warnDefault == null) {
return ret
}
visitedLinkStack.push(linkPlaceholder)
let translated = _interpolate!(linkPlaceholder, locale, values, visitedLinkStack, interpolateMode)
translated = _warnDefault!(linkPlaceholder, translated, values, interpolateMode)
// 如果有自定义_modifiers 否则使用默认defaultModifiers
if (_modifiers.size > 0 && formatterName != null && _modifiers.has(formatterName)) {
} else if (translated != null && formatterName != null && defaultModifiers.has(formatterName)) {
const modifier = defaultModifiers.get(formatterName) as LinkedModify
translated = modifier(translated)
}
visitedLinkStack.pop()
// 将链接替换为已翻译的
ret = translated == null ? ret : ret.replace(link, translated)
}
return ret
}
/**
* 获取指定语言字符。
* @param {string} key - 要翻译的键。
* @param {StringOrNull} [locale=null] - 指定语言环境。
* @param {any} values - 用于插值的变量。
* @param {string[]} visitedLinkStack - 已访问过的链接堆栈。
* @param {string} interpolateMode - 插值模式。
* @returns {StringOrNull} 返回翻译后的字符串或null。
*/
_interpolate = (key : string, locale : StringOrNull, values : any, visitedLinkStack : string[], interpolateMode : string) : StringOrNull => {
const ___locale = locale ?? _locale.value
let ret = _messages.value.get(___locale)?.getString(key)
// console.log(_messages)
// console.log(key,ret)
if (fallbackLocale.value != false && ret == null) {
if (typeof fallbackLocale.value == 'string' && ___locale != fallbackLocale.value) {
ret = _messages.value.get(fallbackLocale.value as string)?.getString(key) ?? ret
} else if (Array.isArray(fallbackLocale.value)) {
const arr = (fallbackLocale.value as string[])
for (let i = 0; i < arr.length; i++) {
const _ret = _messages.value.get(arr[i])?.getString(key)
if (_ret != null) {
ret = _ret
break;
}
}
}
}
// 检查翻译后的字符串中是否存在链接
if (typeof ret == 'string' && (ret!.indexOf('@:') >= 0 || ret!.indexOf('@.') >= 0)) {
// @ts-ignore
ret = _link(ret!, locale, values, visitedLinkStack, interpolateMode)
}
return ret
}
/**
* 获取指定语言字符并渲染。
* @param {string} message - 要翻译的字符串。
* @param {any} values - 用于插值的变量。
* @param {string} interpolateMode - 插值模式。
* @returns {string} 返回渲染后的字符串。
*/
const _render = (message : string, values : any, interpolateMode : string) : string => {
const ret = _formatter.interpolate(message, values)
return interpolateMode == 'string' ? `${ret.join('')}` : JSON.stringify(ret)
}
/**
* 在无法翻译的情况下发出警告并提供默认值。
* @param {string} key - 要翻译的键。
* @param {StringOrNull} message - 翻译后的字符串或null。
* @param {any} values - 用于插值的变量。
* @param {string} interpolateMode - 插值模式。
* @returns {StringOrNull} 返回警告信息或默认值。
*/
_warnDefault = (key : string, message : StringOrNull, values : any, interpolateMode : string) : StringOrNull => {
if (message == null) {
warn(`无法翻译键路径 '${key}'. ` + '使用键路径的值作为默认值.')
}
if (message == null) return null
if (key == message) return key
return _render(message, values, interpolateMode)
}
/**
* 获取复数形式的选择。
* @param {string} message - 包含复数选择的字符串。
* @param {number | null} [choice=null] - 复数形式的选择。
* @param {string | null} [locale=null] - 指定语言环境。
* @returns {string} 返回选择后的字符串。
*/
const fetchChoice = (message : string, choice ?: number, locale ?: string) : string => {
if (message == '') return message;
const choices : Array<string> = message.split('|');
// 默认 vue-i18ngetChoiceIndex实现 - 兼容英文
const defaultImpl = (_choice : NumberOrNull, _choicesLength : number) : number => {
_choice = Math.abs(_choice ?? 1)
if (_choicesLength == 2) {
return _choice != 0
? _choice > 1
? 1
: 0
: 1
}
return _choice != 0 ? Math.min(_choice, 2) : 0
}
let index : number;
if (_pluralizationRules.has(locale ?? _locale.value)) {
index = _pluralizationRules.get(locale ?? _locale.value)!(choice ?? 1, choices.length)
} else {
index = defaultImpl(choice, choices.length)
}
if (choices[index] == '') return message
return choices[index].trim()
}
/**
* 翻译指定的键。
* @param {string} key - 要翻译的键。
* @param {any} [values=null] - 用于插值的变量。
* @param {string | null} [locale=null] - 指定语言环境。
* @returns {string} 返回翻译后的字符串。
*/
const t = (key : string, values ?: any, locale ?: string) : string => {
const parsedArgs = values ?? {}
// #ifndef APP
if (_warnDefault == null || _interpolate == null) return ''
// #endif
const msg = _warnDefault(
key,
_interpolate(
key,
locale,
parsedArgs,
[key],
'string'),
parsedArgs,
'string'
)
return msg ?? ''
}
/**
* 翻译指定的键并获取复数形式的选择。
* @param {string} key - 要翻译的键。
* @param {number | null} [choice=null] - 复数形式的选择。
* @param {any} [values=null] - 用于插值的变量。
* @param {string | null} [locale=null] - 指定语言环境。
* @returns {string} 返回翻译后的复数形式选择字符串。
*/
const tc = (key : string, choice ?: number, values ?: any, locale ?: string) : string => {
// 预定义的count和n参数
const _obj = { 'count': choice, 'n': choice }
const predefined = values == null
? _obj
: values instanceof UTSJSONObject
? UTSJSONObject.assign(_obj, values as UTSJSONObject)
: values;
return fetchChoice(t(key, predefined, locale), choice, locale)
}
/**
* 格式化日期。
* @param {any} date - 要格式化的日期。
* @param {StringOrNull} [key=null] - 日期格式化的键。
* @param {StringOrNull} [locale=null] - 指定语言环境。
* @param {UTSJSONObject | null} [options=null] - 日期格式化的选项。
* @returns {string} 返回格式化后的日期字符串。
*/
const d = (date : any, key : StringOrNull, locale : StringOrNull, options : UTSJSONObject | null) : string => {
if (!availabilities.dateTimeFormat) {
warn('无法格式化日期值,因为不支持 Intl.DateTimeFormat. ' + `key: ${key}, locale: ${locale}, options: ${options}`)
return `${date}`
}
// #ifndef APP
const __locale = locale ?? _locale.value
if (key == null) {
// @ts-ignore
const dtf = options == null ? new Intl.DateTimeFormat(__locale) : new Intl.DateTimeFormat(__locale, options)
return dtf.format(date)
}
const formats = _datetimeFormats.value!.get(__locale)
let formatter;
if (formats == null || formats!.getJSON(key) == null) {
warn(`回退到根号下的日期时间本地化key '${key}'。`)
return `${date}`
}
const format = formats!.getJSON(key) ?? {}
if (options != null) {
// @ts-ignore
formatter = new Intl.DateTimeFormat(__locale, Object.assign({}, format, options))
} else {
// @ts-ignore
formatter = new Intl.DateTimeFormat(__locale, format)
}
return formatter.format(date)
// #endif
return `${date}`
}
/**
* 格式化数字。
* @param {number} number - 要格式化的数字。
* @param {StringOrNull} [key=null] - 数字格式化的键。
* @param {StringOrNull} [locale=null] - 指定语言环境。
* @param {UTSJSONObject | null} [options=null] - 数字格式化的选项。
* @returns {string} 返回格式化后的数字字符串。
*/
const n = (number : number, key : StringOrNull, locale : StringOrNull, options : UTSJSONObject | null) : string => {
if (!availabilities.numberFormat) {
warn('无法格式化数字值,因为不支持 Intl.NumberFormat. ' + `key: ${key}, locale: ${locale}, options: ${options}`)
return number.toString()
}
// #ifndef APP
const __locale = locale ?? _locale.value
if (key == null) {
// @ts-ignore
const nf = options == null ? new Intl.NumberFormat(__locale) : new Intl.NumberFormat(locale, options)
return nf.format(number)
}
const formats = _numberFormats.value!.get(__locale)
let formatter;
if (formats == null || formats!.getJSON(key) == null) {
warn(`回退到根号下的数字本地化key '${key}'`)
return number.toString()
}
const format = formats!.getJSON(key)
if (options != null) {
// @ts-ignore
formatter = new Intl.NumberFormat(__locale, Object.assign({}, format, options))
} else {
// @ts-ignore
formatter = new Intl.NumberFormat(__locale, format)
}
if (formatter) {
return formatter.format(number)
}
// #endif
return number.toString()
}
/**
* 设置语言环境的locale信息。
* @param {string} locale - 语言。
* @param {UTSJSONObject} message - locale信息。
*/
const setLocaleMessage = (locale : string, message : UTSJSONObject) => {
const map = new Map<string, UTSJSONObject>();
_messages.value.forEach((value, key) => {
map.set(key, value)
})
map.set(locale, message)
_messages.value = map
availableLocales = getAllKeys(map).sort()
}
/**
* 获取语言环境的locale信息。
* @param {string} locale - 语言。
* @returns {UTSJSONObject} - locale信息。
*/
const getLocaleMessage = (locale : string) : UTSJSONObject => {
return _messages.value.get(locale) ?? {}
}
/**
* 将语言环境信息locale合并到已注册的语言环境信息中。
* @param {string} locale - 语言。
* @param {UTSJSONObject} message - locale信息。
*/
const mergeLocaleMessage = (locale : string, message : UTSJSONObject) => {
const map = new Map<string, UTSJSONObject>();
_messages.value.forEach((value, key) => {
if (key == locale) {
map.set(key, UTSJSONObject.assign({}, value, message))
} else {
map.set(key, value)
}
})
_messages.value = map
availableLocales = getAllKeys(map).sort()
}
/**
* 设置日期时间格式。
* @param {string} locale - 语言。
* @param {UTSJSONObject} format - 日期时间格式。
*/
const setDateTimeFormat = (locale : string, format : UTSJSONObject) => {
const map = new Map<string, UTSJSONObject>();
_datetimeFormats.value.forEach((value, key) => {
map.set(key, value)
})
map.set(locale, format)
_datetimeFormats.value = map
}
/**
* 获取日期时间格式。
* @param {string} locale - 语言。
* @returns {UTSJSONObject} - 日期时间格式。
*/
const getDateTimeFormat = (locale : string) : UTSJSONObject => {
return _datetimeFormats.value.get(locale) ?? {}
}
/**
* 合并日期时间格式到已注册的日期时间格式中。
* @param {string} locale - 语言。
* @param {UTSJSONObject} format - 日期时间格式。
*/
const mergeDateTimeFormat = (locale : string, format : UTSJSONObject) => {
const map = new Map<string, UTSJSONObject>();
_datetimeFormats.value.forEach((value, key) => {
if (key == locale) {
map.set(key, UTSJSONObject.assign({}, value, format))
} else {
map.set(key, value)
}
})
_datetimeFormats.value = map
}
/**
* 设置数字格式。
* @param {string} locale - 语言。
* @param {UTSJSONObject} format - 数字格式。
*/
const setNumberFormat = (locale : string, format : UTSJSONObject) => {
const map = new Map<string, UTSJSONObject>();
_numberFormats.value.forEach((value, key) => {
map.set(key, value)
})
map.set(locale, format)
_numberFormats.value = map
}
/**
* 获取数字格式。
* @param {string} locale - 语言。
* @returns {UTSJSONObject} - 数字格式。
*/
const getNumberFormat = (locale : string) : UTSJSONObject => {
return _numberFormats.value.get(locale) ?? {}
}
/**
* 合并数字格式到已注册的数字格式中。
* @param {string} locale - 语言。
* @param {UTSJSONObject} format - 数字格式。
*/
const mergeNumberFormat = (locale : string, format : UTSJSONObject) => {
const map = new Map<string, UTSJSONObject>();
_numberFormats.value.forEach((value, key) => {
if (key == locale) {
map.set(key, UTSJSONObject.assign({}, value, format))
} else {
map.set(key, value)
}
})
_numberFormats.value = map
}
/**
* 设置TabBar。
* @param {string} locale - 语言。
* @param {string[]} tabbar - TabBar项目。
*/
const setTabBar = (locale : string, tabbar : string[]) => {
const map = new Map<string, string[]>();
_tabBars.value.forEach((value, key) => {
map.set(key, value)
})
map.set(locale, tabbar)
_tabBars.value = map
}
/**
* 获取TabBar。
* @param {string} locale - 语言。
* @returns {string[]} - TabBar项目。
*/
const getTabBar = (locale : string) : string[] => {
return _tabBars.value.get(locale) ?? []
}
composerID++;
const interceptor = {
complete: (_ : NavigateToComplete) => {
setTimeout(()=>{
setTabBarItems(_tabBars.value.get(_locale.value))
},50)
}
} as Interceptor
if(__root == null) {
uni.addInterceptor('switchTab', interceptor);
}
const composer : Composer = {
id: composerID,
locale: _locale,
fallbackLocale,
messages: _messages,
setLocaleMessage,
getLocaleMessage,
mergeLocaleMessage,
setDateTimeFormat,
getDateTimeFormat,
mergeDateTimeFormat,
setNumberFormat,
getNumberFormat,
mergeNumberFormat,
setTabBar,
getTabBar,
t,
tc,
d,
n,
availableLocales,
availabilities
}
return composer
}