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 = 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 { //'messages' const __messages = UTSJSONObject.assign({}, options.getJSON(key) ?? {}) // #ifdef APP let map = new Map() __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 { const __messages = options.getJSON(key) ?? {} let map = new Map() __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 { const __modifiers = (options.getJSON('modifiers') ?? {}).toMap(); const _modifiers = new Map() __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 { const __pluralizationRules = (options.getJSON('pluralizationRules') ?? {}).toMap() const _pluralizationRules = new Map() __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( useRoot ? __root!.locale.value : options.getString('locale') ?? DEFAULT_LOCALE ) const _fallbackLocale = ref( useRoot ? __root!.fallbackLocale.value : options.get('fallbackLocale') ) const _messages = ref>(getLocaleMap(__locale.value, 'messages', options, __root)) const _numberFormats = ref>(getLocaleMap(__locale.value, 'numberFormats', options, __root)) const _datetimeFormats = ref>(getLocaleMap(__locale.value, 'datetimeFormats', options, __root)) const _tabBars = ref>(getLocaleTabbarMap(__locale.value, 'tabBars', options)) const _locale = computed({ 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) const fallbackLocale = computed({ set(val : any) { _fallbackLocale.value = val }, get() : any { return _fallbackLocale.value ?? false } } as WritableComputedOptions) 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 = message.split('|'); // 默认 vue-i18n(旧)getChoiceIndex实现 - 兼容英文 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(); _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(); _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(); _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(); _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(); _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(); _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(); _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 }