diff --git a/packages/editor-preset-vision/src/i18n-reducer.ts b/packages/editor-preset-vision/src/deep-value-parser.ts similarity index 62% rename from packages/editor-preset-vision/src/i18n-reducer.ts rename to packages/editor-preset-vision/src/deep-value-parser.ts index fb792d810..227e5da09 100644 --- a/packages/editor-preset-vision/src/i18n-reducer.ts +++ b/packages/editor-preset-vision/src/deep-value-parser.ts @@ -1,21 +1,18 @@ import Env from './env'; import { isJSSlot, isI18nData, isJSExpression } from '@ali/lowcode-types'; import { isPlainObject } from '@ali/lowcode-utils'; -const I18nUtil = require('@ali/ve-i18n-util'); +import i18nUtil from './i18n-util'; -interface I18nObject { - type?: string; - use?: string; - key?: string; - [lang: string]: string | undefined; -} - -export function i18nReducer(obj?: any): any { +// FIXME: 表达式使用 mock 值,未来live 模式直接使用原始值 +export function deepValueParser(obj?: any): any { + if (isJSExpression(obj)) { + obj = obj.mock; + } if (!obj) { return obj; } if (Array.isArray(obj)) { - return obj.map((item) => i18nReducer(item)); + return obj.map((item) => deepValueParser(item)); } if (isPlainObject(obj)) { if (isI18nData(obj)) { @@ -23,19 +20,20 @@ export function i18nReducer(obj?: any): any { let locale = Env.getLocale(); if (obj.key) { // FIXME: 此处需要升级I18nUtil,改成响应式 - return I18nUtil.get(obj.key, locale); + return i18nUtil.get(obj.key, locale); } if (locale !== 'zh_CN' && locale !== 'zh_TW' && !obj[locale]) { locale = 'en_US'; } return obj[obj.use || locale] || obj.zh_CN; } - if (isJSSlot(obj) || isJSExpression(obj)) { + + if (isJSSlot(obj)) { return obj; } - const out: I18nObject = {}; + const out: any = {}; Object.keys(obj).forEach((key) => { - out[key] = i18nReducer(obj[key]); + out[key] = deepValueParser(obj[key]); }); return out; } diff --git a/packages/editor-preset-vision/src/i18n-util/index.d.ts b/packages/editor-preset-vision/src/i18n-util/index.d.ts new file mode 100644 index 000000000..d258fe9d2 --- /dev/null +++ b/packages/editor-preset-vision/src/i18n-util/index.d.ts @@ -0,0 +1,79 @@ +declare enum LANGUAGES { + zh_CN = 'zh_CN', + en_US = 'en_US' +} + +export interface I18nRecord { + type?: 'i18n'; + [key: string]: string; + /** + * i18n unique key + */ + key?: string; +} + +export interface I18nRecordData { + gmtCreate: Date; + gmtModified: Date; + i18nKey: string; + i18nText: I18nRecord; + id: number; +} + +export interface II18nUtilConfigs { + items?: {}; + /** + * 是否禁用初始化加载 + */ + disableInstantLoad?: boolean; + /** + * 初始化的时候是否全量加载 + */ + disableFullLoad?: boolean; + loader?: (configs: ILoaderConfigs) => Promise; + remover?: (key: string, dic: I18nRecord) => Promise; + saver?: (key: string, dic: I18nRecord) => Promise; +} + +export interface ILoaderConfigs { + /** + * search keywords + */ + keyword?: string; + /** + * should load all i18n items + */ + isFull?: boolean; + /** + * search i18n item based on uniqueKey + */ + key?: string; +} + +export interface II18nUtil { + init(config: II18nUtilConfigs): void; + isInitialized(): boolean; + isReady(): boolean; + attach(prop: object, value: I18nRecord, updator: () => any); + search(keyword: string, silent?: boolean); + load(configs: ILoaderConfigs): Promise; + /** + * Get local i18n Record + * @param key + * @param lang + */ + get(key: string, lang: string): string | I18nRecord; + getFromRemote(key: string): Promise; + getItem(key: string, forceData?: boolean): any; + getItems(): I18nRecord[]; + update(key: string, doc: I18nRecord, lang: LANGUAGES); + create(doc: I18nRecord, lang: LANGUAGES): string; + remove(key: string): Promise; + + onReady(func: () => any); + onRowsChange(func: () => any); + onChange(func: (dic: I18nRecord) => any); +} + +declare const i18nUtil: II18nUtil; +export default i18nUtil; diff --git a/packages/editor-preset-vision/src/i18n-util/index.js b/packages/editor-preset-vision/src/i18n-util/index.js new file mode 100644 index 000000000..06bd973ac --- /dev/null +++ b/packages/editor-preset-vision/src/i18n-util/index.js @@ -0,0 +1,310 @@ +import { EventEmitter } from 'events'; +import { obx } from '@ali/lowcode-editor-core'; + + +let keybase = Date.now(); +function keygen(maps) { + let key; + do { + key = `i18n-${(keybase).toString(36)}`; + keybase += 1; + } while (key in maps); + return key; +} + +class DocItem { + constructor(parent, doc, unInitial) { + this.parent = parent; + const { use, ...strings } = doc; + this.doc = obx.val({ + type: 'i18n', + ...strings, + }); + this.emitter = new EventEmitter; + this.inited = unInitial !== true; + } + + getKey() { + return this.doc.key; + } + + getDoc(lang) { + if (lang) { + return this.doc[lang]; + } + return this.doc; + } + + setDoc(doc, lang, initial) { + if (lang) { + this.doc[lang] = doc; + } else { + const { use, strings } = doc || {}; + Object.assign(this.doc, strings); + } + this.emitter.emit('change', this.doc); + + if (initial) { + this.inited = true; + } else if (this.inited) { + this.parent._saveChange(this.doc.key, this.doc); + } + } + + remove() { + if (!this.inited) return Promise.reject('not initialized'); + + const { key, ...doc } = this.doc; // eslint-disable-line + this.emitter.emit('change', doc); + return this.parent.remove(this.getKey()); + } + + onChange(func) { + this.emitter.on('change', func); + return () => { + this.emitter.removeListener('change', func); + }; + } +} + +class I18nUtil { + constructor() { + this.emitter = new EventEmitter; + // original data source from remote + this.i18nData = {}; + // current i18n records on the left pane + this.items = []; + this.maps = {}; + // full list of i18n records for synchronized call + this.fullList = []; + this.fullMap = {}; + + this.config = {}; + this.ready = false; + this.isInited = false; + } + + _prepareItems(items, isFull = false, isSilent = false) { + this[isFull ? 'fullList' : 'items'] = items.map((dict) => { + let item = this[isFull ? 'fullMap' : 'maps'][dict.key]; + if (item) { + item.setDoc(dict, null, true); + } else { + item = new DocItem(this, dict); + this[isFull ? 'fullMap' : 'maps'][dict.key] = item; + } + return item; + }); + + if (this.ready && !isSilent) { + this.emitter.emit('rowschange'); + this.emitter.emit('change'); + } else { + this.ready = true; + this.emitter.emit('ready'); + } + } + + _load(configs = {}, silent) { + if (!this.config.loader) { + console.error(new Error('Please load loader while init I18nUtil.')); + return Promise.reject(); + } + + return this.config.loader(configs).then((data) => { + if (configs.i18nKey) { + return Promise.resolve(data.i18nText); + } + this._prepareItems(data.data, configs.isFull, silent); + // set pagination data to i18nData + this.i18nData = data; + if (!silent) { + this.emitter.emit('rowschange'); + this.emitter.emit('change'); + } + return Promise.resolve(this.items.map(i => i.getDoc())); + }); + } + + _saveToItems(key, dict) { + let item = null; + item = this.items.find(doc => doc.getKey() === key); + if (!item) { + item = this.fullList.find(doc => doc.getKey() === key); + } + + if (item) { + item.setDoc(dict); + } else { + item = new DocItem(this, { + key, + ...dict, + }); + this.items.unshift(item); + this.fullList.unshift(item); + this.maps[key] = item; + this.fullMap[key] = item; + this._saveChange(key, dict, true); + } + } + + _saveChange(key, dict, rowschange) { + if (rowschange) { + this.emitter.emit('rowschange'); + } + this.emitter.emit('change'); + if (dict === null) { + delete this.maps[key]; + delete this.fullMap[key]; + } + return this._save(key, dict); + } + + _save(key, dict) { + const saver = dict === null ? this.config.remover : this.config.saver; + if (!saver) return Promise.reject('Saver function is not set'); + return saver(key, dict); + } + + init(config) { + if (this.isInited) return; + this.config = config || {}; + if (this.config.items) { + // inject to current page + this._prepareItems(this.config.items); + } + if (!this.config.disableInstantLoad) { + this._load({ isFull: !this.config.disableFullLoad }); + } + this.isInited = true; + } + + isInitialized() { + return this.isInited; + } + + isReady() { + return this.ready; + } + + // add events updater when i18n record change + // we should notify engine's view to change + attach(prop, value, updator) { + const isI18nValue = value && value.type === 'i18n' && value.key; + const key = isI18nValue ? value.key : null; + if (prop.i18nLink) { + if (isI18nValue && (key === prop.i18nLink.key)) { + return prop.i18nLink; + } + prop.i18nLink.detach(); + } + + if (isI18nValue) { + return { + key, + detach: this.getItem(key, value).onChange(updator), + }; + } + + return null; + } + + /** + * 搜索 i18n 词条 + * + * @param {any} keyword 搜索关键字 + * @param {boolean} [silent=false] 是否刷新左侧的 i18n 数据 + * @returns + * + * @memberof I18nUtil + */ + search(keyword, silent = false) { + return this._load({ keyword }, silent); + } + + load(configs = {}) { + return this._load(configs); + } + + get(key, lang) { + const item = this.getItem(key); + if (item) { + return item.getDoc(lang); + } + return null; + } + + getFromRemote(key) { + return this._load({ i18nKey: key }); + } + + getItem(key, forceData) { + if (forceData && !this.maps[key] && !this.fullList[key]) { + const item = new DocItem(this, { + key, + ...forceData, + }, true); + this.maps[key] = item; + this.fullMap[key] = item; + this.fullList.push(item); + this.items.push(item); + } + return this.maps[key] || this.fullMap[key]; + } + + getItems() { + return this.items; + } + + update(key, doc, lang) { + let dict = this.get(key) || {}; + if (!lang) { + dict = doc; + } else { + dict[lang] = doc; + } + this._saveToItems(key, dict); + } + + create(doc, lang) { + const dict = lang ? { [lang]: doc } : doc; + const key = keygen(this.fullMap); + this._saveToItems(key, dict); + return key; + } + + remove(key) { + const index = this.items.findIndex(item => item.getKey() === key); + const indexG = this.fullList.findIndex(item => item.getKey() === key); + if (index > -1) { + this.items.splice(index, 1); + } + if (indexG > -1) { + this.fullList.splice(index, 1); + } + return this._saveChange(key, null, true); + } + + onReady(func) { + this.emitter.on('ready', func); + return () => { + this.emitter.removeListener('ready', func); + }; + } + + onRowsChange(func) { + this.emitter.on('rowschange', func); + return () => { + this.emitter.removeListener('rowschange', func); + }; + } + + onChange(func) { + this.emitter.on('change', func); + return () => { + this.emitter.removeListener('change', func); + }; + } +} + +export default new I18nUtil(); diff --git a/packages/editor-preset-vision/src/index.ts b/packages/editor-preset-vision/src/index.ts index 42643cb31..556fa050d 100644 --- a/packages/editor-preset-vision/src/index.ts +++ b/packages/editor-preset-vision/src/index.ts @@ -3,7 +3,7 @@ import Popup from '@ali/ve-popups'; import Icons from '@ali/ve-icons'; import logger from '@ali/vu-logger'; import { render } from 'react-dom'; -import I18nUtil from '@ali/ve-i18n-util'; +import I18nUtil from './i18n-util'; import { hotkey as Hotkey } from '@ali/lowcode-editor-core'; import { createElement } from 'react'; import { VE_EVENTS as EVENTS, VE_HOOKS as HOOKS, VERSION as Version } from './base/const';