From c5ef97a30b89f055823e0ef439d1cc6cb9922890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 7 Dec 2021 14:04:36 +0100 Subject: [PATCH] :zap: Initial plugin implementation --- packages/editor-ui/src/components/About.vue | 8 +- packages/editor-ui/src/main.ts | 4 +- packages/editor-ui/src/plugins/i18n/index.ts | 252 +++++++++++++++++++ 3 files changed, 259 insertions(+), 5 deletions(-) create mode 100644 packages/editor-ui/src/plugins/i18n/index.ts diff --git a/packages/editor-ui/src/components/About.vue b/packages/editor-ui/src/components/About.vue index 79c60f8213..c14d92ed3b 100644 --- a/packages/editor-ui/src/components/About.vue +++ b/packages/editor-ui/src/components/About.vue @@ -4,7 +4,7 @@
- {{ $baseText('about.n8nVersion') }} + {{ $i18n2.baseText('about.n8nVersion') }} {{ versionCli }} @@ -12,7 +12,7 @@ - {{ $baseText('about.sourceCode') }} + {{ $i18n2.baseText('about.sourceCode') }} https://github.com/n8n-io/n8n @@ -20,11 +20,11 @@ - {{ $baseText('about.license') }} + {{ $i18n2.baseText('about.license') }} - {{ $baseText('about.apacheWithCommons20Clause') }} + {{ $i18n2.baseText('about.apacheWithCommons20Clause') }} diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index f125a943d5..dd1de05cb4 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -18,6 +18,7 @@ import router from './router'; import { runExternalHook } from './components/mixins/externalHooks'; import { TelemetryPlugin } from './plugins/telemetry'; +import { I18nPlugin } from './plugins/i18n'; import { i18n } from './i18n'; import { store } from './store'; @@ -28,11 +29,12 @@ router.afterEach((to, from) => { }); Vue.use(TelemetryPlugin); +Vue.use(I18nPlugin); new Vue({ router, store, - i18n, + i18n, // legacy i18n render: h => h(App), }).$mount('#app'); diff --git a/packages/editor-ui/src/plugins/i18n/index.ts b/packages/editor-ui/src/plugins/i18n/index.ts new file mode 100644 index 0000000000..0df5ece1da --- /dev/null +++ b/packages/editor-ui/src/plugins/i18n/index.ts @@ -0,0 +1,252 @@ +import _Vue from "vue"; +import { IRootState } from '@/Interface'; +import VueI18n from 'vue-i18n'; +import { i18n as i18nLib } from '../../i18n'; + +const REUSABLE_DYNAMIC_TEXT_KEY = 'reusableDynamicText'; +const CREDENTIALS_MODAL_KEY = 'credentialsModal'; +const NODE_VIEW_KEY = 'nodeView'; + +// this.$i18n.baseText('key1.key1', { interpolate: { keyName: '...' } }); +// this.$i18n.nodeText.placeholder('...'); +// this.$i18n.credText.placeholder('...'); +// this.$i18n.headerText.placeholder({ key: '...', fallback: '...' }); + +export function I18nPlugin(vue: typeof _Vue, store: IRootState): void { + const i18n = new I18nClass(store); + + if (!vue.prototype.hasOwnProperty('$i18n2')) { // TODO: Rename to `$i18n` after removing legacy i18n property + Object.defineProperty(vue, '$i18n2', { // TODO: Rename to `$i18n` after removing legacy i18n property + get() { return i18n; }, + }); + + Object.defineProperty(vue.prototype, '$i18n2', { // TODO: Rename to `$i18n` after removing legacy i18n property + get() { return i18n; }, + }); + } +} + +export class I18nClass { + $store: IRootState; + + constructor(store: IRootState) { + this.$store = store; + } + + private get i18n(): VueI18n { + return i18nLib; + } + + /** + * Render a string of dynamic text, i.e. a string with a constructed path to the localized value in the node text object - in the credentials modal (`$credText`), in the node view (`$nodeText`), or in the headers (`$headerText`) in the nodes panel and node view. _Private method_, to be called only from within this mixin. + * + * Unlike in `$baseText`, the fallback has to be set manually for dynamic text. + */ + private __render( + { key, fallback }: { key: string; fallback: string; }, + ) { + return this.i18n.te(key) ? this.i18n.t(key).toString() : fallback; + } + + $shortNodeType(longNodeType: string) { + return longNodeType.replace('n8n-nodes-base.', ''); + } + + /** + * Render a string of base text, i.e. a string with a fixed path to the localized value in the base text object. Optionally allows for [interpolation](https://kazupon.github.io/vue-i18n/guide/formatting.html#named-formatting) when the localized value contains a string between curly braces. + */ + baseText( + key: string, options?: { interpolate: { [key: string]: string } }, + ): string { + return this.i18n.t(key, options && options.interpolate).toString(); + } + + /** + * Render a string of dynamic header text, used in the nodes panel and in the node view. + */ + headerText(arg: { key: string; fallback: string; }) { + return this.__render(arg); + } + + credText () { + const keys = this.$store.credentialTextRenderKeys; + const nodeType = keys ? keys.nodeType : ''; + const credentialType = keys ? keys.credentialType : ''; + const credentialPrefix = `${nodeType}.${CREDENTIALS_MODAL_KEY}.${credentialType}`; + const context = this; + + return { + + /** + * Display name for a top-level parameter in the credentials modal. + */ + topParameterDisplayName( + { name: parameterName, displayName }: { name: string; displayName: string; }, + ) { + if (['clientId', 'clientSecret'].includes(parameterName)) { + return context.__render({ + key: `${REUSABLE_DYNAMIC_TEXT_KEY}.oauth2.${parameterName}`, + fallback: displayName, + }); + } + + return context.__render({ + key: `${credentialPrefix}.${parameterName}.displayName`, + fallback: displayName, + }); + }, + + /** + * Description for a top-level parameter in the credentials modal. + */ + topParameterDescription( + { name: parameterName, description }: { name: string; description: string; }, + ) { + return context.__render({ + key: `${credentialPrefix}.${parameterName}.description`, + fallback: description, + }); + }, + + /** + * Display name for an option inside an `options` or `multiOptions` parameter in the credentials modal. + */ + optionsOptionDisplayName( + { name: parameterName }: { name: string; }, + { value: optionName, name: displayName }: { value: string; name: string; }, + ) { + return context.__render({ + key: `${credentialPrefix}.${parameterName}.options.${optionName}.displayName`, + fallback: displayName, + }); + }, + + /** + * Description for an option inside an `options` or `multiOptions` parameter in the credentials modal. + */ + optionsOptionDescription( + { name: parameterName }: { name: string; }, + { value: optionName, description }: { value: string; description: string; }, + ) { + return context.__render({ + key: `${credentialPrefix}.${parameterName}.options.${optionName}.description`, + fallback: description, + }); + }, + + /** + * Placeholder for a `string` or `collection` or `fixedCollection` parameter in the credentials modal. + * - For a `string` parameter, the placeholder is unselectable greyed-out sample text. + * - For a `collection` or `fixedCollection` parameter, the placeholder is the button text. + */ + placeholder( + { name: parameterName, displayName }: { name: string; displayName: string; }, + ) { + return context.__render({ + key: `${credentialPrefix}.${parameterName}.placeholder`, + fallback: displayName, + }); + }, + }; + } + + nodeText () { + // @ts-ignore TODO + const type = this.$store.activeNode.type; + const nodePrefix = `${type}.${NODE_VIEW_KEY}`; + const context = this; + + return { + /** + * Display name for a top-level parameter in the node view. + */ + topParameterDisplayName( + { name: parameterName, displayName }: { name: string; displayName: string; }, + ) { + return context.__render({ + key: `${nodePrefix}.${parameterName}.displayName`, + fallback: displayName, + }); + }, + + /** + * Description for a top-level parameter in the node view in the node view. + */ + topParameterDescription( + { name: parameterName, description }: { name: string; description: string; }, + ) { + return context.__render({ + key: `${nodePrefix}.${parameterName}.description`, + fallback: description, + }); + }, + + /** + * Display name for an option inside a `collection` or `fixedCollection` parameter in the node view. + */ + collectionOptionDisplayName( + { name: parameterName }: { name: string; }, + { name: optionName, displayName }: { name: string; displayName: string; }, + ) { + return context.__render({ + key: `${nodePrefix}.${parameterName}.options.${optionName}.displayName`, + fallback: displayName, + }); + }, + + /** + * Display name for an option inside an `options` or `multiOptions` parameter in the node view. + */ + optionsOptionDisplayName( + { name: parameterName }: { name: string; }, + { value: optionName, name: displayName }: { value: string; name: string; }, + ) { + return context.__render({ + key: `${nodePrefix}.${parameterName}.options.${optionName}.displayName`, + fallback: displayName, + }); + }, + + /** + * Description for an option inside an `options` or `multiOptions` parameter in the node view. + */ + optionsOptionDescription( + { name: parameterName }: { name: string; }, + { value: optionName, description }: { value: string; description: string; }, + ) { + return context.__render({ + key: `${nodePrefix}.${parameterName}.options.${optionName}.description`, + fallback: description, + }); + }, + + /** + * Text for a button to add another option inside a `collection` or `fixedCollection` parameter having`multipleValues: true` in the node view. + */ + multipleValueButtonText( + { name: parameterName, typeOptions: { multipleValueButtonText } }: + { name: string; typeOptions: { multipleValueButtonText: string; } }, + ) { + return context.__render({ + key: `${nodePrefix}.${parameterName}.multipleValueButtonText`, + fallback: multipleValueButtonText, + }); + }, + + /** + * Placeholder for a `string` or `collection` or `fixedCollection` parameter in the node view. + * - For a `string` parameter, the placeholder is unselectable greyed-out sample text. + * - For a `collection` or `fixedCollection` parameter, the placeholder is the button text. + */ + placeholder( + { name: parameterName, placeholder }: { name: string; placeholder: string; }, + ) { + return context.__render({ + key: `${nodePrefix}.${parameterName}.placeholder`, + fallback: placeholder, + }); + }, + }; + } + +}