Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,642 @@
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
}

View File

@@ -0,0 +1,60 @@
type I18nErrorCodesTypes = {
UNEXPECTED_RETURN_TYPE: number
INVALID_ARGUMENT: number
MUST_BE_CALL_SETUP_TOP: number
NOT_INSTALLED: number
REQUIRED_VALUE: number
INVALID_VALUE: number
CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN: number
NOT_INSTALLED_WITH_PROVIDE: number
UNEXPECTED_ERROR: number
NOT_COMPATIBLE_LEGACY_VUE_I18N: number
NOT_AVAILABLE_COMPOSITION_IN_LEGACY: number
TYPE_MISMATCH: number
}
export const I18nErrorCodes: I18nErrorCodesTypes = {
// composer模块错误
UNEXPECTED_RETURN_TYPE: 24,
// legacy模块错误
INVALID_ARGUMENT: 25,
// i18n模块错误
MUST_BE_CALL_SETUP_TOP: 26,
NOT_INSTALLED: 27,
// directive模块错误
REQUIRED_VALUE: 28,
INVALID_VALUE: 29,
// vue-devtools错误
CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN: 30,
NOT_INSTALLED_WITH_PROVIDE: 31,
// 意外错误
UNEXPECTED_ERROR: 32,
// 不兼容的旧版vue-i18n构造函数
NOT_COMPATIBLE_LEGACY_VUE_I18N: 33,
// 在旧版API模式下Compostion API不可用。请确保旧版API模式正常工作
NOT_AVAILABLE_COMPOSITION_IN_LEGACY: 34,
// 类型不匹配
TYPE_MISMATCH: 35
}
export const errorMessages: Map<number, string> = new Map<number, string>([
[I18nErrorCodes.UNEXPECTED_RETURN_TYPE, 'composer中返回类型异常'],
[I18nErrorCodes.INVALID_ARGUMENT, '参数无效'],
[I18nErrorCodes.MUST_BE_CALL_SETUP_TOP, '必须在`setup`函数的顶部调用'],
[I18nErrorCodes.NOT_INSTALLED, '需要用`app.use`函数安装'],
[I18nErrorCodes.UNEXPECTED_ERROR, '意外错误'],
[I18nErrorCodes.REQUIRED_VALUE, `值中必需,{0}`],
[I18nErrorCodes.INVALID_VALUE, `值无效`],
[I18nErrorCodes.CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN, `无法设置vue-devtools插件`],
[I18nErrorCodes.NOT_INSTALLED_WITH_PROVIDE, '需要用`provide`函数安装'],
[I18nErrorCodes.NOT_COMPATIBLE_LEGACY_VUE_I18N, '不兼容的旧版VueI18n。'],
[I18nErrorCodes.NOT_AVAILABLE_COMPOSITION_IN_LEGACY, '在旧版API模式下Compostion API不可用。请确保旧版API模式正常工作'],
[I18nErrorCodes.TYPE_MISMATCH, '类型不匹配']
])
// export function createI18nError(code: number, msg?: string) {
// if(process.env.NODE_ENV !== 'production') {
// console.warn(`[vue-i18n] : ${msg ?? errorMessages.get(code)}`)
// }
// new Error(errorMessages.get(code) ?? 'code error')
// }

View File

@@ -0,0 +1,149 @@
// @ts-nocheck
import { warn, isObject } from './util'
type Token = {
type : 'text' | 'named' | 'list' | 'unknown',
value : string
}
const RE_TOKEN_LIST_VALUE = /^(?:\d)+/
const RE_TOKEN_NAMED_VALUE = /^(?:\w)+/
/**
* 解析格式化字符串并生成一个包含标记Token的数组。
* 这些标记可以是文本、列表或命名值。
*
* @param {string} format - 需要解析的格式化字符串。
* @returns {Array<Token>} 返回一个包含解析后的标记的数组。
*/
export function parse(format : string) : Array<Token> {
const tokens : Array<Token> = []
let position : number = 0
let text : string = ''
while (position < format.length) {
let char : string = format.charAt(position++)
if (char == '{') {
if (text.length > 0) {
const token : Token = { type: 'text', value: text }
tokens.push(token)
}
text = ''
let sub : string = ''
char = format.charAt(position++)
while (char != '}') {
sub += char
char = format.charAt(position++)
}
const isClosed = char == '}'
const type = RE_TOKEN_LIST_VALUE.test(sub)
? 'list'
: isClosed && RE_TOKEN_NAMED_VALUE.test(sub)
? 'named'
: 'unknown'
const token : Token = { type, value: sub }
tokens.push(token)
} else if (char == '%') {
// when found rails i18n syntax, skip text capture
if (format.charAt(position) != '{') {
text += char
}
} else {
text += char
}
}
if (text.length > 0) {
const token : Token = { type: 'text', value: text }
tokens.push(token)
}
return tokens
}
/**
* 根据给定的标记数组和值对象或数组,编译出相应的值数组。
*
* @param {Array<Token>} tokens - 标记数组,包含文本、列表和命名值。
* @param {Object | Array<any>} values - 值对象或数组,用于替换标记中的占位符。
* @returns {Array<any>} 返回编译后的值数组。
*/
function compile(tokens : Array<Token>, values : UTSJSONObject) : Array<any>
function compile(tokens : Array<Token>, values : Array<any>) : Array<any>
function compile(tokens : Array<Token>, values : any) : Array<any> {
const compiled : Array<any> = []
let index : number = 0;
const mode : string = Array.isArray(values)
? 'list'
: isObject(values)
? 'named'
: 'unknown'
if (mode == 'unknown') {
return compiled
}
while (index < tokens.length) {
const token : Token = tokens[index]
switch (token.type) {
case 'text':
compiled.push(token.value)
break
case 'list':
const index = parseInt(token.value, 10)
if(mode == 'list') {
const value = (values as any[])[index]
compiled.push(value)
} else {
if (process.env.NODE_ENV !== 'production') {
warn('list did not receive a valid values array')
}
}
break
case 'named':
if (mode == 'named') {
const value = (values as UTSJSONObject)[token.value] ?? ''
compiled.push(`${value}`)
} else {
if (process.env.NODE_ENV !== 'production') {
warn(`Type of token '${token.type}' and format of value '${mode}' don't match!`)
}
}
break
case 'unknown':
if(token.value.startsWith("'") && token.value.endsWith("'")) {
compiled.push(token.value.slice(1, -1))
} else if (process.env.NODE_ENV !== 'production') {
warn(`Detect 'unknown' type of token!`)
}
break
}
index++
}
return compiled
}
export {compile}
export default class BaseFormatter {
private _caches : Map<string, Token[]>
constructor() {
this._caches = new Map<string, Token[]>()
}
interpolate(message : string, values : any | null) : any[] {
if (values == null) {
return [message]
}
let tokens : Array<Token> | null = this._caches.get(message)
if (tokens == null) {
tokens = parse(message)
this._caches.set(message, tokens)
}
return compile(tokens, values)
}
}

View File

@@ -0,0 +1,79 @@
import { createComposer } from './composer'
import { error, warn, getAllKeys } from './util'
import { I18nErrorCodes } from './errors'
import { AnyOrNull, NumberOrNull, StringOrNull, Composer } from './types'
type I18nMode = "legacy" | "composition"
// #ifndef APP
type VuePlugin = any
// #endif
let i18n : UvueI18n | null = null
class UvueI18n {
private __global : Composer
private __scope : EffectScope
constructor(options : UTSJSONObject = {}, root : Composer | null = null) {
this.__scope = effectScope()
this.__global = this.__scope.run(() : Composer => createComposer(UTSJSONObject.assign({}, options), root))!
}
get mode() : I18nMode {
return "composition"
}
get global() : Composer {
return this.__global
}
get availableLocales():string[] {
return getAllKeys(this.global.messages.value).sort()
}
dispose() {
this.__scope.stop()
}
get install() : VuePlugin {
const _install = (app : VueApp) => {
app.config.globalProperties.$i18n = i18n!
app.config.globalProperties.$t = function (key : string, values : AnyOrNull = null, locale : StringOrNull = null) : string {
const isLocale = typeof values == 'string'
const _values = isLocale ? null : values
const _locale = isLocale ? values as string : locale
return i18n!.global.t(key, _values, _locale)
}
app.config.globalProperties.$tc = function (key : string, choice : NumberOrNull = null, values : AnyOrNull = null, locale : StringOrNull = null) : string {
const isLocale = typeof values == 'string'
const _values = isLocale ? null : values
const _locale = isLocale ? values as string : locale
return i18n!.global.tc(key, choice, _values, _locale)
}
app.config.globalProperties.$d = function(date: any, key: StringOrNull = null, locale : StringOrNull = null, options: UTSJSONObject | null = null):string {
return i18n!.global.d(date, key, locale, options)
}
app.config.globalProperties.$n = function(number: number, key: StringOrNull = null, locale : AnyOrNull = null, options: UTSJSONObject | null = null):string {
const _locale = typeof locale == 'string' ? locale as string : null
const _options = typeof locale == 'object' && locale != null ? locale as UTSJSONObject : options
return i18n!.global.n(number, key, _locale, _options)
}
app.config.globalProperties.$locale = i18n!.global.locale
}
// #ifdef APP-ANDROID
return definePlugin({
install: _install
})
// #endif
// #ifndef APP-ANDROID
return _install
// #endif
}
}
export function createI18n(options : UTSJSONObject = {}) : UvueI18n {
// const __legacyMode = true
i18n = new UvueI18n(options)
return i18n!
}
export function useI18n(options : UTSJSONObject = {}) : Composer {
const instance = getCurrentInstance()
if (instance == null) {
error(I18nErrorCodes.MUST_BE_CALL_SETUP_TOP)
}
return new UvueI18n(options, i18n!.global).global
}

View File

@@ -0,0 +1,29 @@
console.log('i18n format test:::::::::::::::::::')
import { parse, compile } from '../format'
function appTest() {
// 示例1文本插值
const tokens = parse('Hello, {name}!')
const values = { name: 'Alice' }
console.log('tokens app', tokens)
console.log('compile app', compile(tokens, values)) // 输出:['Hello, ', 'Alice', '!']
// 示例2列表插值
const tokens2 = parse('The {0}st person is {1}.')
const values2 = ['first', 'Alice']
console.log('tokens2 app', tokens2)
console.log('compile2 app', compile(tokens2, values2))
// 示例3混合插值
const tokens3 = parse('The {0}st person is {name}.')
const values3 = ['first', { name: 'Alice' }]
console.log('tokens3 app', tokens3)
console.log('compile2 app',compile(tokens3, values3)) // 输出:['The ', 'first', 'st person is ', 'Alice', '.']
// 示例4未知类型
const tokens4 = parse('Hello, {unknown}!')
const values4 = { name: 'Alice' }
console.log('tokens4 web', tokens4)
console.log('compile4 web',compile(tokens4, values4)) // 输出:['Hello, ', 'unknown', '!']
}
appTest()

View File

@@ -0,0 +1,3 @@
// import * as utils from './utils.uts';
// import * as format from './format.uts';
console.log('i18n test:::::::::::::::::::')

View File

@@ -0,0 +1,29 @@
console.log('i18n utils test:::::::::::::::::::')
import { warn, error, isObject, isBoolean, isString, isPlainObject, isNull, isFunction, parseArgs, arrayFrom, hasOwn, merge, looseEqual } from '../util'
console.log('warn', warn('test warn'))
console.log('error', error('test error'))
console.log('isArray', isArray('test isArray'))
console.log('isObject', isObject({}))
console.log('isBoolean', isBoolean(false))
console.log('isString', isString('false'))
console.log('isPlainObject', isPlainObject({}))
console.log('isNull', isNull(null))
console.log('isFunction', isFunction(null))
console.log('parseArgs', parseArgs(1,2,23,5))
console.log('parseArgs', parseArgs('zh-CN'))
console.log('parseArgs', parseArgs({ a: 1, b: 2 }))
console.log('parseArgs', parseArgs('zh-CN', { a: 1, b: 2 }))
console.log('parseArgs', parseArgs({ a: 1, b: 2 }, 'zh-CN'))
console.log('arrayFrom', arrayFrom(new Set([1, 2, 3, 4, 5])))
console.log('hasOwn', hasOwn({ a: { b: 2 }, c: 3 }, 'a'))
console.log('hasOwn', hasOwn({ a: { b: 2 }, c: 3 }, 'd'))
console.log('merge', merge({ a: { b: 2 }, c: 3 }, { b: 2 }))
console.log('looseEqual', looseEqual(123,123))
console.log('looseEqual', looseEqual('hello','hello'))
console.log('looseEqual', looseEqual([1, 2, 3],[1, 2, 3]))
console.log('looseEqual', looseEqual([1, 2, 3],[1, 2, 4]))
console.log('looseEqual', looseEqual({},[1, 2, 4]))
console.log('looseEqual', looseEqual({},{}))
console.log('looseEqual', looseEqual({},{a:1}))
console.log('looseEqual', looseEqual({a:1},{a:1}))

View File

@@ -0,0 +1,53 @@
// #ifndef APP
import { ComputedRef, WritableComputedRef } from 'vue'
type ComputedRefImpl<T> = WritableComputedRef<T>;
// #endif
export type AnyOrNull = any | null;
export type NumberOrNull = number | null;
export type StringOrNull = string | null;
// 定义特定的函数类型别名
export type Interpolate = (key : string, locale : StringOrNull, values : any, visitedLinkStack : string[], interpolateMode : string) => StringOrNull;
export type Link = (str : string, locale : StringOrNull, values : any, visitedLinkStack : string[], interpolateMode : string) => StringOrNull;
export type WarnDefault = (key : string, message : StringOrNull, values : any, interpolateMode : string) => StringOrNull;
export type LinkedModify = (str : string) => string;
export type PluralizationRule = (choice : number, choicesLength : number) => number
export interface Availabilities {
dateTimeFormat : boolean
numberFormat : boolean
}
export type Composer = {
id : number,
locale : Ref<string>,
fallbackLocale : ComputedRefImpl<any>,
messages : Ref<Map<string, UTSJSONObject>>,
t(key : string, values ?: any, locale ?: string) : string,
tc(key : string, choice ?: number, values ?: any, locale ?: string) : string,
d(date : any, key : StringOrNull, locale : StringOrNull, options : UTSJSONObject | null) : string
n(number : number, key : StringOrNull, locale : StringOrNull, options : UTSJSONObject | null) : string
setLocaleMessage(locale : string, message : UTSJSONObject) : void,
getLocaleMessage(locale : string) : UTSJSONObject,
mergeLocaleMessage(locale : string, message : UTSJSONObject) : void,
setDateTimeFormat(locale : string, format : UTSJSONObject) : void,
getDateTimeFormat(locale : string) : UTSJSONObject,
mergeDateTimeFormat(locale : string, format : UTSJSONObject) : void,
setNumberFormat(locale : string, format : UTSJSONObject) : void,
getNumberFormat(locale : string) : UTSJSONObject,
mergeNumberFormat(locale : string, format : UTSJSONObject) : void,
setTabBar(locale : string, tabbar : string[]) : void,
getTabBar(locale : string) : string[],
// 可用的语言环境列表。
availableLocales : string[],
// 可用的功能列表。
availabilities : Availabilities
}

View File

@@ -0,0 +1,371 @@
// @ts-nocheck
/* @flow */
/**
* constants
*/
import { errorMessages } from './errors'
import { warnMessages } from './warnings'
export const numberFormatKeys = [
'compactDisplay',
'currency',
'currencyDisplay',
'currencySign',
'localeMatcher',
'notation',
'numberingSystem',
'signDisplay',
'style',
'unit',
'unitDisplay',
'useGrouping',
'minimumIntegerDigits',
'minimumFractionDigits',
'maximumFractionDigits',
'minimumSignificantDigits',
'maximumSignificantDigits'
]
export const dateTimeFormatKeys = [
'dateStyle',
'timeStyle',
'calendar',
'localeMatcher',
"hour12",
"hourCycle",
"timeZone",
"formatMatcher",
'weekday',
'era',
'year',
'month',
'day',
'hour',
'minute',
'second',
'timeZoneName',
]
/**
* utilities
*/
export function getAllKeys(map:Map<string, UTSJSONObject>):string[] {
let keys:string[] = []
map.forEach((_, key) => {
keys.push(key)
})
return keys
}
/**
* 打印警告信息
* @param {string} msg - 警告信息
* @param {Error} err - 可选的错误对象
*/
export function warn(msg : string, code:number = -1) {
if(process.env.NODE_ENV !== 'production') {
console.warn(`[uvue-i18n] : ${code!=-1?warnMessages.get(code):msg}`)
}
}
/**
* 打印错误信息
* @param {string} msg - 错误信息
* @param {Error} err - 可选的错误对象
*/
export function error(code: number, msg : string|null = null) {
if(process.env.NODE_ENV !== 'production') {
console.error(`[uvue-i18n] : ${msg ?? errorMessages.get(code)}`)
}
}
export function isArray(value : any) : boolean {
return Array.isArray(value)
}
/**
* 判断一个值是否为对象
* @param {mixed} obj - 需要判断的值
* @returns {boolean} - 如果值为对象,则返回 true否则返回 false
*/
export function isObject(obj : any | null) : boolean {
return obj != null && typeof obj == 'object'
}
/**
* 判断一个值是否为布尔值
* @param {mixed} val - 需要判断的值
* @returns {boolean} - 如果值为布尔值,则返回 true否则返回 false
*/
export function isBoolean(val : any) : boolean {
return typeof val == 'boolean'
}
/**
* 判断一个值是否为字符串
* @param {mixed} val - 需要判断的值
* @returns {boolean} - 如果值为字符串,则返回 true否则返回 false
*/
export function isString(val : any) : boolean {
return typeof val == 'string'
}
/**
* 判断一个值是否为普通对象
* @param {any} obj - 需要判断的值
* @returns {boolean} - 如果值为普通对象,则返回 true否则返回 false
*/
export function isPlainObject(obj : any) : boolean {
// #ifndef APP-ANDROID || APP-IOS
const toString = Object.prototype.toString
const OBJECT_STRING : string = '[object Object]'
return toString.call(obj) === OBJECT_STRING
// #endif
// #ifdef APP-ANDROID || APP-IOS
return typeof obj == 'object' && obj instanceof UTSJSONObject
// #endif
}
/**
* 判断一个值是否为 null 或 undefined
* @param {mixed} val - 需要判断的值
* @returns {boolean} - 如果值为 null 或 undefined则返回 true否则返回 false
*/
// #ifndef APP-ANDROID || APP-IOS
export function isNull(val : any | null | undefined) : boolean {
return val == null || val == undefined
}
// #endif
// #ifdef APP-ANDROID || APP-IOS
export function isNull(val : any | null) : boolean {
return val == null
}
// #endif
/**
* 判断一个值是否为函数
* @param {mixed} val - 需要判断的值
* @returns {boolean} - 如果值为函数,则返回 true否则返回 false
*/
export function isFunction(val : any) : boolean {
return typeof val == 'function'
}
/**
* 解析参数
* @param {...mixed} args - 输入的参数
* @returns {Object} - 包含 locale 和 params 的对象
*/
export function parseArgs(...args : Array<any>) : Map<string, UTSJSONObject> {
let locale : string | null = null
let params : UTSJSONObject | null = null
if (args.length == 1) {
if (isObject(args[0]) || isArray(args[0]) ) {
params = args[0] //as UTSJSONObject
} else if (typeof args[0] == 'string') {
locale = args[0] as string
}
} else if (args.length == 2) {
if (typeof args[0] == 'string') {
locale = args[0] as string
}
if (isObject(args[1]) || isArray(args[1])) {
params = args[1] //as UTSJSONObject
}
}
if(locale == null || params == null) return new Map<string, UTSJSONObject>()
return new Map([
[locale, params]
])
}
/**
* looseClone 函数用于对一个对象进行浅拷贝。
* 它通过将对象序列化为 JSON 字符串,然后再将其解析回对象来实现这一目的。
* 请注意这种方法仅适用于可序列化的对象不适用于包含循环引用或特殊对象如函数、Date 对象等)的对象。
*
* @param {Object} obj - 需要进行浅拷贝的对象。
* @returns {Object} 返回一个新的对象,它是原始对象的浅拷贝。
*/
export function looseClone(obj : UTSJSONObject) : UTSJSONObject {
return JSON.parse(JSON.stringify(obj))
}
/**
* remove 函数用于从数组中删除指定的元素。
* 如果成功删除元素,则返回修改后的数组;否则,不返回任何值。
*
* @param {Array} arr - 需要操作的数组。
* @param {*} item - 需要删除的元素。
* @returns {Array} 返回修改后的数组,或者不返回任何值。
*/
export function remove(arr : Set<any>, item : any) : Set<any> | null {
if (arr.delete(item)) {
return arr
}
return null
}
/**
* arrayFrom 函数用于将类数组对象(如 Set 集合)转换为数组。
*
* @param {Set} arr - 需要转换的类数组对象。
* @returns {Array} 返回一个新数组,其中包含原类数组对象的所有元素。
*/
export function arrayFrom(arr : Set<any>) : Array<any> {
const ret : any[] = []
arr.forEach(a => {
ret.push(a)
})
return ret
}
/**
* includes 函数用于检查数组中是否包含指定的元素。
*
* @param {Array} arr - 需要检查的数组。
* @param {*} item - 需要查找的元素。
* @returns {boolean} 如果数组中包含指定元素,则返回 true否则返回 false。
*/
export function includes(arr : Array<any>, item : any) : boolean {
return arr.indexOf(item)
}
/**
* hasOwn 函数用于检查对象是否具有指定的属性。
* 与直接使用 `obj.hasOwnProperty` 不同,此函数可以正确处理通过原型链继承的属性。
*
* @param {Object|Array} obj - 需要检查的对象或数组。
* @param {string} key - 需要检查的属性名。
* @returns {boolean} 如果对象具有指定的属性,则返回 true否则返回 false。
*/
export function hasOwn(obj : UTSJSONObject, key : string) : boolean {
return obj[key] != null
}
/**
* merge 函数用于合并多个对象。
* 它会将源对象的所有可枚举属性值复制到目标对象。
* 如果目标对象和源对象有相同的属性,且它们的属性值都是对象,则会递归地合并这两个属性值。
*
* @param {Object} target - 目标对象,将被合并的对象。
* @returns {Object} 返回合并后的新对象。
*/
export function merge(...target : UTSJSONObject[]) : UTSJSONObject {
return UTSJSONObject.assign(...target)
// const output = Object(target)
// for (let i = 1; i < arguments.length; i++) {
// const source = arguments[i]
// if (source !== undefined && source !== null) {
// let key
// for (key in source) {
// if (hasOwn(source, key)) {
// if (isObject(source[key])) {
// output[key] = merge(output[key], source[key])
// } else {
// output[key] = source[key]
// }
// }
// }
// }
// }
// return output
}
/**
* looseEqual 函数用于比较两个值是否宽松相等。
* 宽松相等意味着在比较时会进行类型转换,例如将字符串转换为数字。
* 该函数可以处理对象、数组和其他基本数据类型的值。
*
* @param {any} a - 要比较的第一个值。
* @param {any} b - 要比较的第二个值。
* @returns {boolean} 如果两个值宽松相等,则返回 true否则返回 false。
*/
export function looseEqual(a : any, b : any) : boolean {
// 如果 a 和 b 严格相等,直接返回 true
if (a == b) { return true }
// 检查 a 和 b 是否都是对象
const isObjectA : boolean = isObject(a)
const isObjectB : boolean = isObject(b)
// 如果 a 和 b 都是对象
if (isObjectA && isObjectB) {
try {
// 检查 a 和 b 是否都是数组
const isArrayA : boolean = Array.isArray(a)
const isArrayB : boolean = Array.isArray(b)
// 如果 a 和 b 都是数组
if (isArrayA && isArrayB) {
// 比较它们的长度是否相等,以及它们的每个元素是否宽松相等
return (a as any[]).length == (b as any[]).length && a.every((e : any, i : number) : boolean => {
return looseEqual(e, b[i])
})
} else if (!isArrayA && !isArrayB) { // 如果 a 和 b 都不是数组
// 比较它们的键的数量是否相等,以及对应的键对应的值是否宽松相等
const keysA : Array<string> = UTSJSONObject.keys(a as UTSJSONObject)
const keysB : Array<string> = UTSJSONObject.keys(b as UTSJSONObject)
return keysA.length == keysB.length && keysA.every((key : string) : boolean => {
const valueA = a[key]
const valueB = b[key]
if(valueA == null || valueB == null) {
return false
}
return looseEqual(valueA, valueB)
})
} else {
// 如果 a 和 b 类型不同(一个是数组,另一个不是),返回 false
return false
}
} catch (e) {
// 如果在比较过程中发生异常,返回 false
return false
}
} else if (!isObjectA && !isObjectB) { // 如果 a 和 b 都不是对象
// 尝试将它们转换为字符串并比较
return `${a}` == `${b}`
} else {
// 如果 a 和 b 类型不同(一个是对象,另一个不是),返回 false
return false
}
}
/**
* 对用户输入的原始文本进行 HTML 特殊字符转义,以降低 XSS 攻击的风险。
* @param {string} rawText - 需要转义的原始用户输入文本。
* @returns {string} 返回转义后的文本。
*/
function escapeHtml(rawText: string): string {
return rawText
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
/**
* 从 `parseArgs().params` 返回的所有提供的参数中转义 HTML 标签和特殊符号。
* 此方法对 `params` 对象执行原地操作。
*
* @param {any} params - 从 `parseArgs().params` 提供的参数。
* 可能是字符串数组或字符串到任意值的映射。
* @returns {any} 返回被操纵过的 `params` 对象。
*/
export function escapeParams(params: UTSJSONObject|null): UTSJSONObject|null {
if(params != null) {
UTSJSONObject.keys(params).forEach(key => {
if(typeof(params[key]) == 'string') {
params[key] = escapeHtml(params[key])
}
})
}
return params
}

View File

@@ -0,0 +1,25 @@
type warnMessagesTypes = {
FALLBACK_TO_ROOT: number
NOT_FOUND_PARENT_SCOPE: number
IGNORE_OBJ_FLATTEN: number
DEPRECATE_TC: number
}
export const I18nWarnCodes:warnMessagesTypes = {
// 使用根语言环境回退到{type} '{key}'
FALLBACK_TO_ROOT: 8,
// 未找到父作用域,使用全局作用域
NOT_FOUND_PARENT_SCOPE: 9,
// 忽略对象扁平化:'{key}'键具有字符串值
IGNORE_OBJ_FLATTEN: 10,
// 'tc'和'$tc'已在v10中被弃用请使用't'或'$t'代替。'tc'和'$tc'将在v11中移除
DEPRECATE_TC: 11
}
export const warnMessages : Map<number, string> = new Map<number, string>([
[I18nWarnCodes.FALLBACK_TO_ROOT, `使用根语言环境回退到{type} '{key}'。`],
[I18nWarnCodes.NOT_FOUND_PARENT_SCOPE, `未找到父作用域,使用全局作用域。`],
[I18nWarnCodes.IGNORE_OBJ_FLATTEN, `忽略对象扁平化:'{key}'键具有字符串值。`],
[I18nWarnCodes.DEPRECATE_TC, `'tc'和'$tc'已在v10中被弃用请使用't'或'$t'代替。'tc'和'$tc'将在v11中移除。`],
])