提交 9dc717e3 编写于 作者: fxy060608's avatar fxy060608

wip(i18n): json i18n

上级 073d3134
......@@ -188,6 +188,7 @@ declare namespace UniApp {
interface PageRouteMeta extends PagesJsonPageStyle {
id?: number
route: string
i18n?: boolean
isQuit?: boolean
isEntry?: boolean
isTabBar?: boolean
......
......@@ -107,7 +107,7 @@ var serviceContext = (function (vue) {
const extend = Object.assign;
const hasOwnProperty$1 = Object.prototype.hasOwnProperty;
const hasOwn$1 = (val, key) => hasOwnProperty$1.call(val, key);
const isArray = Array.isArray;
const isArray$1 = Array.isArray;
const isFunction = (val) => typeof val === 'function';
const isString = (val) => typeof val === 'string';
const isObject$1 = (val) => val !== null && typeof val === 'object';
......@@ -164,7 +164,7 @@ var serviceContext = (function (vue) {
return str;
}
function elemsInArray(strArr, optionalVal) {
if (!isArray(strArr) ||
if (!isArray$1(strArr) ||
strArr.length === 0 ||
strArr.find((val) => optionalVal.indexOf(val) === -1)) {
return optionalVal;
......@@ -189,7 +189,7 @@ var serviceContext = (function (vue) {
if (!protocol) {
return;
}
if (!isArray(protocol)) {
if (!isArray$1(protocol)) {
return validateProtocol(name, args[0] || Object.create(null), protocol, onFail);
}
const len = protocol.length;
......@@ -219,7 +219,7 @@ var serviceContext = (function (vue) {
// type check
if (type != null) {
let isValid = false;
const types = isArray(type) ? type : [type];
const types = isArray$1(type) ? type : [type];
const expectedTypes = [];
// value is valid as long as one of the specified types match
for (let i = 0; i < types.length && !isValid; i++) {
......@@ -252,7 +252,7 @@ var serviceContext = (function (vue) {
valid = isObject$1(value);
}
else if (expectedType === 'Array') {
valid = isArray(value);
valid = isArray$1(value);
}
else {
{
......@@ -460,7 +460,7 @@ var serviceContext = (function (vue) {
function wrapperOptions(interceptors, options = {}) {
[HOOK_SUCCESS, HOOK_FAIL, HOOK_COMPLETE].forEach((name) => {
const hooks = interceptors[name];
if (!isArray(hooks)) {
if (!isArray$1(hooks)) {
return;
}
const oldCallback = options[name];
......@@ -474,11 +474,11 @@ var serviceContext = (function (vue) {
}
function wrapperReturnValue(method, returnValue) {
const returnValueHooks = [];
if (isArray(globalInterceptors.returnValue)) {
if (isArray$1(globalInterceptors.returnValue)) {
returnValueHooks.push(...globalInterceptors.returnValue);
}
const interceptor = scopedInterceptors[method];
if (interceptor && isArray(interceptor.returnValue)) {
if (interceptor && isArray$1(interceptor.returnValue)) {
returnValueHooks.push(...interceptor.returnValue);
}
returnValueHooks.forEach((hook) => {
......@@ -506,7 +506,7 @@ var serviceContext = (function (vue) {
function invokeApi(method, api, options, ...params) {
const interceptor = getApiInterceptorHooks(method);
if (interceptor && Object.keys(interceptor).length) {
if (isArray(interceptor.invoke)) {
if (isArray$1(interceptor.invoke)) {
const res = queue(interceptor.invoke, options);
return res.then((options) => {
return api(wrapperOptions(interceptor, options), ...params);
......@@ -797,7 +797,7 @@ var serviceContext = (function (vue) {
if (key in query) {
// an extra variable for ts types
let currentValue = query[key];
if (!isArray(currentValue)) {
if (!isArray$1(currentValue)) {
currentValue = query[key] = [currentValue];
}
currentValue.push(value);
......@@ -1262,18 +1262,20 @@ var serviceContext = (function (vue) {
ON_NAVIGATION_BAR_SEARCH_INPUT_FOCUS_CHANGED,
];
const isArray = Array.isArray;
const isObject = (val) => val !== null && typeof val === 'object';
const defaultDelimiters = ['{', '}'];
class BaseFormatter {
constructor() {
this._caches = Object.create(null);
}
interpolate(message, values) {
interpolate(message, values, delimiters = defaultDelimiters) {
if (!values) {
return [message];
}
let tokens = this._caches[message];
if (!tokens) {
tokens = parse(message);
tokens = parse(message, delimiters);
this._caches[message] = tokens;
}
return compile(tokens, values);
......@@ -1281,24 +1283,24 @@ var serviceContext = (function (vue) {
}
const RE_TOKEN_LIST_VALUE = /^(?:\d)+/;
const RE_TOKEN_NAMED_VALUE = /^(?:\w)+/;
function parse(format) {
function parse(format, [startDelimiter, endDelimiter]) {
const tokens = [];
let position = 0;
let text = '';
while (position < format.length) {
let char = format[position++];
if (char === '{') {
if (char === startDelimiter) {
if (text) {
tokens.push({ type: 'text', value: text });
}
text = '';
let sub = '';
char = format[position++];
while (char !== undefined && char !== '}') {
while (char !== undefined && char !== endDelimiter) {
sub += char;
char = format[position++];
}
const isClosed = char === '}';
const isClosed = char === endDelimiter;
const type = RE_TOKEN_LIST_VALUE.test(sub)
? 'list'
: isClosed && RE_TOKEN_NAMED_VALUE.test(sub)
......@@ -1306,12 +1308,12 @@ var serviceContext = (function (vue) {
: 'unknown';
tokens.push({ value: sub, type });
}
else if (char === '%') {
// when found rails i18n syntax, skip text capture
if (format[position] !== '{') {
text += char;
}
}
// else if (char === '%') {
// // when found rails i18n syntax, skip text capture
// if (format[position] !== '{') {
// text += char
// }
// }
else {
text += char;
}
......@@ -1322,7 +1324,7 @@ var serviceContext = (function (vue) {
function compile(tokens, values) {
const compiled = [];
let index = 0;
const mode = Array.isArray(values)
const mode = isArray(values)
? 'list'
: isObject(values)
? 'named'
......@@ -1425,9 +1427,12 @@ var serviceContext = (function (vue) {
this.messages[this.locale] = {};
}
this.message = this.messages[this.locale];
this.watchers.forEach((watcher) => {
watcher(this.locale, oldLocale);
});
// 仅发生变化时,通知
if (oldLocale !== this.locale) {
this.watchers.forEach((watcher) => {
watcher(this.locale, oldLocale);
});
}
}
getLocale() {
return this.locale;
......@@ -1543,6 +1548,9 @@ var serviceContext = (function (vue) {
add(locale, message) {
return i18n.add(locale, message);
},
watch(fn) {
return i18n.watchLocale(fn);
},
getLocale() {
return i18n.getLocale();
},
......@@ -2611,7 +2619,7 @@ var serviceContext = (function (vue) {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: isArray(childVal)
: isArray$1(childVal)
? childVal
: [childVal]
: parentVal;
......@@ -7350,7 +7358,7 @@ var serviceContext = (function (vue) {
plus.gallery.save(options.filePath, warpPlusSuccessCallback(resolve), warpPlusErrorCallback(reject));
}, SaveImageToPhotosAlbumProtocol, SaveImageToPhotosAlbumOptions);
const compressImage = defineAsyncApi(API_COMPRESS_IMAGE, (options, { resolve, reject }) => {
const compressImage$1 = defineAsyncApi(API_COMPRESS_IMAGE, (options, { resolve, reject }) => {
const dst = `${TEMP_PATH}/compressed/${Date.now()}_${getFileName(options.src)}`;
plus.zip.compressImage(extend({}, options, {
dst,
......@@ -7384,6 +7392,23 @@ var serviceContext = (function (vue) {
}, reject);
});
}
function compressImage(tempFilePath) {
const dst = `${TEMP_PATH}/compressed/${Date.now()}_${getFileName(tempFilePath)}`;
return new Promise((resolve) => {
plus.nativeUI.showWaiting();
plus.zip.compressImage({
src: tempFilePath,
dst,
overwrite: true,
}, () => {
plus.nativeUI.closeWaiting();
resolve(dst);
}, () => {
plus.nativeUI.closeWaiting();
resolve(tempFilePath);
});
});
}
const chooseImage = defineAsyncApi(API_CHOOSE_IMAGE,
// @ts-ignore crop 属性App特有
({ count, sizeType, sourceType, crop } = {}, { resolve, reject }) => {
......@@ -7409,7 +7434,22 @@ var serviceContext = (function (vue) {
}
function openCamera() {
const camera = plus.camera.getCamera();
camera.captureImage((path) => successCallback([path]), errorCallback, {
camera.captureImage((path) => {
// fix By Lxh 暂时添加拍照压缩逻辑,等客户端增加逻辑后修改
// 判断是否需要压缩
if (sizeType && sizeType.includes('compressed')) {
return getFileInfo(path)
.then(({ size }) => {
// 压缩阈值 0.5 兆
const THRESHOLD = 1024 * 1024 * 0.5;
return size && size > THRESHOLD
? compressImage(path).then((dstPath) => successCallback([dstPath]))
: successCallback([path]);
})
.catch(errorCallback);
}
return successCallback([path]);
}, errorCallback, {
filename: TEMP_PATH + '/camera/',
resolution: 'high',
crop,
......@@ -9717,7 +9757,7 @@ var serviceContext = (function (vue) {
Object.keys(options).forEach((name) => {
if (name.indexOf('on') === 0) {
const hooks = options[name];
if (isArray(hooks)) {
if (isArray$1(hooks)) {
hooks.forEach((hook) => injectLifecycleHook(name, hook, publicThis, instance));
}
else {
......@@ -9740,7 +9780,7 @@ var serviceContext = (function (vue) {
initModules(instance, options.$renderjs, options['$' + RENDERJS_MODULES]);
}
function initModules(instance, modules, moduleIds = {}) {
if (!isArray(modules)) {
if (!isArray$1(modules)) {
return;
}
const ownerId = instance.uid;
......@@ -10309,7 +10349,7 @@ var serviceContext = (function (vue) {
else if (name === 'titleImage' && value) {
titleNView.tags = createTitleImageTags(value);
}
else if (name === 'buttons' && isArray(value)) {
else if (name === 'buttons' && isArray$1(value)) {
titleNView.buttons = value.map((button, index) => {
button.onclick = createTitleNViewBtnClick(index);
return button;
......@@ -12468,7 +12508,7 @@ var serviceContext = (function (vue) {
getRecorderManager: getRecorderManager,
saveVideoToPhotosAlbum: saveVideoToPhotosAlbum,
saveImageToPhotosAlbum: saveImageToPhotosAlbum,
compressImage: compressImage,
compressImage: compressImage$1,
compressVideo: compressVideo,
chooseImage: chooseImage,
chooseVideo: chooseVideo,
......@@ -12548,7 +12588,7 @@ var serviceContext = (function (vue) {
if ((process.env.NODE_ENV !== 'production')) {
console.log(formatLog('publishHandler', event, args, pageIds));
}
if (!isArray(pageIds)) {
if (!isArray$1(pageIds)) {
pageIds = [pageIds];
}
const evalJSCode = `typeof UniViewJSBridge !== 'undefined' && UniViewJSBridge.subscribeHandler("${event}",${args},__PAGE_ID__)`;
......
因为 它太大了无法显示 source diff 。你可以改为 查看blob
......@@ -24,11 +24,15 @@ export type Define = typeof def
export type Require = typeof req
export type Exports = Record<string, any>
export function def(name: string, deps: string[], definition: Function) {
req(deps, () => resolve(name, definition()))
export function def(
name: string,
deps: string[],
definition: (...args: any[]) => void
) {
req(deps, () => resolve(name))
}
export function req(modules: string[], definition: Function) {
export function req(modules: string[], definition: (...args: any[]) => void) {
Promise.all(modules.map(deps)).then((result) =>
definition.apply(null, result)
)
......
......@@ -2,16 +2,35 @@ import {
isServiceNativeTag,
isServiceCustomElement,
} from '@dcloudio/uni-shared'
import { UniVitePlugin } from '@dcloudio/uni-cli-shared'
import { compileI18nJsonStr } from '@dcloudio/uni-i18n'
import {
UniVitePlugin,
initI18nOptions,
getFallbackLocale,
} from '@dcloudio/uni-cli-shared'
export function uniOptions(): UniVitePlugin['uni'] {
return {
copyOptions() {
const inputDir = process.env.UNI_INPUT_DIR
const outputDir = process.env.UNI_OUTPUT_DIR
return {
assets: [
'androidPrivacy.json',
'hybrid/html/**/*',
'uni_modules/*/hybrid/html/**/*',
assets: ['hybrid/html/**/*', 'uni_modules/*/hybrid/html/**/*'],
targets: [
{
src: 'androidPrivacy.json',
dest: outputDir,
transform(source) {
const options = initI18nOptions(
inputDir,
getFallbackLocale(inputDir)
)
if (!options) {
return
}
return compileI18nJsonStr(source.toString(), options)
},
},
],
}
},
......
import { normalizeI18nLocale } from '../src/i18n'
describe('normalizeI18nLocale', () => {
test('specifying locale', () => {
expect(normalizeI18nLocale({ 'zh-Hans': {}, fr: {} }, 'fr')).toBe('fr')
})
test('fallback en', () => {
expect(normalizeI18nLocale({ 'zh-Hans': {}, en: {} }, 'fr')).toBe('en')
})
test('fallback zh-Hans', () => {
expect(normalizeI18nLocale({ 'zh-Hans': {}, es: {} })).toBe('zh-Hans')
})
test('fallback zh-Hant', () => {
expect(normalizeI18nLocale({ 'zh-Hant': {}, es: {} })).toBe('zh-Hant')
})
test('fallback first locale', () => {
expect(normalizeI18nLocale({ fr: {}, es: {} })).toBe('fr')
})
})
import fs from 'fs'
import path from 'path'
import { I18N_JSON_DELIMITERS } from '@dcloudio/uni-shared'
export function initI18nOptions(inputDir: string, fallbackLocale?: string) {
const locales = initLocales(path.resolve(inputDir, 'locale'))
if (!Object.keys(locales).length) {
return
}
const locale = normalizeI18nLocale(locales, fallbackLocale)
return {
locale,
locales,
delimiters: I18N_JSON_DELIMITERS,
}
}
function initLocales(dir: string) {
if (!fs.existsSync(dir)) {
return {}
}
return fs.readdirSync(dir).reduce((res, filename) => {
if (path.extname(filename) === '.json') {
try {
res[path.basename(filename).replace('.json', '')] = JSON.parse(
fs.readFileSync(path.join(dir, filename), 'utf8')
)
} catch (e) {}
}
return res
}, {} as Record<string, Record<string, string>>)
}
const defaultFallbackLocale = 'en'
// specifying locale > en > zh-Hans > zh-Hant > first locale
export function normalizeI18nLocale(
locales: Record<string, Record<string, string>>,
locale: string = defaultFallbackLocale
) {
if (locales[locale]) {
return locale
}
return (
['en', 'zh-Hans', 'zh-Hant'].find((n) => locales[n]) ||
Object.keys(locales)[0] ||
defaultFallbackLocale
)
}
......@@ -3,6 +3,7 @@ export * from './env'
export * from './hbx'
export * from './logs'
export * from './ssr'
export * from './i18n'
export * from './deps'
export * from './json'
export * from './vite'
......
import { recursive } from 'merge'
import { M } from '../../../messages'
import { initI18nOptions } from '../../../i18n'
export function initRecursiveMerge(
manifestJson: Record<string, any>,
userManifestJson: Record<string, any>
): Record<string, any> {
const i18nOptions = initI18nOptions(
process.env.UNI_INPUT_DIR,
userManifestJson.fallbackLocale
)
let fallbackLocale: string | undefined = undefined
if (i18nOptions) {
fallbackLocale = i18nOptions.locale
if (!userManifestJson.fallbackLocale) {
console.warn(
M['i18n.fallbackLocale.missing'].replace('{locale}', fallbackLocale)
)
} else if (userManifestJson.fallbackLocale !== fallbackLocale) {
console.warn(
M['i18n.fallbackLocale.unmatch'].replace(
'{locale}',
userManifestJson.fallbackLocale
)
)
}
}
return recursive(
true,
manifestJson,
......@@ -15,7 +37,8 @@ export function initRecursiveMerge(
name: userManifestJson.versionName,
code: userManifestJson.versionCode,
},
language: userManifestJson.locale,
locale: userManifestJson.locale,
fallbackLocale,
},
{ plus: userManifestJson['app-plus'] }
)
......
......@@ -52,3 +52,8 @@ export function getUniStatistics(inputDir: string, platform: UniApp.PLATFORM) {
manifest[platform] && manifest[platform].uniStatistics
)
}
export function getFallbackLocale(inputDir: string) {
const manifest = parseManifestJsonOnce(inputDir)
return manifest.fallbackLocale
}
......@@ -12,4 +12,8 @@ export const M = {
'dev.watching.end.files': 'DONE Build complete. FILES:{files}',
'stat.warn.appid':
'当前应用未配置Appid,无法使用uni统计,详情参考:https://ask.dcloud.net.cn/article/36303',
'i18n.fallbackLocale.missing':
'当前应用未在manifest.json配置fallbackLocale,默认使用:{locale}',
'i18n.fallbackLocale.unmatch':
'当前应用配置的fallbackLocale为:${locale},但locale目录缺少该语言文件',
}
......@@ -3,7 +3,14 @@ import path from 'path'
import { ConfigEnv } from 'vite'
import { extend, isArray, isString } from '@vue/shared'
interface ProjectFeatures {}
interface ProjectFeatures {
i18nLocales: boolean
i18nEn: boolean
i18nEs: boolean
i18nFr: boolean
i18nZhHans: boolean
i18nZhHant: boolean
}
interface PagesFeatures {
nvue: boolean
pages: boolean
......@@ -25,18 +32,26 @@ interface ManifestFeatures {
promise: boolean
longpress: boolean
routerMode: '"hash"' | '"history"'
i18nEn: boolean
i18nEs: boolean
i18nFr: boolean
i18nZhHans: boolean
i18nZhHant: boolean
vueOptionsApi: boolean
vueProdDevTools: boolean
}
function initProjectFeature({ command }: InitFeaturesOptions) {
const features: ProjectFeatures = {}
if (command === 'build') {
function initProjectFeature({ inputDir }: InitFeaturesOptions) {
const features: ProjectFeatures = {
i18nLocales: false,
i18nEn: true,
i18nEs: true,
i18nFr: true,
i18nZhHans: true,
i18nZhHant: true,
}
const localesDir = path.resolve(inputDir, 'locales')
if (fs.existsSync(localesDir)) {
if (
fs.readdirSync(localesDir).find((file) => path.extname(file) === '.json')
) {
features.i18nLocales = true
}
}
return features
}
......@@ -155,11 +170,6 @@ function initManifestFeature({
promise: false,
longpress: true,
routerMode: '"hash"',
i18nEn: true,
i18nEs: true,
i18nFr: true,
i18nZhHans: true,
i18nZhHant: true,
vueOptionsApi: true,
vueProdDevTools: false,
}
......@@ -176,28 +186,6 @@ function initManifestFeature({
) {
features.routerMode = '"history"'
}
const platformJson = manifestJson[platform] || {}
const manifestFeatures = platformJson.features
if (manifestFeatures) {
const { i18n } = manifestFeatures
if (isArray(i18n)) {
if (!i18n.includes('en')) {
features.i18nEn = false
}
if (!i18n.includes('es')) {
features.i18nEs = false
}
if (!i18n.includes('fr')) {
features.i18nFr = false
}
if (!i18n.includes('zh-Hans')) {
features.i18nZhHans = false
}
if (!i18n.includes('zh-Hant')) {
features.i18nZhHant = false
}
}
}
// TODO other features
return features
}
......@@ -224,6 +212,7 @@ export function initFeatures(options: InitFeaturesOptions) {
i18nFr,
i18nZhHans,
i18nZhHant,
i18nLocales,
vueOptionsApi,
vueProdDevTools,
pages,
......@@ -261,6 +250,7 @@ export function initFeatures(options: InitFeaturesOptions) {
__UNI_FEATURE_I18N_ZH_HANS__: i18nZhHans, // 是否启用zh_Hans
__UNI_FEATURE_I18N_ZH_HANT__: i18nZhHant, // 是否启用zh_Hant
// 以下特性,编译器已自动识别是否需要启用
__UNI_FEATURE_I18N_LOCALES__: i18nLocales, // 是否启用i18n
__UNI_FEATURE_NVUE__: nvue, // 是否启用nvue
__UNI_FEATURE_ROUTER_MODE__: routerMode, // 路由模式
__UNI_FEATURE_PAGES__: pages, // 是否多页面
......
import { Plugin, ResolvedConfig } from 'vite'
import { FileWatcher } from '../../watcher'
import { FileWatcher, FileWatcherOptions } from '../../watcher'
import { M } from '../../messages'
export interface UniViteCopyPluginTarget {
src: string | string[]
dest: string
}
export type UniViteCopyPluginTarget = Omit<FileWatcherOptions, 'verbose'>
export interface UniViteCopyPluginOptions {
targets: UniViteCopyPluginTarget[]
verbose: boolean
......
import fs from 'fs-extra'
import path from 'path'
import { FSWatcher, watch, WatchOptions } from 'chokidar'
interface FileWatcherOptions {
type FileTransform = (source: Buffer, filename: string) => void | string
export interface FileWatcherOptions {
src: string | string[]
dest: string
transform?: FileTransform
verbose?: boolean
}
export class FileWatcher {
private src: string[]
private dest: string
private transform?: FileTransform
private verbose?: boolean
private watcher!: FSWatcher
private onChange?: () => void
constructor({ src, dest, verbose }: FileWatcherOptions) {
constructor({ src, dest, transform, verbose }: FileWatcherOptions) {
this.src = !Array.isArray(src) ? [src] : src
this.dest = dest
this.transform = transform
this.verbose = verbose
}
watch(
......@@ -57,6 +60,19 @@ export class FileWatcher {
copy(from: string) {
const to = this.to(from)
this.info('copy', from + '=>' + to)
let content: string | void
if (this.transform) {
const filename = this.from(from)
content = this.transform(fs.readFileSync(filename), filename)
}
if (content) {
return fs
.outputFile(to, content)
.catch((e) => {
// this.info('copy', e)
})
.then(() => this.onChange && this.onChange())
}
return fs
.copy(this.from(from), to, { overwrite: true })
.catch((e) => {
......
......@@ -56,6 +56,7 @@ function generatePagesJsonCode(
import { defineAsyncComponent, resolveComponent, createVNode, withCtx, openBlock, createBlock } from 'vue'
import { PageComponent, AsyncLoadingComponent, AsyncErrorComponent, setupWindow } from '@dcloudio/uni-h5'
import { appid, debug, networkTimeout, router, async, sdkConfigs, qqMapKey, nvue, locale } from '${manifestJsonPath}'
const locales = import.meta.globEager('./locale/*.json')
${importLayoutComponentsCode}
const extend = Object.assign
${cssCode}
......@@ -260,7 +261,8 @@ delete ${globalName}['____'+appid+'____']
qqMapKey,
nvue,
locale,
router
locales:Object.keys(locales).reduce((res,name)=>{res[name.replace(/\\.\\/locale\\/(.*).json/,'$1')]=locales[name].default;return res},{}),
router,
})
`
)
......
......@@ -315,7 +315,7 @@ function usePageHeadButtons({ id, navigationBar }: UniApp.PageRouteMeta) {
}
btn.fontFamily = fontFamily
}
const pageHeadBtn = usePageHeadButton(id, index, btn, isTransparent)
const pageHeadBtn = usePageHeadButton(id!, index, btn, isTransparent)
if (btn.float === 'left') {
left.push(pageHeadBtn)
} else {
......@@ -374,7 +374,7 @@ function usePageHeadSearchInput({
const { disabled } = searchInput!
if (disabled) {
const onClick = () => {
invokeHook(id, 'onNavigationBarSearchInputClicked')
invokeHook(id!, 'onNavigationBarSearchInputClicked')
}
return {
focus,
......@@ -385,19 +385,19 @@ function usePageHeadSearchInput({
}
const onFocus = () => {
focus.value = true
invokeHook(id, 'onNavigationBarSearchInputFocusChanged', { focus: true })
invokeHook(id!, 'onNavigationBarSearchInputFocusChanged', { focus: true })
}
const onBlur = () => {
focus.value = false
invokeHook(id, 'onNavigationBarSearchInputFocusChanged', { focus: false })
invokeHook(id!, 'onNavigationBarSearchInputFocusChanged', { focus: false })
}
const onInput = (evt: { detail: { value: string } }) => {
text.value = evt.detail.value
invokeHook(id, 'onNavigationBarSearchInputChanged', { text: text.value })
invokeHook(id!, 'onNavigationBarSearchInputChanged', { text: text.value })
}
const onKeyup = (evt: KeyboardEvent) => {
if (evt.key === 'Enter' || evt.keyCode === 13) {
invokeHook(id, 'onNavigationBarSearchInputConfirmed', {
invokeHook(id!, 'onNavigationBarSearchInputConfirmed', {
text: text.value,
})
}
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compileI18nJsonStr androidPrivacy.json 1`] = `
"{
\\"version\\": \\"1\\",
\\"prompt\\": \\"template\\",
\\"title\\": \\"服务协议和隐私政策\\",
\\"message\\": \\"  请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/>  你可阅读<a href=\\\\\\"\\\\\\">《服务协议》</a>和<a href=\\\\\\"\\\\\\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。\\",
\\"buttonAccept\\": \\"同意并接受\\",
\\"buttonRefuse\\": \\"暂不同意\\",
\\"second\\": {
\\"title\\": \\"确认提示\\",
\\"message\\": \\"  进入应用前,你需先同意<a href=\\\\\\"\\\\\\">《服务协议》</a>和<a href=\\\\\\"\\\\\\">《隐私政策》</a>,否则将退出应用。\\",
\\"buttonAccept\\": \\"同意并继续\\",
\\"buttonRefuse\\": \\"退出应用\\",
\\"titleLocales\\": {
\\"zh-Hans\\": \\"确认提示\\",
\\"en\\": \\"confirm\\"
}
},
\\"styles\\": {
\\"backgroundColor\\": \\"#00FF00\\",
\\"borderRadius\\": \\"5px\\",
\\"title\\": {
\\"color\\": \\"#ff00ff\\",
\\"colorLocales\\": {
\\"zh-Hans\\": \\"#ff00ff\\",
\\"en\\": \\"#ff00ff\\"
}
},
\\"buttonAccept\\": {
\\"color\\": \\"#ffff00\\"
},
\\"buttonRefuse\\": {
\\"color\\": \\"#00ffff\\"
}
},
\\"titleLocales\\": {
\\"zh-Hans\\": \\"服务协议和隐私政策\\",
\\"en\\": \\"Privacy\\"
},
\\"messageLocales\\": {
\\"zh-Hans\\": \\"  请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/>  你可阅读<a href=\\\\\\"\\\\\\">《服务协议》</a>和<a href=\\\\\\"\\\\\\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。\\",
\\"en\\": \\"  privacy\\"
},
\\"buttonAcceptLocales\\": {
\\"zh-Hans\\": \\"同意并接受\\",
\\"en\\": \\"accept\\"
},
\\"buttonRefuseLocales\\": {
\\"zh-Hans\\": \\"暂不同意\\",
\\"en\\": \\"refuse\\"
}
}"
`;
exports[`compileI18nJsonStr pages.json->tabBar 1`] = `
"{
\\"color\\": \\"#7A7E83\\",
\\"selectedColor\\": \\"#007AFF\\",
\\"borderStyle\\": \\"black\\",
\\"backgroundColor\\": \\"#f8f8f8\\",
\\"list\\": [
{
\\"pagePath\\": \\"pages/tabBar/component/component\\",
\\"iconPath\\": \\"static/component.png\\",
\\"selectedIconPath\\": \\"static/componentHL.png\\",
\\"text\\": \\"组件\\",
\\"textLocales\\": {
\\"zh-Hans\\": \\"组件\\",
\\"en\\": \\"Component\\"
}
},
{
\\"pagePath\\": \\"pages/tabBar/API/API\\",
\\"iconPath\\": \\"static/api.png\\",
\\"selectedIconPath\\": \\"static/apiHL.png\\",
\\"text\\": \\"接口\\",
\\"textLocales\\": {
\\"zh-Hans\\": \\"接口\\",
\\"en\\": \\"API\\"
}
}
],
\\"backgroundColorLocales\\": {
\\"zh-Hans\\": \\"#f8f8f8\\",
\\"en\\": \\"#f6f6f6\\"
}
}"
`;
import { I18N_JSON_DELIMITERS } from '@dcloudio/uni-shared'
import { parseI18nJson, compileI18nJsonStr } from '../src/json'
const delimiters: [string, string] = I18N_JSON_DELIMITERS
describe('parseI18nJson', () => {
test('pages.json->style', () => {
const pageMeta = parseI18nJson(
{
tabBarIndex: 0,
bounce: 'vertical',
navigationBar: {
titleText: '%component.title%组件',
buttons: [
{
text: '%component.btns.0.text%',
fontSrc: '/static/uni.ttf',
fontSize: '22px',
color: '#FFFFFF',
},
],
},
route: 'pages/tabBar/component/component',
},
{
'component.title': '内置',
'component.btns.0.text': '\ue534',
},
delimiters
) as Record<string, any>
expect(pageMeta.navigationBar.titleText).toBe('内置组件')
expect(pageMeta.navigationBar.buttons[0].text).toBe('\ue534')
})
})
describe('compileI18nJsonStr', () => {
test('empty', () => {
expect(
compileI18nJsonStr(JSON.stringify({}), {
locale: '',
locales: {},
delimiters,
})
).toBe('{}')
})
test('pages.json->tabBar', () => {
expect(
compileI18nJsonStr(
JSON.stringify({
color: '#7A7E83',
selectedColor: '#007AFF',
borderStyle: 'black',
backgroundColor: '#%tabBar.backgroundColor%',
list: [
{
pagePath: 'pages/tabBar/component/component',
iconPath: 'static/component.png',
selectedIconPath: 'static/componentHL.png',
text: '%tabBar.0.title%',
},
{
pagePath: 'pages/tabBar/API/API',
iconPath: 'static/api.png',
selectedIconPath: 'static/apiHL.png',
text: '%tabBar.1.title%',
},
],
}),
{
locale: 'zh-Hans',
locales: {
'zh-Hans': {
'tabBar.backgroundColor': 'f8f8f8',
'tabBar.0.title': '组件',
'tabBar.1.title': '接口',
},
en: {
'tabBar.backgroundColor': 'f6f6f6',
'tabBar.0.title': 'Component',
'tabBar.1.title': 'API',
},
},
delimiters,
}
)
).toMatchSnapshot()
})
test('androidPrivacy.json', () => {
expect(
compileI18nJsonStr(
JSON.stringify({
version: '1',
prompt: 'template',
title: '%p.title%',
message: '  %p.message%',
buttonAccept: '%p.accept%',
buttonRefuse: '%p.refuse%',
second: {
title: '%p.second.title%',
message:
'  进入应用前,你需先同意<a href="">《服务协议》</a>和<a href="">《隐私政策》</a>,否则将退出应用。',
buttonAccept: '同意并继续',
buttonRefuse: '退出应用',
},
styles: {
backgroundColor: '#00FF00',
borderRadius: '5px',
title: {
color: '%p.title.color%',
},
buttonAccept: {
color: '#ffff00',
},
buttonRefuse: {
color: '#00ffff',
},
},
}),
{
locale: 'zh-Hans',
locales: {
'zh-Hans': {
'p.title': '服务协议和隐私政策',
'p.message':
'请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/>  你可阅读<a href="">《服务协议》</a>和<a href="">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。',
'p.accept': '同意并接受',
'p.refuse': '暂不同意',
'p.second.title': '确认提示',
'p.title.color': '#ff00ff',
},
en: {
'p.title': 'Privacy',
'p.message': 'privacy',
'p.accept': 'accept',
'p.refuse': 'refuse',
'p.second.title': 'confirm',
'p.title.color': '#ff00ff',
},
},
delimiters,
}
)
).toMatchSnapshot()
})
})
......@@ -2,18 +2,20 @@
Object.defineProperty(exports, '__esModule', { value: true });
const isArray = Array.isArray;
const isObject = (val) => val !== null && typeof val === 'object';
const defaultDelimiters = ['{', '}'];
class BaseFormatter {
constructor() {
this._caches = Object.create(null);
}
interpolate(message, values) {
interpolate(message, values, delimiters = defaultDelimiters) {
if (!values) {
return [message];
}
let tokens = this._caches[message];
if (!tokens) {
tokens = parse(message);
tokens = parse(message, delimiters);
this._caches[message] = tokens;
}
return compile(tokens, values);
......@@ -21,24 +23,24 @@ class BaseFormatter {
}
const RE_TOKEN_LIST_VALUE = /^(?:\d)+/;
const RE_TOKEN_NAMED_VALUE = /^(?:\w)+/;
function parse(format) {
function parse(format, [startDelimiter, endDelimiter]) {
const tokens = [];
let position = 0;
let text = '';
while (position < format.length) {
let char = format[position++];
if (char === '{') {
if (char === startDelimiter) {
if (text) {
tokens.push({ type: 'text', value: text });
}
text = '';
let sub = '';
char = format[position++];
while (char !== undefined && char !== '}') {
while (char !== undefined && char !== endDelimiter) {
sub += char;
char = format[position++];
}
const isClosed = char === '}';
const isClosed = char === endDelimiter;
const type = RE_TOKEN_LIST_VALUE.test(sub)
? 'list'
: isClosed && RE_TOKEN_NAMED_VALUE.test(sub)
......@@ -46,12 +48,12 @@ function parse(format) {
: 'unknown';
tokens.push({ value: sub, type });
}
else if (char === '%') {
// when found rails i18n syntax, skip text capture
if (format[position] !== '{') {
text += char;
}
}
// else if (char === '%') {
// // when found rails i18n syntax, skip text capture
// if (format[position] !== '{') {
// text += char
// }
// }
else {
text += char;
}
......@@ -62,7 +64,7 @@ function parse(format) {
function compile(tokens, values) {
const compiled = [];
let index = 0;
const mode = Array.isArray(values)
const mode = isArray(values)
? 'list'
: isObject(values)
? 'named'
......@@ -165,9 +167,12 @@ class I18n {
this.messages[this.locale] = {};
}
this.message = this.messages[this.locale];
this.watchers.forEach((watcher) => {
watcher(this.locale, oldLocale);
});
// 仅发生变化时,通知
if (oldLocale !== this.locale) {
this.watchers.forEach((watcher) => {
watcher(this.locale, oldLocale);
});
}
}
getLocale() {
return this.locale;
......@@ -283,6 +288,9 @@ function initVueI18n(locale = LOCALE_EN, messages = {}, fallbackLocale = LOCALE_
add(locale, message) {
return i18n.add(locale, message);
},
watch(fn) {
return i18n.watchLocale(fn);
},
getLocale() {
return i18n.getLocale();
},
......@@ -292,6 +300,111 @@ function initVueI18n(locale = LOCALE_EN, messages = {}, fallbackLocale = LOCALE_
};
}
const isString = (val) => typeof val === 'string';
let formater;
function hasI18nJson(jsonObj, delimiters) {
if (!formater) {
formater = new BaseFormatter();
}
return walkJsonObj(jsonObj, (jsonObj, key) => {
const value = jsonObj[key];
if (isString(value)) {
if (isI18nStr(value, delimiters)) {
return true;
}
}
else {
return hasI18nJson(value, delimiters);
}
});
}
function parseI18nJson(jsonObj, values, delimiters) {
if (!formater) {
formater = new BaseFormatter();
}
walkJsonObj(jsonObj, (jsonObj, key) => {
const value = jsonObj[key];
if (isString(value)) {
if (isI18nStr(value, delimiters)) {
jsonObj[key] = compileStr(value, values, delimiters);
}
}
else {
parseI18nJson(value, values, delimiters);
}
});
return jsonObj;
}
function compileI18nJsonStr(jsonStr, { locale, locales, delimiters, }) {
if (!isI18nStr(jsonStr, delimiters)) {
return jsonStr;
}
if (!formater) {
formater = new BaseFormatter();
}
const localeValues = [];
Object.keys(locales).forEach((name) => {
if (name !== locale) {
localeValues.push({
locale: name,
values: locales[name],
});
}
});
localeValues.unshift({ locale, values: locales[locale] });
try {
return JSON.stringify(compileJsonObj(JSON.parse(jsonStr), localeValues, delimiters), null, 2);
}
catch (e) { }
return jsonStr;
}
function isI18nStr(value, delimiters) {
return value.indexOf(delimiters[0]) > -1;
}
function compileStr(value, values, delimiters) {
return formater.interpolate(value, values, delimiters).join('');
}
function compileValue(jsonObj, key, localeValues, delimiters) {
const value = jsonObj[key];
if (isString(value)) {
// 存在国际化
if (isI18nStr(value, delimiters)) {
jsonObj[key] = compileStr(value, localeValues[0].values, delimiters);
// 格式化国际化语言
const valueLocales = (jsonObj[key + 'Locales'] = {});
localeValues.forEach((localValue) => {
valueLocales[localValue.locale] = compileStr(value, localValue.values, delimiters);
});
}
}
else {
compileJsonObj(value, localeValues, delimiters);
}
}
function compileJsonObj(jsonObj, localeValues, delimiters) {
walkJsonObj(jsonObj, (jsonObj, key) => {
compileValue(jsonObj, key, localeValues, delimiters);
});
return jsonObj;
}
function walkJsonObj(jsonObj, walk) {
if (isArray(jsonObj)) {
for (let i = 0; i < jsonObj.length; i++) {
if (walk(jsonObj, i)) {
return true;
}
}
}
else if (isObject(jsonObj)) {
for (const key in jsonObj) {
if (walk(jsonObj, key)) {
return true;
}
}
}
return false;
}
exports.Formatter = BaseFormatter;
exports.I18n = I18n;
exports.LOCALE_EN = LOCALE_EN;
......@@ -299,4 +412,8 @@ exports.LOCALE_ES = LOCALE_ES;
exports.LOCALE_FR = LOCALE_FR;
exports.LOCALE_ZH_HANS = LOCALE_ZH_HANS;
exports.LOCALE_ZH_HANT = LOCALE_ZH_HANT;
exports.compileI18nJsonStr = compileI18nJsonStr;
exports.hasI18nJson = hasI18nJson;
exports.initVueI18n = initVueI18n;
exports.isString = isString;
exports.parseI18nJson = parseI18nJson;
export declare type BuiltInLocale = typeof LOCALE_ZH_HANS | typeof LOCALE_ZH_HANT | typeof LOCALE_EN | typeof LOCALE_FR | typeof LOCALE_ES;
export declare function compileI18nJsonStr(jsonStr: string, { locale, locales, delimiters, }: {
locale: string;
locales: Record<string, Record<string, string>>;
delimiters: [string, string];
}): string;
export declare class Formatter {
_caches: {
[key: string]: Array<Token>;
};
constructor();
interpolate(message: string, values?: Record<string, unknown> | Array<unknown>): Array<unknown>;
interpolate(message: string, values?: Record<string, unknown> | Array<unknown>, delimiters?: [string, string]): Array<unknown>;
}
declare interface Formatter_2 {
interpolate: (message: string, values?: Record<string, unknown> | Array<unknown>) => Array<unknown>;
interpolate: (message: string, values?: Record<string, unknown> | Array<unknown>, delimiters?: [string, string]) => Array<unknown>;
}
export declare function hasI18nJson(jsonObj: unknown, delimiters: [string, string]): boolean;
export declare class I18n {
private locale;
private fallbackLocale;
......@@ -40,10 +48,13 @@ export declare function initVueI18n(locale?: BuiltInLocale, messages?: LocaleMes
i18n: I18n;
t(key: string, values?: Record<string, unknown> | unknown[] | undefined): string;
add(locale: BuiltInLocale, message: Record<string, string>): void;
watch(fn: LocaleWatcher): () => void;
getLocale(): BuiltInLocale;
setLocale(newLocale: BuiltInLocale): void;
};
export declare const isString: (val: unknown) => val is string;
export declare const LOCALE_EN = "en";
export declare const LOCALE_ES = "es";
......@@ -60,6 +71,8 @@ export declare type LocaleMessages = {
export declare type LocaleWatcher = (newLocale: BuiltInLocale, oldLocale: BuiltInLocale) => void;
export declare function parseI18nJson(jsonObj: unknown, values: Record<string, string>, delimiters: [string, string]): unknown;
declare type Token = {
type: 'text' | 'named' | 'list' | 'unknown';
value: string;
......
const isArray = Array.isArray;
const isObject = (val) => val !== null && typeof val === 'object';
const defaultDelimiters = ['{', '}'];
class BaseFormatter {
constructor() {
this._caches = Object.create(null);
}
interpolate(message, values) {
interpolate(message, values, delimiters = defaultDelimiters) {
if (!values) {
return [message];
}
let tokens = this._caches[message];
if (!tokens) {
tokens = parse(message);
tokens = parse(message, delimiters);
this._caches[message] = tokens;
}
return compile(tokens, values);
......@@ -17,24 +19,24 @@ class BaseFormatter {
}
const RE_TOKEN_LIST_VALUE = /^(?:\d)+/;
const RE_TOKEN_NAMED_VALUE = /^(?:\w)+/;
function parse(format) {
function parse(format, [startDelimiter, endDelimiter]) {
const tokens = [];
let position = 0;
let text = '';
while (position < format.length) {
let char = format[position++];
if (char === '{') {
if (char === startDelimiter) {
if (text) {
tokens.push({ type: 'text', value: text });
}
text = '';
let sub = '';
char = format[position++];
while (char !== undefined && char !== '}') {
while (char !== undefined && char !== endDelimiter) {
sub += char;
char = format[position++];
}
const isClosed = char === '}';
const isClosed = char === endDelimiter;
const type = RE_TOKEN_LIST_VALUE.test(sub)
? 'list'
: isClosed && RE_TOKEN_NAMED_VALUE.test(sub)
......@@ -42,12 +44,12 @@ function parse(format) {
: 'unknown';
tokens.push({ value: sub, type });
}
else if (char === '%') {
// when found rails i18n syntax, skip text capture
if (format[position] !== '{') {
text += char;
}
}
// else if (char === '%') {
// // when found rails i18n syntax, skip text capture
// if (format[position] !== '{') {
// text += char
// }
// }
else {
text += char;
}
......@@ -58,7 +60,7 @@ function parse(format) {
function compile(tokens, values) {
const compiled = [];
let index = 0;
const mode = Array.isArray(values)
const mode = isArray(values)
? 'list'
: isObject(values)
? 'named'
......@@ -161,9 +163,12 @@ class I18n {
this.messages[this.locale] = {};
}
this.message = this.messages[this.locale];
this.watchers.forEach((watcher) => {
watcher(this.locale, oldLocale);
});
// 仅发生变化时,通知
if (oldLocale !== this.locale) {
this.watchers.forEach((watcher) => {
watcher(this.locale, oldLocale);
});
}
}
getLocale() {
return this.locale;
......@@ -279,6 +284,9 @@ function initVueI18n(locale = LOCALE_EN, messages = {}, fallbackLocale = LOCALE_
add(locale, message) {
return i18n.add(locale, message);
},
watch(fn) {
return i18n.watchLocale(fn);
},
getLocale() {
return i18n.getLocale();
},
......@@ -288,4 +296,109 @@ function initVueI18n(locale = LOCALE_EN, messages = {}, fallbackLocale = LOCALE_
};
}
export { BaseFormatter as Formatter, I18n, LOCALE_EN, LOCALE_ES, LOCALE_FR, LOCALE_ZH_HANS, LOCALE_ZH_HANT, initVueI18n };
const isString = (val) => typeof val === 'string';
let formater;
function hasI18nJson(jsonObj, delimiters) {
if (!formater) {
formater = new BaseFormatter();
}
return walkJsonObj(jsonObj, (jsonObj, key) => {
const value = jsonObj[key];
if (isString(value)) {
if (isI18nStr(value, delimiters)) {
return true;
}
}
else {
return hasI18nJson(value, delimiters);
}
});
}
function parseI18nJson(jsonObj, values, delimiters) {
if (!formater) {
formater = new BaseFormatter();
}
walkJsonObj(jsonObj, (jsonObj, key) => {
const value = jsonObj[key];
if (isString(value)) {
if (isI18nStr(value, delimiters)) {
jsonObj[key] = compileStr(value, values, delimiters);
}
}
else {
parseI18nJson(value, values, delimiters);
}
});
return jsonObj;
}
function compileI18nJsonStr(jsonStr, { locale, locales, delimiters, }) {
if (!isI18nStr(jsonStr, delimiters)) {
return jsonStr;
}
if (!formater) {
formater = new BaseFormatter();
}
const localeValues = [];
Object.keys(locales).forEach((name) => {
if (name !== locale) {
localeValues.push({
locale: name,
values: locales[name],
});
}
});
localeValues.unshift({ locale, values: locales[locale] });
try {
return JSON.stringify(compileJsonObj(JSON.parse(jsonStr), localeValues, delimiters), null, 2);
}
catch (e) { }
return jsonStr;
}
function isI18nStr(value, delimiters) {
return value.indexOf(delimiters[0]) > -1;
}
function compileStr(value, values, delimiters) {
return formater.interpolate(value, values, delimiters).join('');
}
function compileValue(jsonObj, key, localeValues, delimiters) {
const value = jsonObj[key];
if (isString(value)) {
// 存在国际化
if (isI18nStr(value, delimiters)) {
jsonObj[key] = compileStr(value, localeValues[0].values, delimiters);
// 格式化国际化语言
const valueLocales = (jsonObj[key + 'Locales'] = {});
localeValues.forEach((localValue) => {
valueLocales[localValue.locale] = compileStr(value, localValue.values, delimiters);
});
}
}
else {
compileJsonObj(value, localeValues, delimiters);
}
}
function compileJsonObj(jsonObj, localeValues, delimiters) {
walkJsonObj(jsonObj, (jsonObj, key) => {
compileValue(jsonObj, key, localeValues, delimiters);
});
return jsonObj;
}
function walkJsonObj(jsonObj, walk) {
if (isArray(jsonObj)) {
for (let i = 0; i < jsonObj.length; i++) {
if (walk(jsonObj, i)) {
return true;
}
}
}
else if (isObject(jsonObj)) {
for (const key in jsonObj) {
if (walk(jsonObj, key)) {
return true;
}
}
}
return false;
}
export { BaseFormatter as Formatter, I18n, LOCALE_EN, LOCALE_ES, LOCALE_FR, LOCALE_ZH_HANS, LOCALE_ZH_HANT, compileI18nJsonStr, hasI18nJson, initVueI18n, isString, parseI18nJson };
......@@ -21,7 +21,8 @@ export type LocaleMessages = {
export interface Formatter {
interpolate: (
message: string,
values?: Record<string, unknown> | Array<unknown>
values?: Record<string, unknown> | Array<unknown>,
delimiters?: [string, string]
) => Array<unknown>
}
......@@ -113,9 +114,12 @@ export class I18n {
this.messages[this.locale] = {}
}
this.message = this.messages[this.locale]!
this.watchers.forEach((watcher) => {
watcher(this.locale, oldLocale)
})
// 仅发生变化时,通知
if (oldLocale !== this.locale) {
this.watchers.forEach((watcher) => {
watcher(this.locale, oldLocale)
})
}
}
getLocale() {
return this.locale
......
const isObject = (val: unknown): val is Record<any, any> =>
export const isArray = Array.isArray
export const isObject = (val: unknown): val is Record<any, any> =>
val !== null && typeof val === 'object'
export const defaultDelimiters: [string, string] = ['{', '}']
export default class BaseFormatter {
_caches: { [key: string]: Array<Token> }
......@@ -10,14 +12,15 @@ export default class BaseFormatter {
interpolate(
message: string,
values?: Record<string, unknown> | Array<unknown>
values?: Record<string, unknown> | Array<unknown>,
delimiters: [string, string] = defaultDelimiters
): Array<unknown> {
if (!values) {
return [message]
}
let tokens: Array<Token> = this._caches[message]
if (!tokens) {
tokens = parse(message)
tokens = parse(message, delimiters)
this._caches[message] = tokens
}
return compile(tokens, values)
......@@ -32,14 +35,17 @@ type Token = {
const RE_TOKEN_LIST_VALUE: RegExp = /^(?:\d)+/
const RE_TOKEN_NAMED_VALUE: RegExp = /^(?:\w)+/
export function parse(format: string): Array<Token> {
export function parse(
format: string,
[startDelimiter, endDelimiter]: [string, string]
): Array<Token> {
const tokens: Array<Token> = []
let position: number = 0
let text: string = ''
while (position < format.length) {
let char: string = format[position++]
if (char === '{') {
if (char === startDelimiter) {
if (text) {
tokens.push({ type: 'text', value: text })
}
......@@ -47,11 +53,11 @@ export function parse(format: string): Array<Token> {
text = ''
let sub: string = ''
char = format[position++]
while (char !== undefined && char !== '}') {
while (char !== undefined && char !== endDelimiter) {
sub += char
char = format[position++]
}
const isClosed = char === '}'
const isClosed = char === endDelimiter
const type = RE_TOKEN_LIST_VALUE.test(sub)
? 'list'
......@@ -59,12 +65,14 @@ export function parse(format: string): Array<Token> {
? 'named'
: 'unknown'
tokens.push({ value: sub, type })
} else if (char === '%') {
// when found rails i18n syntax, skip text capture
if (format[position] !== '{') {
text += char
}
} else {
}
// else if (char === '%') {
// // when found rails i18n syntax, skip text capture
// if (format[position] !== '{') {
// text += char
// }
// }
else {
text += char
}
}
......@@ -81,7 +89,7 @@ export function compile(
const compiled: Array<unknown> = []
let index: number = 0
const mode: string = Array.isArray(values)
const mode: string = isArray(values)
? 'list'
: isObject(values)
? 'named'
......
export * from './I18n'
export * from './vue-i18n'
export * from './json'
export { default as Formatter } from './format'
import { isArray, isObject, default as BaseFormatter } from './format'
import { Formatter } from './I18n'
export const isString = (val: unknown): val is string => typeof val === 'string'
let formater: Formatter | null
interface LocaleValue {
locale: string
values: Record<string, unknown>
}
type LocaleValues = LocaleValue[]
export function hasI18nJson(
jsonObj: unknown,
delimiters: [string, string]
): boolean {
if (!formater) {
formater = new BaseFormatter()
}
return walkJsonObj(jsonObj, (jsonObj, key) => {
const value = (jsonObj as Record<string | number, unknown>)[key]
if (isString(value)) {
if (isI18nStr(value, delimiters)) {
return true
}
} else {
return hasI18nJson(value, delimiters)
}
})
}
export function parseI18nJson(
jsonObj: unknown,
values: Record<string, string>,
delimiters: [string, string]
) {
if (!formater) {
formater = new BaseFormatter()
}
walkJsonObj(jsonObj, (jsonObj, key) => {
const value = (jsonObj as Record<string | number, unknown>)[key]
if (isString(value)) {
if (isI18nStr(value, delimiters)) {
;(jsonObj as Record<string | number, unknown>)[key] = compileStr(
value,
values,
delimiters
)
}
} else {
parseI18nJson(value, values, delimiters)
}
})
return jsonObj
}
export function compileI18nJsonStr(
jsonStr: string,
{
locale,
locales,
delimiters,
}: {
locale: string
locales: Record<string, Record<string, string>>
delimiters: [string, string]
}
) {
if (!isI18nStr(jsonStr, delimiters)) {
return jsonStr
}
if (!formater) {
formater = new BaseFormatter()
}
const localeValues: LocaleValues = []
Object.keys(locales).forEach((name) => {
if (name !== locale) {
localeValues.push({
locale: name,
values: locales[name],
})
}
})
localeValues.unshift({ locale, values: locales[locale] })
try {
return JSON.stringify(
compileJsonObj(JSON.parse(jsonStr), localeValues, delimiters),
null,
2
)
} catch (e) {}
return jsonStr
}
function isI18nStr(value: string, delimiters: [string, string]) {
return value.indexOf(delimiters[0]) > -1
}
function compileStr(
value: string,
values: LocaleValue['values'],
delimiters: [string, string]
) {
return formater!.interpolate(value, values, delimiters).join('')
}
function compileValue(
jsonObj: Record<string, unknown> | unknown[],
key: string | number,
localeValues: LocaleValues,
delimiters: [string, string]
) {
const value = (jsonObj as Record<string | number, unknown>)[key]
if (isString(value)) {
// 存在国际化
if (isI18nStr(value, delimiters)) {
// 格式化默认语言
;(jsonObj as Record<string | number, unknown>)[key] = compileStr(
value,
localeValues[0].values,
delimiters
)
// 格式化国际化语言
const valueLocales: Record<string, string> = ((
jsonObj as Record<string | number, unknown>
)[key + 'Locales'] = {})
localeValues.forEach((localValue) => {
valueLocales[localValue.locale] = compileStr(
value,
localValue.values,
delimiters
)
})
}
} else {
compileJsonObj(value, localeValues, delimiters)
}
}
function compileJsonObj(
jsonObj: unknown,
localeValues: LocaleValues,
delimiters: [string, string]
) {
walkJsonObj(jsonObj, (jsonObj, key) => {
compileValue(jsonObj, key, localeValues, delimiters)
})
return jsonObj
}
type WalkJson = (
jsonObj: unknown[] | Record<string, unknown>,
key: number | string
) => void | boolean
function walkJsonObj(jsonObj: unknown, walk: WalkJson) {
if (isArray(jsonObj)) {
for (let i = 0; i < jsonObj.length; i++) {
if (walk(jsonObj, i)) {
return true
}
}
} else if (isObject(jsonObj)) {
for (const key in jsonObj) {
if (walk(jsonObj, key)) {
return true
}
}
}
return false
}
import { I18n, BuiltInLocale, LocaleMessages, LOCALE_EN } from './I18n'
import {
I18n,
BuiltInLocale,
LocaleMessages,
LOCALE_EN,
LocaleWatcher,
} from './I18n'
const ignoreVueI18n = true
......@@ -95,6 +101,9 @@ export function initVueI18n(
add(locale: BuiltInLocale, message: Record<string, string>) {
return i18n.add(locale, message)
},
watch(fn: LocaleWatcher) {
return i18n.watchLocale(fn)
},
getLocale() {
return i18n.getLocale()
},
......
......@@ -948,6 +948,7 @@ const TABBAR_HEIGHT = 50;
const ON_REACH_BOTTOM_DISTANCE = 50;
const RESPONSIVE_MIN_WIDTH = 768;
const COMPONENT_NAME_PREFIX = 'VUni';
const I18N_JSON_DELIMITERS = ['%', '%'];
const PRIMARY_COLOR = '#007aff';
const SELECTED_COLOR = '#0062cc'; // 选中的颜色,如选项卡默认的选中颜色
const BACKGROUND_COLOR = '#f7f7f7'; // 背景色,如标题栏默认背景色
......@@ -1134,6 +1135,7 @@ exports.COMPONENT_SELECTOR_PREFIX = COMPONENT_SELECTOR_PREFIX;
exports.DATA_RE = DATA_RE;
exports.EventChannel = EventChannel;
exports.EventModifierFlags = EventModifierFlags;
exports.I18N_JSON_DELIMITERS = I18N_JSON_DELIMITERS;
exports.JSON_PROTOCOL = JSON_PROTOCOL;
exports.NAVBAR_HEIGHT = NAVBAR_HEIGHT;
exports.NODE_TYPE_COMMENT = NODE_TYPE_COMMENT;
......
......@@ -186,6 +186,8 @@ declare interface HTMLElementWithDataset extends HTMLElement {
__uniDataset?: Record<string, any>;
}
export declare const I18N_JSON_DELIMITERS: [string, string];
export declare function initCustomDataset(): void;
/**
......
......@@ -944,6 +944,7 @@ const TABBAR_HEIGHT = 50;
const ON_REACH_BOTTOM_DISTANCE = 50;
const RESPONSIVE_MIN_WIDTH = 768;
const COMPONENT_NAME_PREFIX = 'VUni';
const I18N_JSON_DELIMITERS = ['%', '%'];
const PRIMARY_COLOR = '#007aff';
const SELECTED_COLOR = '#0062cc'; // 选中的颜色,如选项卡默认的选中颜色
const BACKGROUND_COLOR = '#f7f7f7'; // 背景色,如标题栏默认背景色
......@@ -1101,4 +1102,4 @@ function getEnvLocale() {
return (lang && lang.replace(/[.:].*/, '')) || 'en';
}
export { ACTION_TYPE_ADD_EVENT, ACTION_TYPE_ADD_WXS_EVENT, ACTION_TYPE_CREATE, ACTION_TYPE_EVENT, ACTION_TYPE_INSERT, ACTION_TYPE_PAGE_CREATE, ACTION_TYPE_PAGE_CREATED, ACTION_TYPE_PAGE_SCROLL, ACTION_TYPE_REMOVE, ACTION_TYPE_REMOVE_ATTRIBUTE, ACTION_TYPE_REMOVE_EVENT, ACTION_TYPE_SET_ATTRIBUTE, ACTION_TYPE_SET_TEXT, ATTR_CHANGE_PREFIX, ATTR_CLASS, ATTR_INNER_HTML, ATTR_STYLE, ATTR_TEXT_CONTENT, ATTR_V_OWNER_ID, ATTR_V_RENDERJS, ATTR_V_SHOW, BACKGROUND_COLOR, BUILT_IN_TAGS, COMPONENT_NAME_PREFIX, COMPONENT_PREFIX, COMPONENT_SELECTOR_PREFIX, DATA_RE, EventChannel, EventModifierFlags, JSON_PROTOCOL, NAVBAR_HEIGHT, NODE_TYPE_COMMENT, NODE_TYPE_ELEMENT, NODE_TYPE_PAGE, NODE_TYPE_TEXT, ON_ADD_TO_FAVORITES, ON_APP_ENTER_BACKGROUND, ON_APP_ENTER_FOREGROUND, ON_BACK_PRESS, ON_ERROR, ON_HIDE, ON_KEYBOARD_HEIGHT_CHANGE, ON_LAUNCH, ON_LOAD, ON_NAVIGATION_BAR_BUTTON_TAP, ON_NAVIGATION_BAR_SEARCH_INPUT_CHANGED, ON_NAVIGATION_BAR_SEARCH_INPUT_CLICKED, ON_NAVIGATION_BAR_SEARCH_INPUT_CONFIRMED, ON_NAVIGATION_BAR_SEARCH_INPUT_FOCUS_CHANGED, ON_PAGE_NOT_FOUND, ON_PAGE_SCROLL, ON_PULL_DOWN_REFRESH, ON_REACH_BOTTOM, ON_REACH_BOTTOM_DISTANCE, ON_READY, ON_RESIZE, ON_SHARE_APP_MESSAGE, ON_SHARE_TIMELINE, ON_SHOW, ON_TAB_ITEM_TAP, ON_THEME_CHANGE, ON_UNHANDLE_REJECTION, ON_UNLOAD, ON_WEB_INVOKE_APP_SERVICE, ON_WXS_INVOKE_CALL_METHOD, PLUS_RE, PRIMARY_COLOR, RENDERJS_MODULES, RESPONSIVE_MIN_WIDTH, SCHEME_RE, SELECTED_COLOR, TABBAR_HEIGHT, TAGS, UNI_SSR, UNI_SSR_DATA, UNI_SSR_GLOBAL_DATA, UNI_SSR_STORE, UNI_SSR_TITLE, UniBaseNode, UniCommentNode, UniElement, UniEvent, UniInputElement, UniLifecycleHooks, UniNode, UniTextAreaElement, UniTextNode, WEB_INVOKE_APPSERVICE, WXS_MODULES, WXS_PROTOCOL, addFont, cache, cacheStringFunction, callOptions, createRpx2Unit, createUniEvent, debounce, decode, decodedQuery, defaultRpx2Unit, formatAppLog, formatDateTime, formatLog, getCustomDataset, getEnvLocale, getLen, getValueByDataPath, initCustomDataset, invokeArrayFns, isBuiltInComponent, isCustomElement, isNativeTag, isRootHook, isServiceCustomElement, isServiceNativeTag, normalizeDataset, normalizeEventType, normalizeTarget, once, parseEventName, parseQuery, parseUrl, passive, plusReady, removeLeadingSlash, resolveOwnerEl, resolveOwnerVm, sanitise, scrollTo, stringifyQuery, updateElementStyle };
export { ACTION_TYPE_ADD_EVENT, ACTION_TYPE_ADD_WXS_EVENT, ACTION_TYPE_CREATE, ACTION_TYPE_EVENT, ACTION_TYPE_INSERT, ACTION_TYPE_PAGE_CREATE, ACTION_TYPE_PAGE_CREATED, ACTION_TYPE_PAGE_SCROLL, ACTION_TYPE_REMOVE, ACTION_TYPE_REMOVE_ATTRIBUTE, ACTION_TYPE_REMOVE_EVENT, ACTION_TYPE_SET_ATTRIBUTE, ACTION_TYPE_SET_TEXT, ATTR_CHANGE_PREFIX, ATTR_CLASS, ATTR_INNER_HTML, ATTR_STYLE, ATTR_TEXT_CONTENT, ATTR_V_OWNER_ID, ATTR_V_RENDERJS, ATTR_V_SHOW, BACKGROUND_COLOR, BUILT_IN_TAGS, COMPONENT_NAME_PREFIX, COMPONENT_PREFIX, COMPONENT_SELECTOR_PREFIX, DATA_RE, EventChannel, EventModifierFlags, I18N_JSON_DELIMITERS, JSON_PROTOCOL, NAVBAR_HEIGHT, NODE_TYPE_COMMENT, NODE_TYPE_ELEMENT, NODE_TYPE_PAGE, NODE_TYPE_TEXT, ON_ADD_TO_FAVORITES, ON_APP_ENTER_BACKGROUND, ON_APP_ENTER_FOREGROUND, ON_BACK_PRESS, ON_ERROR, ON_HIDE, ON_KEYBOARD_HEIGHT_CHANGE, ON_LAUNCH, ON_LOAD, ON_NAVIGATION_BAR_BUTTON_TAP, ON_NAVIGATION_BAR_SEARCH_INPUT_CHANGED, ON_NAVIGATION_BAR_SEARCH_INPUT_CLICKED, ON_NAVIGATION_BAR_SEARCH_INPUT_CONFIRMED, ON_NAVIGATION_BAR_SEARCH_INPUT_FOCUS_CHANGED, ON_PAGE_NOT_FOUND, ON_PAGE_SCROLL, ON_PULL_DOWN_REFRESH, ON_REACH_BOTTOM, ON_REACH_BOTTOM_DISTANCE, ON_READY, ON_RESIZE, ON_SHARE_APP_MESSAGE, ON_SHARE_TIMELINE, ON_SHOW, ON_TAB_ITEM_TAP, ON_THEME_CHANGE, ON_UNHANDLE_REJECTION, ON_UNLOAD, ON_WEB_INVOKE_APP_SERVICE, ON_WXS_INVOKE_CALL_METHOD, PLUS_RE, PRIMARY_COLOR, RENDERJS_MODULES, RESPONSIVE_MIN_WIDTH, SCHEME_RE, SELECTED_COLOR, TABBAR_HEIGHT, TAGS, UNI_SSR, UNI_SSR_DATA, UNI_SSR_GLOBAL_DATA, UNI_SSR_STORE, UNI_SSR_TITLE, UniBaseNode, UniCommentNode, UniElement, UniEvent, UniInputElement, UniLifecycleHooks, UniNode, UniTextAreaElement, UniTextNode, WEB_INVOKE_APPSERVICE, WXS_MODULES, WXS_PROTOCOL, addFont, cache, cacheStringFunction, callOptions, createRpx2Unit, createUniEvent, debounce, decode, decodedQuery, defaultRpx2Unit, formatAppLog, formatDateTime, formatLog, getCustomDataset, getEnvLocale, getLen, getValueByDataPath, initCustomDataset, invokeArrayFns, isBuiltInComponent, isCustomElement, isNativeTag, isRootHook, isServiceCustomElement, isServiceNativeTag, normalizeDataset, normalizeEventType, normalizeTarget, once, parseEventName, parseQuery, parseUrl, passive, plusReady, removeLeadingSlash, resolveOwnerEl, resolveOwnerVm, sanitise, scrollTo, stringifyQuery, updateElementStyle };
......@@ -5,6 +5,8 @@ export const RESPONSIVE_MIN_WIDTH = 768
export const COMPONENT_NAME_PREFIX = 'VUni'
export const I18N_JSON_DELIMITERS: [string, string] = ['%', '%']
export const PRIMARY_COLOR = '#007aff'
export const SELECTED_COLOR = '#0062cc' // 选中的颜色,如选项卡默认的选中颜色
export const BACKGROUND_COLOR = '#f7f7f7' // 背景色,如标题栏默认背景色
......
'use strict';
var version = "3.0.0-alpha-3000020210827003";
var version = "3.0.0-alpha-3000020210827004";
const STAT_VERSION = version;
const STAT_URL = 'https://tongji.dcloud.io/uni/stat';
......
var version = "3.0.0-alpha-3000020210827003";
var version = "3.0.0-alpha-3000020210827004";
const STAT_VERSION = version;
const STAT_URL = 'https://tongji.dcloud.io/uni/stat';
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册