1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-11-14 13:00:33 +03:00

Render node strings

This commit is contained in:
Iván Ovejero 2021-11-15 11:19:43 +01:00
parent 2d8e158012
commit 7fc0395e95
26 changed files with 2442 additions and 2036 deletions

View File

@ -5,6 +5,7 @@
import {
INodeType,
INodeTypeData,
INodeTypeDescription,
INodeTypes,
INodeVersionedType,
NodeHelpers,
@ -18,7 +19,7 @@ class NodeTypesClass implements INodeTypes {
// polling nodes the polling times
// eslint-disable-next-line no-restricted-syntax
for (const nodeTypeData of Object.values(nodeTypes)) {
const nodeType = NodeHelpers.getVersionedTypeNode(nodeTypeData.type);
const nodeType = NodeHelpers.getVersionedNodeType(nodeTypeData.type);
const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeType);
if (applyParameters.length) {
@ -39,11 +40,26 @@ class NodeTypesClass implements INodeTypes {
return this.nodeTypes[nodeType].type;
}
getWithPath(
nodeTypeName: string,
version: number,
): { description: INodeTypeDescription } & { sourcePath: string } {
const nodeType = this.nodeTypes[nodeTypeName];
if (!nodeType) {
throw new Error(`Unknown node type: ${nodeTypeName}`);
}
const { description } = NodeHelpers.getVersionedNodeType(nodeType.type, version);
return { description: { ...description }, sourcePath: nodeType.sourcePath };
}
getByNameAndVersion(nodeType: string, version?: number): INodeType {
if (this.nodeTypes[nodeType] === undefined) {
throw new Error(`The node-type "${nodeType}" is not known!`);
}
return NodeHelpers.getVersionedTypeNode(this.nodeTypes[nodeType].type, version);
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
}
}

View File

@ -24,8 +24,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable import/no-dynamic-require */
import * as express from 'express';
import { readFileSync } from 'fs';
import { readFileSync, existsSync } from 'fs';
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
import { FindManyOptions, getConnectionManager, In, IsNull, LessThanOrEqual, Not } from 'typeorm';
import * as bodyParser from 'body-parser';
@ -144,6 +145,7 @@ import { InternalHooksManager } from './InternalHooksManager';
import { TagEntity } from './databases/entities/TagEntity';
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
import { NameRequest } from './WorkflowHelpers';
import { getTranslationPath } from './TranslationHelpers';
require('body-parser-xml')(bodyParser);
@ -1152,13 +1154,13 @@ class App {
if (onlyLatest) {
allNodes.forEach((nodeData) => {
const nodeType = NodeHelpers.getVersionedTypeNode(nodeData);
const nodeType = NodeHelpers.getVersionedNodeType(nodeData);
const nodeInfo: INodeTypeDescription = getNodeDescription(nodeType);
returnData.push(nodeInfo);
});
} else {
allNodes.forEach((nodeData) => {
const allNodeTypes = NodeHelpers.getVersionedTypeNodeAll(nodeData);
const allNodeTypes = NodeHelpers.getVersionedNodeTypeAll(nodeData);
allNodeTypes.forEach((element) => {
const nodeInfo: INodeTypeDescription = getNodeDescription(element);
returnData.push(nodeInfo);
@ -1179,15 +1181,28 @@ class App {
const nodeInfos = _.get(req, 'body.nodeInfos', []) as INodeTypeNameVersion[];
const nodeTypes = NodeTypes();
const returnData: INodeTypeDescription[] = [];
nodeInfos.forEach((nodeInfo) => {
const nodeType = nodeTypes.getByNameAndVersion(nodeInfo.name, nodeInfo.version);
if (nodeType?.description) {
returnData.push(nodeType.description);
}
});
const language = config.get('defaultLocale') ?? req.headers['accept-language'] ?? 'en';
return returnData;
if (language === 'en') {
return nodeInfos.reduce<INodeTypeDescription[]>((acc, { name, version }) => {
const { description } = nodeTypes.getByNameAndVersion(name, version);
if (description) acc.push(description);
return acc;
}, []);
}
// add node translations where available
return nodeInfos.reduce<INodeTypeDescription[]>((acc, { name, version }) => {
const { description, sourcePath } = nodeTypes.getWithPath(name, version);
const mainTranslationPath = getTranslationPath(sourcePath, language);
if (description && existsSync(mainTranslationPath)) {
description.translation = require(mainTranslationPath);
}
if (description) acc.push(description);
return acc;
}, []);
},
),
);

View File

@ -0,0 +1,8 @@
import { join, dirname } from 'path';
/**
* Retrieve the path to the translation file for a node.
*/
export function getTranslationPath(nodeSourcePath: string, language: string): string {
return join(dirname(nodeSourcePath), 'translations', `${language}.js`);
}

View File

@ -726,7 +726,7 @@ class NodeTypesClass implements INodeTypes {
async init(nodeTypes: INodeTypeData): Promise<void> {}
getAll(): INodeType[] {
return Object.values(this.nodeTypes).map((data) => NodeHelpers.getVersionedTypeNode(data.type));
return Object.values(this.nodeTypes).map((data) => NodeHelpers.getVersionedNodeType(data.type));
}
getByName(nodeType: string): INodeType {
@ -734,7 +734,7 @@ class NodeTypesClass implements INodeTypes {
}
getByNameAndVersion(nodeType: string, version?: number): INodeType {
return NodeHelpers.getVersionedTypeNode(this.nodeTypes[nodeType].type, version);
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
}
}

View File

@ -584,6 +584,7 @@ export interface IRootState {
activeActions: string[];
activeNode: string | null;
baseUrl: string;
credentialTextRenderKeys: { nodeType: string; credentialType: string; } | null;
defaultLocale: string;
endpointWebhook: string;
endpointWebhookTest: string;

View File

@ -1,8 +1,8 @@
<template>
<div v-if="dialogVisible">
<el-dialog :visible="dialogVisible" append-to-body :close-on-click-modal="false" width="80%" :title="`Edit ${parameter.displayName}`" :before-close="closeDialog">
<el-dialog :visible="dialogVisible" append-to-body :close-on-click-modal="false" width="80%" :title="`${$baseText('codeEdit.edit')} ${$nodeText.topParameterDisplayName(parameter)}`" :before-close="closeDialog">
<div class="ignore-key-press">
<n8n-input-label :label="parameter.displayName">
<n8n-input-label :label="$nodeText.topParameterDisplayName(parameter)">
<div :class="$style.editor" @keydown.stop>
<prism-editor :lineNumbers="true" :code="value" :readonly="isReadOnly" @change="valueChanged" language="js"></prism-editor>
</div>

View File

@ -19,7 +19,7 @@
<n8n-option
v-for="item in parameterOptions"
:key="item.name"
:label="item.displayName"
:label="$nodeText.collectionOptionDisplayName(parameter, item)"
:value="item.name">
</n8n-option>
</n8n-select>
@ -67,7 +67,8 @@ export default mixins(
},
computed: {
getPlaceholderText (): string {
return this.parameter.placeholder ? this.parameter.placeholder : this.$baseText('collectionParameter.choose');
const placeholder = this.$nodeText.placeholder(this.parameter);
return placeholder ? placeholder : this.$baseText('collectionParameter.choose');
},
getProperties (): INodeProperties[] {
const returnProperties = [];

View File

@ -72,7 +72,7 @@
</template>
<script lang="ts">
import { ICredentialType } from 'n8n-workflow';
import { ICredentialType, INodeTypeDescription } from 'n8n-workflow';
import { getAppNameFromCredType } from '../helpers';
import Vue from 'vue';
@ -81,10 +81,11 @@ import CopyInput from '../CopyInput.vue';
import CredentialInputs from './CredentialInputs.vue';
import OauthButton from './OauthButton.vue';
import { renderText } from '../mixins/renderText';
import { restApi } from '@/components/mixins/restApi';
import { addNodeTranslation } from '@/i18n';
import mixins from 'vue-typed-mixins';
export default mixins(renderText).extend({
export default mixins(renderText, restApi).extend({
name: 'CredentialConfig',
components: {
Banner,
@ -94,6 +95,7 @@ export default mixins(renderText).extend({
},
props: {
credentialType: {
type: Object,
},
credentialProperties: {
type: Array,
@ -126,6 +128,10 @@ export default mixins(renderText).extend({
type: Boolean,
},
},
async beforeMount() {
await this.findCredentialTextRenderKeys();
await this.addNodeTranslationForCredential();
},
computed: {
appName(): string {
if (!this.credentialType) {
@ -136,7 +142,7 @@ export default mixins(renderText).extend({
(this.credentialType as ICredentialType).displayName,
);
return appName || "the service you're connecting to";
return appName || this.$baseText('credentialEdit.credentialConfig.theServiceYouReConnectingTo');
},
credentialTypeName(): string {
return (this.credentialType as ICredentialType).name;
@ -170,6 +176,62 @@ export default mixins(renderText).extend({
},
},
methods: {
/**
* Find the keys needed by the mixin to render credential text, and place them in the Vuex store.
*/
async findCredentialTextRenderKeys() {
const nodeTypes = await this.restApi().getNodeTypes();
// credential type name node type name
const map = nodeTypes.reduce<Record<string, string>>((acc, cur) => {
if (!cur.credentials) return acc;
cur.credentials.forEach(cred => {
if (acc[cred.name]) return;
acc[cred.name] = cur.name;
});
return acc;
}, {});
const renderKeys = {
nodeType: map[this.credentialType.name],
credentialType: this.credentialType.name,
};
this.$store.commit('setCredentialTextRenderKeys', renderKeys);
},
/**
* Add to the translation object the node translation
* for the credential being viewed.
*/
async addNodeTranslationForCredential() {
// TODO i18n: Check if node translation has already been added (via NodeView)
const { nodeType }: { nodeType: string } = this.$store.getters.credentialTextRenderKeys;
const version = await this.getCurrentNodeVersion(nodeType);
const nodeToBeFetched = [{ name: nodeType, version }];
const nodesInfo = await this.restApi().getNodesInformation(nodeToBeFetched);
const nodeInfo = nodesInfo.pop();
if (nodeInfo && nodeInfo.translation) {
addNodeTranslation(nodeInfo.translation, this.$store.getters.defaultLocale);
}
},
/**
* Get the current version for a node type.
*/
async getCurrentNodeVersion(targetNodeType: string) {
const { allNodeTypes }: { allNodeTypes: INodeTypeDescription[] } = this.$store.getters;
const found = allNodeTypes.find(nodeType => nodeType.name === targetNodeType);
return found ? found.version : 1;
},
onDataChange (event: { name: string; value: string | number | boolean | Date | null }): void {
this.$emit('change', event);
},

View File

@ -43,7 +43,7 @@
<n8n-option
v-for="item in parameterOptions"
:key="item.name"
:label="item.displayName"
:label="$nodeText.collectionOptionDisplayName(parameter, item)"
:value="item.name">
</n8n-option>
</n8n-select>
@ -85,7 +85,8 @@ export default mixins(genericHelpers)
},
computed: {
getPlaceholderText (): string {
return this.parameter.placeholder ? this.parameter.placeholder : this.$baseText('fixedCollectionParameter.choose');
const placeholder = this.$nodeText.placeholder(this.parameter);
return placeholder ? placeholder : this.$baseText('fixedCollectionParameter.choose');
},
getProperties (): INodePropertyCollection[] {
const returnProperties = [];

View File

@ -1,8 +1,8 @@
<template>
<div @keydown.stop class="duplicate-parameter">
<n8n-input-label
:label="parameter.displayName"
:tooltipText="parameter.description"
:label="$nodeText.topParameterDisplayName(parameter)"
:tooltipText="$nodeText.topParameterDescription(parameter)"
:underline="true"
:labelHoverableOnly="true"
size="small"
@ -64,7 +64,14 @@ export default mixins(genericHelpers)
],
computed: {
addButtonText (): string {
return (this.parameter.typeOptions && this.parameter.typeOptions.multipleValueButtonText) ? this.parameter.typeOptions.multipleValueButtonText : 'Add item';
if (
!this.parameter.typeOptions &&
!this.parameter.typeOptions.multipleValueButtonText
) {
return this.$baseText('multipleParameter.addItem');
}
return this.$nodeText.multipleValueButtonText(this.parameter);
},
hideDelete (): boolean {
return this.parameter.options.length === 1;

View File

@ -35,7 +35,7 @@
@focus="setFocus"
@blur="onBlur"
:title="displayTitle"
:placeholder="isValueExpression?'':parameter.placeholder"
:placeholder="isValueExpression ? '' : getPlaceholder()"
>
<div slot="suffix" class="expand-input-icon-container">
<font-awesome-icon v-if="!isValueExpression && !isReadOnly" icon="external-link-alt" class="edit-window-button clickable" :title="$baseText('parameterInput.openEditWindow')" @click="displayEditDialog()" />
@ -78,7 +78,7 @@
:value="displayValue"
:title="displayTitle"
:disabled="isReadOnly"
:placeholder="parameter.placeholder?parameter.placeholder:$baseText('parameterInput.selectDateAndTime')"
:placeholder="parameter.placeholder ? getPlaceholder() : $baseText('parameterInput.selectDateAndTime')"
:picker-options="dateTimePickerOptions"
@change="valueChanged"
@focus="setFocus"
@ -124,11 +124,13 @@
v-for="option in parameterOptions"
:value="option.value"
:key="option.value"
:label="option.name"
:label="getOptionsOptionDisplayName(option)"
>
<div class="list-option">
<div class="option-headline">{{ option.name }}</div>
<div v-if="option.description" class="option-description" v-html="option.description"></div>
<div class="option-headline">
{{ getOptionsOptionDisplayName(option) }}
</div>
<div v-if="option.description" class="option-description" v-html="getOptionsOptionDescription(option)"></div>
</div>
</n8n-option>
</n8n-select>
@ -148,10 +150,10 @@
@blur="onBlur"
:title="displayTitle"
>
<n8n-option v-for="option in parameterOptions" :value="option.value" :key="option.value" :label="option.name" >
<n8n-option v-for="option in parameterOptions" :value="option.value" :key="option.value" :label="getOptionsOptionDisplayName(option)">
<div class="list-option">
<div class="option-headline">{{ option.name }}</div>
<div v-if="option.description" class="option-description" v-html="option.description"></div>
<div class="option-headline">{{ getOptionsOptionDisplayName(option) }}</div>
<div v-if="option.description" class="option-description" v-html="getOptionsOptionDescription(option)"></div>
</div>
</n8n-option>
</n8n-select>
@ -240,6 +242,7 @@ export default mixins(
'value',
'hideIssues', // boolean
'errorHighlight',
'isForCredential', // boolean
],
data () {
return {
@ -255,14 +258,14 @@ export default mixins(
dateTimePickerOptions: {
shortcuts: [
{
text: 'Today',
text: 'Today', // TODO
// tslint:disable-next-line:no-any
onClick (picker: any) {
picker.$emit('pick', new Date());
},
},
{
text: 'Yesterday',
text: 'Yesterday', // TODO
// tslint:disable-next-line:no-any
onClick (picker: any) {
const date = new Date();
@ -271,7 +274,7 @@ export default mixins(
},
},
{
text: 'A week ago',
text: 'A week ago', // TODO
// tslint:disable-next-line:no-any
onClick (picker: any) {
const date = new Date();
@ -325,20 +328,26 @@ export default mixins(
return this.$store.getters.activeNode;
},
displayTitle (): string {
let title = `Parameter: "${this.shortPath}"`;
if (this.getIssues.length) {
title += ` has issues`;
if (this.isValueExpression === true) {
title += ` and expression`;
}
title += `!`;
} else {
if (this.isValueExpression === true) {
title += ` has expression`;
}
const interpolation = { interpolate: { shortPath: this.shortPath } };
if (this.getIssues.length && this.isValueExpression) {
return this.$baseText(
'parameterInput.parameterHasIssuesAndExpression',
interpolation,
);
} else if (this.getIssues.length && !this.isValueExpression) {
return this.$baseText(
'parameterInput.parameterHasIssues',
interpolation,
);
} else if (!this.getIssues.length && this.isValueExpression) {
return this.$baseText(
'parameterInput.parameterHasExpression',
interpolation,
);
}
return title;
return this.$baseText('parameterInput.parameter', interpolation);
},
displayValue (): string | number | boolean | null {
if (this.remoteParameterOptionsLoading === true) {
@ -346,7 +355,7 @@ export default mixins(
// to user that the data is loading. If not it would
// display the user the key instead of the value it
// represents
return 'Loading options...';
return this.$baseText('parameterInput.loadingOptions');
}
let returnValue;
@ -415,7 +424,7 @@ export default mixins(
try {
computedValue = this.resolveExpression(this.value) as NodeParameterValue;
} catch (error) {
computedValue = `[ERROR: ${error.message}]`;
computedValue = `[${this.$baseText('parameterInput.error')}}: ${error.message}]`;
}
// Try to convert it into the corret type
@ -559,6 +568,22 @@ export default mixins(
},
},
methods: {
getPlaceholder(): string {
return this.isForCredential
? this.$credText.placeholder(this.parameter)
: this.$nodeText.placeholder(this.parameter);
},
getOptionsOptionDisplayName(option: { value: string; name: string }): string {
return this.isForCredential
? this.$credText.optionsOptionDisplayName(this.parameter, option)
: this.$nodeText.optionsOptionDisplayName(this.parameter, option);
},
getOptionsOptionDescription(option: { value: string; description: string }): string {
return this.isForCredential
? this.$credText.optionsOptionDescription(this.parameter, option)
: this.$nodeText.optionsOptionDescription(this.parameter, option);
},
async loadRemoteParameterOptions () {
if (this.node === null || this.remoteMethod === undefined || this.remoteParameterOptionsLoading) {
return;

View File

@ -1,7 +1,7 @@
<template>
<n8n-input-label
:label="parameter.displayName"
:tooltipText="parameter.description"
:label="$credText.topParameterDisplayName(parameter)"
:tooltipText="$credText.topParameterDescription(parameter)"
:required="parameter.required"
:showTooltip="focused"
>
@ -13,6 +13,7 @@
:displayOptions="true"
:documentationUrl="documentationUrl"
:errorHighlight="showRequiredErrors"
:isForCredential="true"
@focus="onFocus"
@blur="onBlur"
@textInput="valueChanged"
@ -20,7 +21,7 @@
inputSize="large"
/>
<div class="errors" v-if="showRequiredErrors">
{{ $baseText('parameterInputExpanded.thisFieldIsRequired') }} <a v-if="documentationUrl" :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">Open docs</a>
{{ $baseText('parameterInputExpanded.thisFieldIsRequired') }} <a v-if="documentationUrl" :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">{{ $baseText('parameterInputExpanded.openDocs') }}</a>
</div>
</n8n-input-label>
</template>
@ -29,8 +30,10 @@
import { IUpdateInformation } from '@/Interface';
import ParameterInput from './ParameterInput.vue';
import Vue from 'vue';
import mixins from 'vue-typed-mixins';
import { renderText } from './mixins/renderText';
export default Vue.extend({
export default mixins(renderText).extend({
name: 'ParameterInputExpanded',
components: {
ParameterInput,

View File

@ -1,7 +1,7 @@
<template>
<n8n-input-label
:label="parameter.displayName"
:tooltipText="parameter.description"
:label="$nodeText.topParameterDisplayName(parameter)"
:tooltipText="$nodeText.topParameterDescription(parameter)"
:showTooltip="focused"
:bold="false"
size="small"
@ -27,8 +27,10 @@ import {
} from '@/Interface';
import ParameterInput from '@/components/ParameterInput.vue';
import { renderText } from '@/components/mixins/renderText';
import mixins from 'vue-typed-mixins';
export default Vue
export default mixins(renderText)
.extend({
name: 'ParameterInputFull',
components: {

View File

@ -16,7 +16,7 @@
<div v-else-if="parameter.type === 'notice'" class="parameter-item parameter-notice">
<n8n-text size="small">
<span v-html="parameter.displayName"></span>
<span v-html="$nodeText.topParameterDisplayName(parameter)"></span>
</n8n-text>
</div>
@ -33,8 +33,8 @@
/>
</div>
<n8n-input-label
:label="parameter.displayName"
:tooltipText="parameter.description"
:label="$nodeText.topParameterDisplayName(parameter)"
:tooltipText="$nodeText.topParameterDescription(parameter)"
size="small"
:underline="true"
:labelHoverableOnly="true"

View File

@ -1,11 +1,11 @@
<template>
<div v-if="dialogVisible">
<el-dialog :visible="dialogVisible" append-to-body width="80%" :title="`Edit ${parameter.displayName}`" :before-close="closeDialog">
<el-dialog :visible="dialogVisible" append-to-body width="80%" :title="`${$baseText('textEdit.edit')} ${$nodeText.topParameterDisplayName(parameter)}`" :before-close="closeDialog">
<div class="ignore-key-press">
<n8n-input-label :label="parameter.displayName">
<n8n-input-label :label="$nodeText.topParameterDisplayName(parameter)">
<div @keydown.stop @keydown.esc="closeDialog()">
<n8n-input v-model="tempValue" type="textarea" ref="inputField" :value="value" :placeholder="parameter.placeholder" @change="valueChanged" @keydown.stop="noOp" :rows="15" />
<n8n-input v-model="tempValue" type="textarea" ref="inputField" :value="value" :placeholder="$nodeText.placeholder(parameter)" @change="valueChanged" @keydown.stop="noOp" :rows="15" />
</div>
</n8n-input-label>
</div>
@ -16,9 +16,10 @@
<script lang="ts">
import Vue from 'vue';
import { renderText } from '@/components/mixins/renderText';
import mixins from 'vue-typed-mixins';
export default Vue.extend({
export default mixins(renderText).extend({
name: 'TextEdit',
props: [
'dialogVisible',

View File

@ -1,191 +1,211 @@
/* tslint:disable: variable-name */
// import { TranslationPath } from '@/Interface';
import Vue from 'vue';
export const renderText = Vue.extend({
computed: {
/**
* Node type for the active node in `NodeView.vue`.
*/
activeNodeType (): string {
return this.$store.getters.activeNode.type;
},
},
const REUSABLE_TEXT_KEY = 'reusableText';
const CREDENTIALS_MODAL_KEY = 'credentialsModal';
const NODE_VIEW_KEY = 'nodeView';
export const renderText = Vue.extend({
methods: {
/**
* Render a string of base text, i.e. a string with a **fixed path** to the value in the locale object. Allows for [interpolation](https://kazupon.github.io/vue-i18n/guide/formatting.html#named-formatting) when the localized value contains a string between curly braces.
* ```js
* $baseText('fixed.path.to.localized.value');
* $baseText('fixed.path.to.localized.value', { interpolate: { var: arg } });
* ```
* 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 } },
key: string, options?: { interpolate: { [key: string]: string } },
): string {
return this.$t(key, options && options.interpolate).toString();
},
/**
* Translate a node- or credentials-specific string.
* Called in-mixin by node- or credentials-specific methods,
* which are called directly in Vue templates.
* Render a string of dynamic text, i.e. a string with a constructed path to the localized value in the node text object, either in the credentials modal (`$credText`) or in the node view (`$nodeView`). **Private method**, to be called only from the two namespaces within this mixin.
*/
translateSpecific(
__render(
{ key, fallback }: { key: string, fallback: string },
): string {
) {
return this.$te(key) ? this.$t(key).toString() : fallback;
},
},
// -----------------------------------------
// node-specific methods
// -----------------------------------------
computed: {
$credText () {
const { credentialTextRenderKeys: keys } = this.$store.getters;
const nodeType = keys ? keys.nodeType : '';
const credentialType = keys ? keys.credentialType : '';
const credentialPrefix = `${nodeType}.${CREDENTIALS_MODAL_KEY}.${credentialType}`;
const context = this;
/**
* Translate a top-level node parameter name, i.e. leftmost parameter in `NodeView.vue`.
*/
$translateNodeParameterName(
{ name: parameterName, displayName }: { name: string; displayName: string; },
) {
return this.translateSpecific({
key: `${this.activeNodeType}.parameters.${parameterName}.displayName`,
fallback: displayName,
});
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_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,
});
},
};
},
/**
* Translate a top-level parameter description for a node or for credentials.
*/
$translateDescription(
{ name: parameterName, description }: { name: string; description: string; },
) {
return this.translateSpecific({
key: `${this.activeNodeType}.parameters.${parameterName}.description`,
fallback: description,
});
},
$nodeText () {
const nodePrefix = `${this.$store.getters.activeNode.type}.${NODE_VIEW_KEY}`;
const context = this;
/**
* Translate the name for an option in a `collection` or `fixed collection` parameter,
* e.g. an option name in an "Additional Options" fixed collection.
*/
$translateCollectionOptionName(
{ name: parameterName }: { name: string; },
{ name: optionName, displayName }: { name: string; displayName: string; },
) {
return this.translateSpecific({
key: `${this.activeNodeType}.parameters.${parameterName}.options.${optionName}.displayName`,
fallback: displayName,
});
},
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,
});
},
/**
* Translate the label for a button that adds another field-input pair to a collection.
*/
$translateMultipleValueButtonText(
{ name: parameterName, typeOptions: { multipleValueButtonText } }:
{ name: string, typeOptions: { multipleValueButtonText: string } },
) {
return this.translateSpecific({
key: `${this.activeNodeType}.parameters.${parameterName}.multipleValueButtonText`,
fallback: multipleValueButtonText,
});
},
/**
* 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,
});
},
// -----------------------------------------
// creds-specific methods
// -----------------------------------------
/**
* 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,
});
},
/**
* Translate a credentials property name, i.e. leftmost parameter in `CredentialsEdit.vue`.
*/
$translateCredentialsPropertyName(
{ name: parameterName, displayName }: { name: string; displayName: string; },
{ nodeType, credentialsName }: { nodeType: string, credentialsName: string; },
) {
if (['clientId', 'clientSecret'].includes(parameterName)) {
return this.$t(`oauth2.${parameterName}`);
}
/**
* 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,
});
},
return this.translateSpecific({
key: `${nodeType}.credentials.${credentialsName}.${parameterName}.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,
});
},
/**
* Translate a credentials property description, i.e. label tooltip in `CredentialsEdit.vue`.
*/
$translateCredentialsPropertyDescription(
{ name: parameterName, description }: { name: string; description: string; },
{ nodeType, credentialsName }: { nodeType: string, credentialsName: string; },
) {
return this.translateSpecific({
key: `${nodeType}.credentials.${credentialsName}.${parameterName}.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,
});
},
// -----------------------------------------
// node- and creds-specific methods
// -----------------------------------------
/**
* Translate the placeholder inside the input field for a string-type parameter.
*/
$translatePlaceholder(
{ name: parameterName, placeholder }: { name: string; placeholder: string; },
isCredential = false,
{ nodeType, credentialsName } = { nodeType: '', credentialsName: '' },
) {
const key = isCredential
? `${nodeType}.credentials.${credentialsName}.placeholder`
: `${this.activeNodeType}.parameters.${parameterName}.placeholder`;
return this.translateSpecific({
key,
fallback: placeholder,
});
},
/**
* Translate the name for an option in an `options` parameter,
* e.g. an option name in a "Resource" or "Operation" dropdown menu.
*/
$translateOptionsOptionName(
{ name: parameterName }: { name: string },
{ value: optionName, name: displayName }: { value: string; name: string; },
isCredential = false,
{ nodeType, credentialsName } = { nodeType: '', credentialsName: '' },
) {
const key = isCredential
? `${nodeType}.credentials.${credentialsName}.options.${optionName}.displayName`
: `${this.activeNodeType}.parameters.${parameterName}.options.${optionName}.displayName`;
return this.translateSpecific({
key,
fallback: displayName,
});
},
/**
* Translate the description for an option in an `options` parameter,
* e.g. an option name in a "Resource" or "Operation" dropdown menu.
*/
$translateOptionsOptionDescription(
{ name: parameterName }: { name: string },
{ value: optionName, description }: { value: string; description: string; },
isCredential = false,
{ nodeType, credentialsName } = { nodeType: '', credentialsName: '' },
) {
const key = isCredential
? `${nodeType}.credentials.${credentialsName}.options.${optionName}.description`
: `${this.activeNodeType}.parameters.${parameterName}.options.${optionName}.description`;
return this.translateSpecific({
key,
fallback: description,
});
/**
* 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,
});
},
};
},
},
});

View File

@ -2,24 +2,27 @@ import Vue from 'vue';
import VueI18n from 'vue-i18n';
import englishBaseText from './locales/en';
import axios from 'axios';
import path from 'path';
Vue.use(VueI18n);
// TODO i18n: Remove next line
console.log('About to initialize i18n'); // eslint-disable-line no-console
export const i18n = new VueI18n({
locale: 'en',
fallbackLocale: 'en',
messages: englishBaseText,
messages: { en: englishBaseText },
silentTranslationWarn: true,
});
const loadedLanguages = ['en'];
function setLanguage(language: string): string {
function setLanguage(language: string) {
i18n.locale = language;
axios.defaults.headers.common['Accept-Language'] = language;
document!.querySelector('html')!.setAttribute('lang', language);
return language;
}
@ -36,21 +39,30 @@ export async function loadLanguage(language?: string) {
return Promise.resolve(setLanguage(language));
}
const { default: { [language]: messages }} = require(`./locales/${language}`);
i18n.setLocaleMessage(language, messages);
const baseText = require(`./locales/${language}`).default; // TODO i18n: `path.join()`
console.log(baseText);
i18n.setLocaleMessage(language, baseText);
loadedLanguages.push(language);
return setLanguage(language);
}
export function addNodeTranslations(translations: { [key: string]: string | object }) {
const lang = Object.keys(translations)[0];
const messages = translations[lang];
export function addNodeTranslation(
nodeTranslation: { [key: string]: object },
language: string,
) {
const newNodesBase = {
'n8n-nodes-base': Object.assign(
i18n.messages[lang]['n8n-nodes-base'],
messages,
i18n.messages[language]['n8n-nodes-base'] || {},
nodeTranslation,
),
};
i18n.setLocaleMessage(lang, Object.assign(i18n.messages[lang], newNodesBase));
// TODO i18n: Remove next line
console.log('newNodesBase', newNodesBase); // eslint-disable-line no-console
i18n.setLocaleMessage(
language,
Object.assign(i18n.messages[language], newNodesBase),
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -48,6 +48,7 @@ const state: IRootState = {
activeNode: null,
// @ts-ignore
baseUrl: process.env.VUE_APP_URL_BASE_API ? process.env.VUE_APP_URL_BASE_API : (window.BASE_PATH === '/%BASE_PATH%/' ? '/' : window.BASE_PATH),
credentialTextRenderKeys: null,
defaultLocale: 'en',
endpointWebhook: 'webhook',
endpointWebhookTest: 'webhook-test',
@ -559,6 +560,9 @@ export const store = new Vuex.Store({
setActiveNode (state, nodeName: string) {
state.activeNode = nodeName;
},
setCredentialTextRenderKeys (state, renderKeys: { nodeType: string; credentialType: string; }) {
state.credentialTextRenderKeys = renderKeys;
},
setLastSelectedNode (state, nodeName: string) {
state.lastSelectedNode = nodeName;
@ -653,6 +657,10 @@ export const store = new Vuex.Store({
return state.activeExecutions;
},
credentialTextRenderKeys: (state): object | null => {
return state.credentialTextRenderKeys;
},
getBaseUrl: (state): string => {
return state.baseUrl;
},

View File

@ -170,7 +170,7 @@ import {
IExecutionsSummary,
} from '../Interface';
import { mapGetters } from 'vuex';
import { loadLanguage } from '@/i18n';
import { loadLanguage, addNodeTranslation } from '@/i18n';
const NODE_SIZE = 100;
const DEFAULT_START_POSITION_X = 250;
@ -2384,8 +2384,16 @@ export default mixins(
if (nodesToBeFetched.length > 0) {
// Only call API if node information is actually missing
this.startLoading();
const nodeInfo = await this.restApi().getNodesInformation(nodesToBeFetched);
this.$store.commit('updateNodeTypes', nodeInfo);
const nodesInfo = await this.restApi().getNodesInformation(nodesToBeFetched);
nodesInfo.forEach(nodeInfo => {
if (nodeInfo.translation) {
addNodeTranslation(nodeInfo.translation, this.$store.getters.defaultLocale);
}
});
this.$store.commit('updateNodeTypes', nodesInfo);
this.stopLoading();
}
},

View File

@ -0,0 +1,22 @@
module.exports = {
bitwarden: {
credentialsModal: {
bitwardenApi: {
environment: {
displayName: '🇩🇪 Environment',
description: '🇩🇪 Description for environment',
options: {
cloudHosted: {
displayName: '🇩🇪 Cloud-hosted',
description: '🇩🇪 Description for cloud-hosted',
},
selfHosted: {
displayName: '🇩🇪 Self-hosted',
},
},
},
},
},
nodeView: {},
},
};

View File

@ -0,0 +1,165 @@
module.exports = {
github: {
credentialsModal: {
githubOAuth2Api: {
server: {
displayName: '🇩🇪 Github Server',
description: '🇩🇪 The server to connect to. Only has to be set if Github Enterprise is used.',
},
},
githubApi: {
server: {
displayName: '🇩🇪 Github Server',
description: '🇩🇪 The server to connect to. Only has to be set if Github Enterprise is used.',
},
user: {
placeholder: '🇩🇪 Hans',
},
accessToken: {
placeholder: '🇩🇪 123',
},
},
},
nodeView: {
/**
* Examples of `options` parameters.
*/
authentication: {
displayName: '🇩🇪 Authentication',
options: {
accessToken: {
displayName: '🇩🇪 Access Token',
},
oAuth2: {
displayName: '🇩🇪 OAuth2',
},
},
},
resource: {
displayName: '🇩🇪 Resource',
description: '🇩🇪 The resource to operate on.',
options: {
issue: {
displayName: '🇩🇪 Issue',
},
file: {
displayName: '🇩🇪 File',
},
repository: {
displayName: '🇩🇪 Repository',
},
release: {
displayName: '🇩🇪 Release',
},
review: {
displayName: '🇩🇪 Review',
},
user: {
displayName: '🇩🇪 User',
},
},
},
operation: {
displayName: '🇩🇪 Operation',
options: {
create: {
displayName: '🇩🇪 Create',
description: '🇩🇪 Create a new issue.',
},
get: {
displayName: '🇩🇪 Get',
description: '🇩🇪 Get the data of a single issue.',
},
},
},
/**
* Examples of `string` parameters.
*/
owner: {
displayName: '🇩🇪 Repository Owner',
placeholder: '🇩🇪 n8n-io',
description: '🇩🇪 Owner of the repository.',
},
repository: {
displayName: '🇩🇪 Repository Name',
placeholder: '🇩🇪 n8n',
},
title: {
displayName: '🇩🇪 Title',
},
body: {
displayName: '🇩🇪 Body',
},
/**
* Examples of `collection` parameters.
* `multipleValueButtonText` is the button label.
*/
labels: {
displayName: '🇩🇪 Labels',
multipleValueButtonText: '🇩🇪 Add Label',
},
assignees: {
displayName: '🇩🇪 Assignees',
multipleValueButtonText: '🇩🇪 Add Assignee',
},
/**
* Examples of fields in `collection` parameters.
* Note: Same level of nesting as `collection`.
*/
label: {
displayName: '🇩🇪 Label',
description: '🇩🇪 Label to add to issue.',
},
assignee: {
displayName: '🇩🇪 Assignee',
description: '🇩🇪 User to assign issue to.',
},
/**
* Example of a `fixedCollection` parameter.
* `placeholder` is the button label.
*/
additionalParameters: {
displayName: '🇩🇪 Additional Fields',
placeholder: '🇩🇪 Add Field',
options: {
author: {
displayName: '🇩🇪 Author',
},
branch: {
displayName: '🇩🇪 Branch',
},
committer: {
displayName: '🇩🇪 Committer',
},
},
},
/**
* Example of a field in a `fixedCollection` parameter.
* Note: Same level of nesting as `fixedCollection`.
*/
committer: {
displayName: '🇩🇪 Commit-Macher',
description: '🇩🇪 Beschreibung',
},
/**
* Examples of options in a field in a `fixedCollection` parameter.
* Note: Same level of nesting as `fixedCollection`.
*/
name: {
displayName: '🇩🇪 Name',
description: '🇩🇪 The name of the author of the commit.',
},
email: {
displayName: '🇩🇪 Email',
description: '🇩🇪 The email of the author of the commit.'
},
},
},
};

View File

@ -817,6 +817,7 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription {
deactivate?: INodeHookDescription[];
};
webhooks?: IWebhookDescription[];
translation?: { [key: string]: object };
}
export interface INodeHookDescription {

View File

@ -1390,7 +1390,7 @@ export function mergeNodeProperties(
}
}
export function getVersionedTypeNode(
export function getVersionedNodeType(
object: INodeVersionedType | INodeType,
version?: number,
): INodeType {
@ -1400,7 +1400,7 @@ export function getVersionedTypeNode(
return object as INodeType;
}
export function getVersionedTypeNodeAll(object: INodeVersionedType | INodeType): INodeType[] {
export function getVersionedNodeTypeAll(object: INodeVersionedType | INodeType): INodeType[] {
if (isNodeTypeVersioned(object)) {
return Object.values((object as INodeVersionedType).nodeVersions).map((element) => {
element.description.name = object.description.name;

View File

@ -99,7 +99,7 @@ class NodeTypesClass implements INodeTypes {
async init(nodeTypes: INodeTypeData): Promise<void> {}
getAll(): INodeType[] {
return Object.values(this.nodeTypes).map((data) => NodeHelpers.getVersionedTypeNode(data.type));
return Object.values(this.nodeTypes).map((data) => NodeHelpers.getVersionedNodeType(data.type));
}
getByName(nodeType: string): INodeType {
@ -107,7 +107,7 @@ class NodeTypesClass implements INodeTypes {
}
getByNameAndVersion(nodeType: string, version?: number): INodeType {
return NodeHelpers.getVersionedTypeNode(this.nodeTypes[nodeType].type, version);
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
}
}